From 042f2571591f1b27b31a3c7a1bba3f39fd814768 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 03:29:09 +0000 Subject: [PATCH 1/4] module: route the Node compile cache through the concurrent transpiler Enabling NODE_COMPILE_CACHE (or module.enableCompileCache()) forced every import through the synchronous transpile path so the cache's fetch hook would see each module. That path classifies a .cjs file with no CommonJS features as CommonJS from its extension alone, and CommonJS modules imported from ESM evaluate eagerly during graph loading, so enabling the cache flipped ESM evaluation order: a featureless .cjs statically imported after an .mjs sibling ran before it. The concurrent path classifies such a file as ESM and evaluates it in graph order. Add the compile cache fetch and parse-failure hooks to RuntimeTranspilerStore so concurrently transpiled modules read and populate the cache too, and drop the force-synchronous gate. The cache no longer changes which pipeline a module goes through, so evaluation order is identical with and without it, cold and warm. --- src/jsc/RuntimeTranspilerStore.rs | 53 ++++++++++++++++++- src/runtime/jsc_hooks.rs | 3 -- .../js/node/module/node-module-module.test.js | 33 ++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/jsc/RuntimeTranspilerStore.rs b/src/jsc/RuntimeTranspilerStore.rs index 016b9bd524a2..35c62c282a65 100644 --- a/src/jsc/RuntimeTranspilerStore.rs +++ b/src/jsc/RuntimeTranspilerStore.rs @@ -892,6 +892,17 @@ impl TranspilerJob { } } + // Node compile cache: record the failed module so exit-time + // persist logs the "was not initialized" skip (Node parity). + if crate::node_compile_cache::is_enabled() + && loader.is_java_script_like() + && path.is_file() + { + crate::node_compile_cache::note_parse_failure( + path.text, + !matches!(module_type, ModuleType::Esm), + ); + } self.parse_error = Some(crate::CrateError::ParseError); return; }; @@ -961,6 +972,25 @@ impl TranspilerJob { ptr::null_mut() }; + let is_commonjs_module = entry.metadata.module_type == CacheModuleType::Cjs; + // Node compile cache hook (transpiler-cache-hit path). UTF-16 + // output would hash differently than the print path, so skip it. + let node_compile_cache_blob = if crate::node_compile_cache::is_enabled() + && path.is_file() + && loader.is_java_script_like() + && !matches!(&entry.output_code, OutputCode::String(s) if s.is_utf16()) + { + crate::node_compile_cache::fetch( + path.text, + is_commonjs_module, + entry.output_code.byte_slice(), + ) + } else { + None + }; + let (bytecode_cache, bytecode_cache_size) = + node_compile_cache_blob.unwrap_or((ptr::null_mut(), 0)); + self.resolved_source = OwnedResolvedSource::from(ResolvedSource { source_code: match &mut entry.output_code { OutputCode::String(s) => *s, @@ -970,9 +1000,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() }); @@ -1132,6 +1164,23 @@ impl TranspilerJob { dump_source(vm, specifier, source_code_printer); } + // Node compile cache hook (concurrent transpile path). `fetch` copies + // the printed bytes, so replacing the print buffer below is safe. + let node_compile_cache_blob = if crate::node_compile_cache::is_enabled() + && path.is_file() + && loader.is_java_script_like() + { + crate::node_compile_cache::fetch( + path.text, + is_commonjs_module, + source_code_printer.ctx.get_written(), + ) + } else { + None + }; + let (bytecode_cache, bytecode_cache_size) = + node_compile_cache_blob.unwrap_or((ptr::null_mut(), 0)); + let source_code = 'brk: { let written = source_code_printer.ctx.get_written(); @@ -1172,6 +1221,8 @@ impl TranspilerJob { }) .unwrap_or(ptr::null_mut()), tag: this_tag, + bytecode_cache, + bytecode_cache_size, ..Default::default() }); diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 538ccf7e3715..4f0371b99c99 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -4401,9 +4401,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 diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index d0e722444fbd..ad453a25cfee 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -111,6 +111,39 @@ describe.concurrent("node-module-module", () => { expect(exitCode).toBe(0); }); + test("compile cache does not change ESM evaluation order", async () => { + // A `.cjs` sibling with no CommonJS features is loaded as ESM and must + // evaluate after the `.mjs` module imported before it, with or without + // the compile cache (cold and warm), and via the API enable path. + using dir = tempDir("compile-cache-eval-order", { + "main.mjs": `import "./e.mjs";\nimport "./c.cjs";\nconsole.log(globalThis.o.join(","));`, + "e.mjs": `(globalThis.o ??= []).push("esm");\nexport {};`, + "c.cjs": `(globalThis.o ??= []).push("cjs");`, + "preload.cjs": `require("node:module").enableCompileCache(__dirname + "/cc-api");`, + }); + const cacheDir = path.join(String(dir), "cc"); + const run = async (args, env) => { + await using proc = Bun.spawn({ + cmd: [bunExe(), ...args], + env: { ...bunEnv, ...env }, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + return stdout.trim(); + }; + expect(await run(["main.mjs"], {})).toBe("esm,cjs"); + expect(await run(["main.mjs"], { NODE_COMPILE_CACHE: cacheDir })).toBe("esm,cjs"); // cold + expect(await run(["main.mjs"], { NODE_COMPILE_CACHE: cacheDir })).toBe("esm,cjs"); // warm + expect(await run(["--preload", "./preload.cjs", "main.mjs"], {})).toBe("esm,cjs"); + // The cached runs only prove anything if the cache was actually active. + const tagged = fs.readdirSync(cacheDir); + expect(tagged).toHaveLength(1); + expect(fs.readdirSync(path.join(cacheDir, tagged[0])).length).toBeGreaterThanOrEqual(3); + }); + test.skipIf(process.platform === "win32")( "compile cache persists modules loaded after a non-fatal self-kill", async () => { From ad30d37c3ddb812cb102929b62d2b787990350f0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 03:51:35 +0000 Subject: [PATCH 2/4] Key parse-failure compile cache entries by extension like the sync path The concurrent path's module_type comes from package.json alone, so a parse-failed .mjs inside a "type": "commonjs" package was recorded (and logged under NODE_DEBUG_NATIVE=COMPILE_CACHE) as CommonJS while the synchronous path and Node record it as ESM. Sniff the extension first, consulting package.json only for .js/.ts, matching transpile_file. --- src/jsc/RuntimeTranspilerStore.rs | 15 +++++++++++---- .../js/node/module/node-module-module.test.js | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/jsc/RuntimeTranspilerStore.rs b/src/jsc/RuntimeTranspilerStore.rs index 35c62c282a65..47ad1004f232 100644 --- a/src/jsc/RuntimeTranspilerStore.rs +++ b/src/jsc/RuntimeTranspilerStore.rs @@ -898,10 +898,17 @@ impl TranspilerJob { && loader.is_java_script_like() && path.is_file() { - crate::node_compile_cache::note_parse_failure( - path.text, - !matches!(module_type, ModuleType::Esm), - ); + // `module_type` here comes from package.json alone; mirror the + // synchronous path's extension sniff (transpile_file) so both + // paths record the same type: the extension wins over + // package.json "type", and only .js/.ts consult it. + let is_cjs = match path.name().ext { + b".cjs" | b".cts" => true, + b".mjs" | b".mts" => false, + b".js" | b".ts" => !matches!(module_type, ModuleType::Esm), + _ => true, + }; + crate::node_compile_cache::note_parse_failure(path.text, is_cjs); } self.parse_error = Some(crate::CrateError::ParseError); return; diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index ad453a25cfee..c33df08980b5 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -144,6 +144,25 @@ describe.concurrent("node-module-module", () => { expect(fs.readdirSync(path.join(cacheDir, tagged[0])).length).toBeGreaterThanOrEqual(3); }); + test("compile cache records a parse-failed .mjs as ESM regardless of package type", async () => { + // Parse-failure bookkeeping keys off the file extension like the + // synchronous loader and Node, not the enclosing package.json "type". + using dir = tempDir("compile-cache-parse-failure-type", { + "main.mjs": `import "./pkg/broken.mjs";`, + "pkg/package.json": `{"type":"commonjs"}`, + "pkg/broken.mjs": `import {;`, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "main.mjs"], + env: { ...bunEnv, NODE_COMPILE_CACHE: path.join(String(dir), "cc"), NODE_DEBUG_NATIVE: "COMPILE_CACHE" }, + cwd: String(dir), + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(stderr).toMatch(/skip persisting ESM file:.*broken\.mjs because the cache was not initialized/); + expect(exitCode).not.toBe(0); + }); + test.skipIf(process.platform === "win32")( "compile cache persists modules loaded after a non-fatal self-kill", async () => { From f46945befe024fb9c03dd1d0784187f83197051c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 04:00:58 +0000 Subject: [PATCH 3/4] Deduplicate the compile cache hooks and the module_type extension sniff Move the is-enabled/is-file/JS-like guards into node_compile_cache (fetch_for_transpiled_module, note_parse_failure_for_module) so the five call sites across the synchronous and concurrent transpile paths share one definition, and extract the extension sniff into ModuleType::from_extension, used by both transpile_file and the concurrent path's parse-failure hook. Test feedback: spawn cache runs with NODE_COMPILE_CACHE cleared from the inherited env, assert stdout before exit codes, and verify the API enable path populated its own cache directory. --- src/jsc/NodeCompileCache.rs | 35 ++++- src/jsc/RuntimeTranspilerStore.rs | 72 ++++------ src/options_types/bundle_enums.rs | 14 ++ src/runtime/jsc_hooks.rs | 128 +++++------------- .../js/node/module/node-module-module.test.js | 24 ++-- 5 files changed, 123 insertions(+), 150 deletions(-) diff --git a/src/jsc/NodeCompileCache.rs b/src/jsc/NodeCompileCache.rs index 40d9b313a2e0..353dec9d3d5d 100644 --- a/src/jsc/NodeCompileCache.rs +++ b/src/jsc/NodeCompileCache.rs @@ -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}; @@ -511,6 +512,38 @@ pub fn get_dir() -> Option> { // 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. +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. +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). diff --git a/src/jsc/RuntimeTranspilerStore.rs b/src/jsc/RuntimeTranspilerStore.rs index 47ad1004f232..c353641c9312 100644 --- a/src/jsc/RuntimeTranspilerStore.rs +++ b/src/jsc/RuntimeTranspilerStore.rs @@ -892,24 +892,13 @@ impl TranspilerJob { } } - // Node compile cache: record the failed module so exit-time - // persist logs the "was not initialized" skip (Node parity). - if crate::node_compile_cache::is_enabled() - && loader.is_java_script_like() - && path.is_file() - { - // `module_type` here comes from package.json alone; mirror the - // synchronous path's extension sniff (transpile_file) so both - // paths record the same type: the extension wins over - // package.json "type", and only .js/.ts consult it. - let is_cjs = match path.name().ext { - b".cjs" | b".cts" => true, - b".mjs" | b".mts" => false, - b".js" | b".ts" => !matches!(module_type, ModuleType::Esm), - _ => true, - }; - crate::node_compile_cache::note_parse_failure(path.text, is_cjs); - } + // Extension-sniffed, not the package.json-only `module_type` the + // parse used, so both transpile paths record the same type. + 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; }; @@ -980,23 +969,19 @@ impl TranspilerJob { }; let is_commonjs_module = entry.metadata.module_type == CacheModuleType::Cjs; - // Node compile cache hook (transpiler-cache-hit path). UTF-16 - // output would hash differently than the print path, so skip it. - let node_compile_cache_blob = if crate::node_compile_cache::is_enabled() - && path.is_file() - && loader.is_java_script_like() - && !matches!(&entry.output_code, OutputCode::String(s) if s.is_utf16()) - { - crate::node_compile_cache::fetch( - path.text, - is_commonjs_module, - entry.output_code.byte_slice(), - ) - } else { - None - }; + // UTF-16 transpiler-cache output cannot byte-match the printed + // form, so it never reaches the compile cache. let (bytecode_cache, bytecode_cache_size) = - node_compile_cache_blob.unwrap_or((ptr::null_mut(), 0)); + 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 { @@ -1171,22 +1156,13 @@ impl TranspilerJob { dump_source(vm, specifier, source_code_printer); } - // Node compile cache hook (concurrent transpile path). `fetch` copies - // the printed bytes, so replacing the print buffer below is safe. - let node_compile_cache_blob = if crate::node_compile_cache::is_enabled() - && path.is_file() - && loader.is_java_script_like() - { - crate::node_compile_cache::fetch( - path.text, + 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(), - ) - } else { - None - }; - let (bytecode_cache, bytecode_cache_size) = - node_compile_cache_blob.unwrap_or((ptr::null_mut(), 0)); + ); let source_code = 'brk: { let written = source_code_printer.ctx.get_written(); diff --git a/src/options_types/bundle_enums.rs b/src/options_types/bundle_enums.rs index ba7eb1429491..7b99529fc315 100644 --- a/src/options_types/bundle_enums.rs +++ b/src/options_types/bundle_enums.rs @@ -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. + 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! { diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 4f0371b99c99..835200505b03 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -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`. @@ -2608,9 +2593,7 @@ 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); }; @@ -2664,9 +2647,7 @@ 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); } @@ -2855,22 +2836,19 @@ 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()) - { - bun_jsc::node_compile_cache::fetch( - source.path.text, - is_commonjs_module, - entry.output_code.byte_slice(), - ) - } else { - None - }; + // UTF-16 transpiler-cache output cannot byte-match the + // printed form, so it never reaches the compile cache. + let (bytecode_cache, bytecode_cache_size) = + if matches!(&entry.output_code, OutputCode::String(s) if s.is_utf16()) { + (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(), + ) + }; let source_code = match &mut entry.output_code { OutputCode::String(s) => *s, OutputCode::Utf8(utf8) => { @@ -2929,8 +2907,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(), @@ -3121,14 +3097,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 { @@ -3141,10 +3116,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)); } @@ -3205,16 +3178,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 @@ -3236,8 +3206,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(), @@ -4344,34 +4312,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)); diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index c33df08980b5..374c29a6fd3f 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -122,26 +122,29 @@ describe.concurrent("node-module-module", () => { "preload.cjs": `require("node:module").enableCompileCache(__dirname + "/cc-api");`, }); const cacheDir = path.join(String(dir), "cc"); + const apiCacheDir = path.join(String(dir), "cc-api"); const run = async (args, env) => { await using proc = Bun.spawn({ cmd: [bunExe(), ...args], - env: { ...bunEnv, ...env }, + env: { ...bunEnv, NODE_COMPILE_CACHE: undefined, ...env }, cwd: String(dir), stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout.trim()).toBe("esm,cjs"); expect(stderr).toBe(""); expect(exitCode).toBe(0); - return stdout.trim(); }; - expect(await run(["main.mjs"], {})).toBe("esm,cjs"); - expect(await run(["main.mjs"], { NODE_COMPILE_CACHE: cacheDir })).toBe("esm,cjs"); // cold - expect(await run(["main.mjs"], { NODE_COMPILE_CACHE: cacheDir })).toBe("esm,cjs"); // warm - expect(await run(["--preload", "./preload.cjs", "main.mjs"], {})).toBe("esm,cjs"); + await run(["main.mjs"], {}); + await run(["main.mjs"], { NODE_COMPILE_CACHE: cacheDir }); // cold + await run(["main.mjs"], { NODE_COMPILE_CACHE: cacheDir }); // warm + await run(["--preload", "./preload.cjs", "main.mjs"], {}); // The cached runs only prove anything if the cache was actually active. - const tagged = fs.readdirSync(cacheDir); - expect(tagged).toHaveLength(1); - expect(fs.readdirSync(path.join(cacheDir, tagged[0])).length).toBeGreaterThanOrEqual(3); + for (const dirToCheck of [cacheDir, apiCacheDir]) { + const tagged = fs.readdirSync(dirToCheck); + expect(tagged).toHaveLength(1); + expect(fs.readdirSync(path.join(dirToCheck, tagged[0])).length).toBeGreaterThanOrEqual(3); + } }); test("compile cache records a parse-failed .mjs as ESM regardless of package type", async () => { @@ -158,7 +161,8 @@ describe.concurrent("node-module-module", () => { cwd: String(dir), stderr: "pipe", }); - const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toBe(""); expect(stderr).toMatch(/skip persisting ESM file:.*broken\.mjs because the cache was not initialized/); expect(exitCode).not.toBe(0); }); From 45a8d6a6855b81e7a399d4fe473a2492755d040f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 04:03:18 +0000 Subject: [PATCH 4/4] [autofix.ci] apply automated fixes --- src/jsc/RuntimeTranspilerStore.rs | 22 ++++++++++---------- src/runtime/jsc_hooks.rs | 34 +++++++++++++++++++------------ 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/jsc/RuntimeTranspilerStore.rs b/src/jsc/RuntimeTranspilerStore.rs index c353641c9312..a2b6f59cce20 100644 --- a/src/jsc/RuntimeTranspilerStore.rs +++ b/src/jsc/RuntimeTranspilerStore.rs @@ -971,17 +971,17 @@ impl TranspilerJob { 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. - 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(), - ) - }; + 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 { diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 835200505b03..79dbb1b87f0d 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -2593,7 +2593,11 @@ fn transpile_source_code_inner( ); } arena_guard.2 = false; // give_back_arena = false - bun_jsc::node_compile_cache::note_parse_failure_for_module(path, loader, module_type); + bun_jsc::node_compile_cache::note_parse_failure_for_module( + path, + loader, + module_type, + ); return Err(crate::Error::ParseError); }; @@ -2647,7 +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; - bun_jsc::node_compile_cache::note_parse_failure_for_module(path, loader, module_type); + bun_jsc::node_compile_cache::note_parse_failure_for_module( + path, + loader, + module_type, + ); return Err(crate::Error::ParseError); } @@ -2838,17 +2846,17 @@ fn transpile_source_code_inner( 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. - let (bytecode_cache, bytecode_cache_size) = - if matches!(&entry.output_code, OutputCode::String(s) if s.is_utf16()) { - (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(), - ) - }; + let (bytecode_cache, bytecode_cache_size) = if matches!(&entry.output_code, OutputCode::String(s) if s.is_utf16()) + { + (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(), + ) + }; let source_code = match &mut entry.output_code { OutputCode::String(s) => *s, OutputCode::Utf8(utf8) => {