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
35 changes: 34 additions & 1 deletion src/jsc/NodeCompileCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
use core::sync::atomic::{AtomicBool, AtomicU8, Ordering};

use bstr::ByteSlice;
use bun_ast::Loader;
use bun_boringssl::c as boring;
use bun_collections::{HashMap, IdentityContext};
use bun_core::String as BunString;
use bun_core::{Mutex, ZStr, env_var};
use bun_options_types::Format;
use bun_options_types::{Format, ModuleType};
use bun_paths::{MAX_PATH_BYTES, PathBuffer, SEP};
use bun_sys::{self as sys, Fd, O};

Expand Down Expand Up @@ -511,6 +512,38 @@ pub fn get_dir() -> Option<Vec<u8>> {
// Fetch-time hook (read + validate)
// ──────────────────────────────────────────────────────────────────────────

/// Guarded [`fetch`] for the transpile paths (synchronous and concurrent):
/// only file-backed, JS-like modules participate. `code` is the exact
/// post-transpile byte text and is copied, so callers may reuse or replace
/// their buffer afterwards. Returns `(null, 0)` when the cache is disabled,
/// the module does not participate, or no on-disk entry validates.
Comment thread
robobun marked this conversation as resolved.
pub fn fetch_for_transpiled_module(
path: &bun_paths::fs::Path<'_>,
loader: Loader,
is_cjs: bool,
code: &[u8],
) -> (*mut u8, usize) {
if !is_enabled() || !path.is_file() || !loader.is_java_script_like() {
return (core::ptr::null_mut(), 0);
}
fetch(path.text, is_cjs, code).unwrap_or((core::ptr::null_mut(), 0))
}

/// Guarded [`note_parse_failure`] for the transpile paths: register the
/// failed module so exit-time persist logs the "was not initialized" skip
/// (Node parity). `module_type` is the extension-sniffed type
/// ([`ModuleType::from_extension`]); `Unknown` records as CommonJS, like Node.
Comment thread
robobun marked this conversation as resolved.
pub fn note_parse_failure_for_module(
path: &bun_paths::fs::Path<'_>,
loader: Loader,
module_type: ModuleType,
) {
if !is_enabled() || !path.is_file() || !loader.is_java_script_like() {
return;
}
note_parse_failure(path.text, !matches!(module_type, ModuleType::Esm));
}

/// Module-fetch hook: register/refresh the entry for `filename`; returns the
/// validated bytecode blob when the on-disk cache matches `code` (post-
/// transpile text). The pointer stays valid for the process (entry map owns it).
Expand Down
36 changes: 35 additions & 1 deletion src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,13 @@ impl TranspilerJob {
}
}

// Extension-sniffed, not the package.json-only `module_type` the
// parse used, so both transpile paths record the same type.
Comment thread
robobun marked this conversation as resolved.
crate::node_compile_cache::note_parse_failure_for_module(
&path,
loader,
ModuleType::from_extension(path.name().ext, module_type),
);
self.parse_error = Some(crate::CrateError::ParseError);
return;
};
Expand Down Expand Up @@ -961,6 +968,21 @@ impl TranspilerJob {
ptr::null_mut()
};

let is_commonjs_module = entry.metadata.module_type == CacheModuleType::Cjs;
// UTF-16 transpiler-cache output cannot byte-match the printed
// form, so it never reaches the compile cache.
Comment thread
robobun marked this conversation as resolved.
let (bytecode_cache, bytecode_cache_size) = if matches!(&entry.output_code, OutputCode::String(s) if s.is_utf16())
{
(ptr::null_mut(), 0)
} else {
crate::node_compile_cache::fetch_for_transpiled_module(
&path,
loader,
is_commonjs_module,
entry.output_code.byte_slice(),
)
};

self.resolved_source = OwnedResolvedSource::from(ResolvedSource {
source_code: match &mut entry.output_code {
OutputCode::String(s) => *s,
Expand All @@ -970,9 +992,11 @@ impl TranspilerJob {
result
}
},
is_commonjs_module: entry.metadata.module_type == CacheModuleType::Cjs,
is_commonjs_module,
module_info,
tag: this_tag,
bytecode_cache,
bytecode_cache_size,
..Default::default()
});

Expand Down Expand Up @@ -1132,6 +1156,14 @@ impl TranspilerJob {
dump_source(vm, specifier, source_code_printer);
}

let (bytecode_cache, bytecode_cache_size) =
crate::node_compile_cache::fetch_for_transpiled_module(
&path,
loader,
is_commonjs_module,
source_code_printer.ctx.get_written(),
);

let source_code = 'brk: {
let written = source_code_printer.ctx.get_written();

Expand Down Expand Up @@ -1172,6 +1204,8 @@ impl TranspilerJob {
})
.unwrap_or(ptr::null_mut()),
tag: this_tag,
bytecode_cache,
bytecode_cache_size,
..Default::default()
});

Expand Down
14 changes: 14 additions & 0 deletions src/options_types/bundle_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ pub enum ModuleType {

impl ModuleType {
pub const LIST: __ComptimeStringMap_MODULE_TYPE_LIST = __ComptimeStringMap_MODULE_TYPE_LIST(());

/// Module type from a file extension (dot included). The extension is
/// authoritative; `package_json_type` (the enclosing package.json
/// `"type"`, [`ModuleType::Unknown`] when absent) applies only to
/// `.js`/`.ts`, and other extensions (`.jsx`, `.tsx`, ...) stay
/// [`ModuleType::Unknown`] so the file contents decide.
Comment thread
robobun marked this conversation as resolved.
pub fn from_extension(ext: &[u8], package_json_type: ModuleType) -> ModuleType {
match ext {
b".cjs" | b".cts" => ModuleType::Cjs,
b".mjs" | b".mts" => ModuleType::Esm,
b".js" | b".ts" => package_json_type,
_ => ModuleType::Unknown,
}
}
}

bun_core::comptime_string_map! {
Expand Down
129 changes: 40 additions & 89 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2025,21 +2025,6 @@ fn to_jsc_fetch_error(err: &crate::Error) -> bun_jsc::CrateError {
_ => bun_jsc::CrateError::ParseError,
}
}
/// Shared guard for the two parse-failure exits: register the module with the
/// Node compile cache (Unknown module type maps to CJS, matching Node).
fn note_compile_cache_parse_failure(
path: &bun_resolver::fs::Path<'_>,
loader: Loader,
module_type: ModuleType,
) {
if bun_jsc::node_compile_cache::is_enabled() && loader.is_java_script_like() && path.is_file() {
bun_jsc::node_compile_cache::note_parse_failure(
path.text,
!matches!(module_type, ModuleType::Esm),
);
}
}

/// `ModuleLoader.transpileSourceCode(...)` — the runtime-transpiler path:
/// read file → `Transpiler::parse`
/// → `js_printer::print` → `ResolvedSource`.
Expand Down Expand Up @@ -2608,9 +2593,11 @@ fn transpile_source_code_inner(
);
}
arena_guard.2 = false; // give_back_arena = false
// Node compile cache: record the failed module so exit-time
// persist logs the "was not initialized" skip (Node parity).
note_compile_cache_parse_failure(path, loader, module_type);
bun_jsc::node_compile_cache::note_parse_failure_for_module(
path,
loader,
module_type,
);
return Err(crate::Error::ParseError);
};

Expand Down Expand Up @@ -2664,9 +2651,11 @@ fn transpile_source_code_inner(
// `transpiler.log` was swapped to non-null `args.log` above.
if unsafe { (*(*jsc_vm).transpiler.log).errors > 0 } {
arena_guard.2 = false;
// Node compile cache: record the failed module so exit-time
// persist logs the "was not initialized" skip (Node parity).
note_compile_cache_parse_failure(path, loader, module_type);
bun_jsc::node_compile_cache::note_parse_failure_for_module(
path,
loader,
module_type,
);
return Err(crate::Error::ParseError);
}

Expand Down Expand Up @@ -2855,21 +2844,18 @@ fn transpile_source_code_inner(
core::ptr::null_mut()
};
let is_commonjs_module = entry.metadata.module_type == CacheModuleType::Cjs;
// Node compile cache hook (transpiler-cache-hit path); must
// read `output_code` before it is consumed below. UTF-16
// output would hash differently than the print path — skip.
let node_compile_cache_blob = if bun_jsc::node_compile_cache::is_enabled()
&& source.path.is_file()
&& loader.is_java_script_like()
&& !matches!(&entry.output_code, OutputCode::String(s) if s.is_utf16())
// UTF-16 transpiler-cache output cannot byte-match the
// printed form, so it never reaches the compile cache.
Comment thread
robobun marked this conversation as resolved.
let (bytecode_cache, bytecode_cache_size) = if matches!(&entry.output_code, OutputCode::String(s) if s.is_utf16())
{
bun_jsc::node_compile_cache::fetch(
source.path.text,
(core::ptr::null_mut(), 0)
} else {
bun_jsc::node_compile_cache::fetch_for_transpiled_module(
&source.path,
loader,
is_commonjs_module,
entry.output_code.byte_slice(),
)
} else {
None
};
let source_code = match &mut entry.output_code {
OutputCode::String(s) => *s,
Expand Down Expand Up @@ -2929,8 +2915,6 @@ fn transpile_source_code_inner(
} else {
ResolvedSourceTag::Javascript
};
let (bytecode_cache, bytecode_cache_size) =
node_compile_cache_blob.unwrap_or((core::ptr::null_mut(), 0));
return Ok(OwnedResolvedSource::from(ResolvedSource {
source_code,
specifier: input_specifier.dupe_ref(),
Expand Down Expand Up @@ -3121,14 +3105,13 @@ fn transpile_source_code_inner(
let printer: &mut bun_js_printer::BufferPrinter =
unsafe { &mut *(*extra).source_code_printer };
let written = printer.ctx.get_written();
let node_compile_cache_blob = if bun_jsc::node_compile_cache::is_enabled()
&& path.is_file()
&& loader.is_java_script_like()
{
bun_jsc::node_compile_cache::fetch(path.text, is_commonjs_module, written)
} else {
None
};
let (bytecode_cache, bytecode_cache_size) =
bun_jsc::node_compile_cache::fetch_for_transpiled_module(
path,
loader,
is_commonjs_module,
written,
);
// SAFETY: per fn contract — `jsc_vm` is the live per-thread
// VM; `printer.ctx.get_written()` borrows thread-local data.
let mut resolved_source = unsafe {
Expand All @@ -3141,10 +3124,8 @@ fn transpile_source_code_inner(
};
resolved_source.is_commonjs_module = is_commonjs_module;
resolved_source.module_info = module_info;
if let Some((ptr, size)) = node_compile_cache_blob {
resolved_source.bytecode_cache = ptr;
resolved_source.bytecode_cache_size = size;
}
resolved_source.bytecode_cache = bytecode_cache;
resolved_source.bytecode_cache_size = bytecode_cache_size;
return Ok(OwnedResolvedSource::from(resolved_source));
}

Expand Down Expand Up @@ -3205,16 +3186,13 @@ fn transpile_source_code_inner(
let printer: &mut bun_js_printer::BufferPrinter =
unsafe { &mut *(*extra).source_code_printer };
let written = printer.ctx.get_written();
// Node compile cache hook (sync transpile path). `fetch` copies
// `written`; the printer may be replaced below.
let node_compile_cache_blob = if bun_jsc::node_compile_cache::is_enabled()
&& path.is_file()
&& loader.is_java_script_like()
{
bun_jsc::node_compile_cache::fetch(path.text, is_commonjs_module, written)
} else {
None
};
let (bytecode_cache, bytecode_cache_size) =
bun_jsc::node_compile_cache::fetch_for_transpiled_module(
path,
loader,
is_commonjs_module,
written,
);
// The `Jsc` vtable bridge `put()` does not write
// `cache.output_code` (only the `r#impl == None` fallback
// does, and `r#impl` is `Some(Jsc)` here), so it is always
Expand All @@ -3236,8 +3214,6 @@ fn transpile_source_code_inner(
// (fd close handled by `_fd_guard` registered above; spec
// :251-256 `defer` fires on every exit path.)

let (bytecode_cache, bytecode_cache_size) =
node_compile_cache_blob.unwrap_or((core::ptr::null_mut(), 0));
return Ok(OwnedResolvedSource::from(ResolvedSource {
source_code,
specifier: input_specifier.dupe_ref(),
Expand Down Expand Up @@ -4344,34 +4320,12 @@ unsafe fn transpile_file(
}

// ── module_type sniff from extension / package.json ─────────────────────
let module_type: ModuleType = 'brk: {
let ext = lr.path.name().ext;
// regex /\.[cm][jt]s$/
if ext.len() == b".cjs".len() {
if ext == b".cjs" {
break 'brk ModuleType::Cjs;
}
if ext == b".mjs" {
break 'brk ModuleType::Esm;
}
if ext == b".cts" {
break 'brk ModuleType::Cjs;
}
if ext == b".mts" {
break 'brk ModuleType::Esm;
}
}
// regex /\.[jt]s$/
if ext.len() == b".ts".len() && (ext == b".js" || ext == b".ts") {
// Use the package.json module type if it exists.
break 'brk lr
.package_json
.map(|pkg| pkg.module_type)
.unwrap_or(ModuleType::Unknown);
}
// For JSX/TSX and other extensions, let the file contents decide.
ModuleType::Unknown
};
let module_type = ModuleType::from_extension(
lr.path.name().ext,
lr.package_json
.map(|pkg| pkg.module_type)
.unwrap_or(ModuleType::Unknown),
);
let pkg_name: Option<&[u8]> = lr
.package_json
.and_then(|pkg| (!pkg.name.is_empty()).then_some(&*pkg.name));
Expand Down Expand Up @@ -4401,9 +4355,6 @@ unsafe fn transpile_file(
// TODO: allow running concurrently when no onLoad handlers match a plugin.
&& plugin_runner_is_none
&& store_enabled
// With the Node compile cache enabled, transpile on-thread so the
// fetch hook sees every module.
&& !bun_jsc::node_compile_cache::is_enabled()
{
// Disgusting workaround: polyfills like
// `reflect-metadata` are CJS-with-side-effects that other ESM
Expand Down
Loading