From 954f4cde71f7ab33e1dfafcf08cee3557c7ef8bc Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:32:43 +0000 Subject: [PATCH 1/5] bundler: pass the real import.meta to cjs output for the bun target --- src/ast/e.rs | 5 + src/bundler/LinkerContext.rs | 8 + src/bundler/ParseTask.rs | 4 + src/bundler/bundle_v2.rs | 7 +- .../generateCodeForFileInChunkJS.rs | 8 +- .../generateCompileResultForJSChunk.rs | 2 + .../linker_context/postProcessJSChunk.rs | 55 +++--- .../linker_context/renameSymbolsInChunk.rs | 5 + src/bundler/transpiler.rs | 1 + src/js_parser/fold.rs | 8 +- src/js_parser/p.rs | 2 +- src/js_parser/parse/parse_entry.rs | 6 + src/js_printer/lib.rs | 32 ++-- test/bundler/bundler_bun.test.ts | 158 ++++++++++++++++++ 14 files changed, 253 insertions(+), 48 deletions(-) diff --git a/src/ast/e.rs b/src/ast/e.rs index 7ae43d52aae1..94a4cd63e251 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -201,6 +201,11 @@ pub struct Super; #[derive(Clone, Copy, Default)] pub struct ImportMeta; +impl ImportMeta { + /// Sixth parameter of the CommonJS wrapper. JSCommonJSModule.cpp passes `import.meta` for it. + pub const CJS_WRAPPER_ARG: &'static [u8] = b"$Bun_import_meta"; +} + #[derive(Clone, Copy, Default)] pub struct ImportMetaMain { /// If true, print `!import.meta.main` (or `require.main != module`). diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 5d8576f64d36..3d834840e882 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -2313,6 +2313,12 @@ impl<'a> LinkerContext<'a> { Ok(true) } + /// The one place that decides whether a chunk gets the `@bun-cjs` function wrapper. + pub(crate) fn chunk_has_bun_cjs_wrapper(&self, chunk: &Chunk) -> bool { + self.options.output_format == Format::Cjs + && self.graph.ast.items_target()[chunk.entry_point.source_index() as usize].is_bun() + } + pub(crate) fn print_code_for_file_in_chunk_js( &mut self, r: renamer::Renamer, @@ -2324,6 +2330,7 @@ impl<'a> LinkerContext<'a> { to_esm_ref: Ref, to_commonjs_ref: Ref, runtime_require_ref: Option, + inside_bun_cjs_wrapper: bool, source_index: Index, source: &Source, module_info: Option<&mut crate::analyze_transpiled_module::ModuleInfo>, @@ -2403,6 +2410,7 @@ impl<'a> LinkerContext<'a> { Format::Cjs => None, // use unbounded global _ => runtime_require_ref, }, + inside_bun_cjs_wrapper, require_or_import_meta_for_source_callback: js_printer::RequireOrImportMetaCallback::init(self), line_offset_tables: Some(line_offset_table), diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index bf59458d4fd6..4f94be88c4e2 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -2639,6 +2639,10 @@ pub mod parse_worker { opts.lower_import_meta_main_for_node_js = true; } + // For bun, cjs output gets import.meta from the `@bun-cjs` wrapper instead. + opts.inline_import_meta_paths = topts.framework.is_some() + || (output_format == options::Format::Cjs && !target.is_bun()); + opts.tree_shaking = if task.source_index.is_runtime() { true } else { diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 00ba44ff481f..4b77f8b08780 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -7766,10 +7766,9 @@ pub mod bv2_impl { fn default() -> Self { CompileResult::Javascript { source_index: 0, - result: bun_js_printer::PrintResult::Result(bun_js_printer::PrintResultSuccess { - code: Box::new([]), - source_map: None, - }), + result: bun_js_printer::PrintResult::Result( + bun_js_printer::PrintResultSuccess::default(), + ), module_info: None, } } diff --git a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs index f7ba8e07ca3c..6b22eaf4dcec 100644 --- a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs +++ b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs @@ -31,6 +31,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( to_common_js_ref: Ref, to_esm_ref: Ref, runtime_require_ref: Option, + inside_bun_cjs_wrapper: bool, stmts: &mut StmtList, arena: &Bump, temp_arena: &Bump, @@ -226,6 +227,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( Ref::NONE, Ref::NONE, None, + false, part_range.source_index, source, module_info, @@ -967,10 +969,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( let out_stmts: &mut [Stmt] = out_stmts.slice_mut(); if out_stmts.is_empty() { - return PrintResult::Result(PrintResultSuccess { - code: Box::new([]), - source_map: None, - }); + return PrintResult::Result(PrintResultSuccess::default()); } // `get_source` returns `&'static Source` (parse_graph SoA is append-only and @@ -987,6 +986,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( to_esm_ref, to_common_js_ref, runtime_require_ref, + inside_bun_cjs_wrapper, part_range.source_index, source, module_info, diff --git a/src/bundler/linker_context/generateCompileResultForJSChunk.rs b/src/bundler/linker_context/generateCompileResultForJSChunk.rs index 5219d91ef861..eb11053da137 100644 --- a/src/bundler/linker_context/generateCompileResultForJSChunk.rs +++ b/src/bundler/linker_context/generateCompileResultForJSChunk.rs @@ -121,6 +121,7 @@ fn generate_compile_result_for_js_chunk_impl( .follow(runtime_members.get(b"__require".as_slice()).unwrap().ref_), ) }; + let inside_bun_cjs_wrapper = c.chunk_has_bun_cjs_wrapper(chunk); // `worker.arena` (= `BackRef` to `worker.heap`) is a disjoint field from // `worker.temporary_arena` / `worker.stmt_list` borrowed `&mut` above, so @@ -149,6 +150,7 @@ fn generate_compile_result_for_js_chunk_impl( to_common_js_ref, to_esm_ref, runtime_require_ref, + inside_bun_cjs_wrapper, stmt_list, worker_alloc, &**arena, diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index 2e7da0c1c7fb..414d32acc1a1 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -99,6 +99,18 @@ fn module_preload_registration( Ok(code) } +fn chunk_uses_import_meta_arg(chunk: &Chunk) -> bool { + chunk.compile_results_for_chunk.iter().any(|result| { + matches!( + result, + CompileResult::Javascript { + result: PrintResult::Result(printed), + .. + } if printed.uses_import_meta_arg + ) + }) +} + /// This runs after we've already populated the compile results pub(crate) fn post_process_js_chunk( ctx: GenerateChunkCtx, @@ -361,10 +373,7 @@ pub(crate) fn post_process_js_chunk( break 'brk CompileResult::Javascript { source_index: Index::INVALID.value(), - result: PrintResult::Result(js_printer::PrintResultSuccess { - code: Box::default(), - source_map: None, - }), + result: PrintResult::Result(js_printer::PrintResultSuccess::default()), module_info: None, }; }; @@ -441,20 +450,27 @@ pub(crate) fn post_process_js_chunk( // Add @bun comments and CJS wrapper start for each chunk when targeting Bun. let is_bun = c.graph.ast.items_target()[chunk.entry_point.source_index() as usize].is_bun(); - if is_bun { - if c.options.generate_bytecode_cache && output_format == options::OutputFormat::Cjs { - const INPUT: &[u8] = - b"// @bun @bytecode @bun-cjs\n(function(exports, require, module, __filename, __dirname) {"; - j.push_static(INPUT); - line_offset.advance(INPUT); - } else if c.options.generate_bytecode_cache { + let has_bun_cjs_wrapper = c.chunk_has_bun_cjs_wrapper(chunk); + if has_bun_cjs_wrapper { + let mut push = |bytes: &'static [u8]| { + j.push_static(bytes); + line_offset.advance(bytes); + }; + push(if c.options.generate_bytecode_cache { + b"// @bun @bytecode @bun-cjs\n" + } else { + b"// @bun @bun-cjs\n" + }); + push(b"(function(exports, require, module, __filename, __dirname"); + if chunk_uses_import_meta_arg(chunk) { + push(b", "); + push(E::ImportMeta::CJS_WRAPPER_ARG); + } + push(b") {"); + } else if is_bun { + if c.options.generate_bytecode_cache { j.push_static(b"// @bun @bytecode\n"); line_offset.advance(b"// @bun @bytecode\n"); - } else if output_format == options::OutputFormat::Cjs { - const INPUT: &[u8] = - b"// @bun @bun-cjs\n(function(exports, require, module, __filename, __dirname) {"; - j.push_static(INPUT); - line_offset.advance(INPUT); } else { j.push_static(b"// @bun\n"); line_offset.advance(b"// @bun\n"); @@ -749,7 +765,7 @@ pub(crate) fn post_process_js_chunk( } } options::OutputFormat::Cjs => { - if is_bun { + if has_bun_cjs_wrapper { j.push_static(b"})\n"); line_offset.advance(b"})\n"); } @@ -1260,10 +1276,7 @@ pub(crate) fn generate_entry_point_tail_js<'a>( if stmts.is_empty() { return CompileResult::Javascript { source_index, - result: PrintResult::Result(js_printer::PrintResultSuccess { - code: Box::default(), - source_map: None, - }), + result: PrintResult::Result(js_printer::PrintResultSuccess::default()), module_info: None, }; } diff --git a/src/bundler/linker_context/renameSymbolsInChunk.rs b/src/bundler/linker_context/renameSymbolsInChunk.rs index ec762073007c..4c335a5f726b 100644 --- a/src/bundler/linker_context/renameSymbolsInChunk.rs +++ b/src/bundler/linker_context/renameSymbolsInChunk.rs @@ -59,6 +59,7 @@ pub(crate) unsafe fn rename_symbols_in_chunk( // borrow does not assert immutability over the heap cells written below. // SAFETY: see fn safety doc — `c` is live for the call. let c: &LinkerContext<'_> = unsafe { &*c }; + let has_bun_cjs_wrapper = c.chunk_has_bun_cjs_wrapper(chunk); // ── raw SoA column pointers (root provenance) ──────────────────────── // `split_raw()` derives `*mut [T]` directly from the buffer base with no @@ -144,6 +145,10 @@ pub(crate) unsafe fn rename_symbols_in_chunk( }; let mut reserved_names = renamer::compute_initial_reserved_names(c.options.output_format)?; + if has_bun_cjs_wrapper { + // The printer refers to this wrapper parameter by name. + reserved_names.put(bun_ast::E::ImportMeta::CJS_WRAPPER_ARG, 1)?; + } for &source_index in files_in_order { renamer::compute_reserved_names_for_scope( &all_module_scopes[source_index as usize], diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 125a51b87f90..339fc0f54d71 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1598,6 +1598,7 @@ impl<'a> Transpiler<'a> { transform_only: self.options.transform_only, import_meta_main_value: None, lower_import_meta_main_for_node_js: false, + inline_import_meta_paths: false, framework: None, repl_mode: self.options.repl_mode, lower_toml_datetimes: false, diff --git a/src/js_parser/fold.rs b/src/js_parser/fold.rs index ac17087f399a..feb26f90787a 100644 --- a/src/js_parser/fold.rs +++ b/src/js_parser/fold.rs @@ -3,7 +3,7 @@ use bun_collections::VecExt; use bun_core::feature_flags as FeatureFlags; use crate::p::P; -use crate::parser::{self as js_parser, IdentifierOpts, RelocateVars, RelocateVarsMode}; +use crate::parser::{IdentifierOpts, RelocateVars, RelocateVarsMode}; use bun_ast::ast_result::CommonJSNamedExport; use bun_ast::{self as js_ast, Binding, E, Expr, Flags, G, LocRef, S}; @@ -599,11 +599,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }); } - // Inline import.meta properties for Bake - if p.options.framework.is_some() - || (p.options.bundle - && p.options.output_format == js_parser::options::Format::Cjs) - { + if p.options.inline_import_meta_paths { if name == b"dir" || name == b"dirname" { // Inline import.meta.dir return Some( diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 7aba6f7d8d38..77d6c4336a04 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -9144,7 +9144,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }; if self.has_import_meta { self.import_meta_ref = - self.new_symbol(js_ast::symbol::Kind::Other, b"$Bun_import_meta"); + self.new_symbol(js_ast::symbol::Kind::Other, E::ImportMeta::CJS_WRAPPER_ARG); args[5] = Arg { binding: self.b( B::Identifier { diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 743cc7c322c6..d852aa76e20f 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -96,6 +96,9 @@ pub struct Options<'a> { pub import_meta_main_value: Option, pub lower_import_meta_main_for_node_js: bool, + /// Inline the source file's paths for `import.meta.dir`, `.dirname`, `.file`, `.path`, `.url`. + pub inline_import_meta_paths: bool, + /// When using react fast refresh or server components, the framework is /// able to customize what import sources are used. pub framework: Option<&'a options::Framework>, // TYPE_ONLY: was bun_runtime::bake::Framework @@ -142,6 +145,7 @@ impl<'a> Default for Options<'a> { transform_only: false, import_meta_main_value: None, lower_import_meta_main_for_node_js: false, + inline_import_meta_paths: false, framework: None, repl_mode: false, lower_toml_datetimes: false, @@ -228,6 +232,7 @@ impl<'a> Options<'a> { transform_only: self.transform_only, import_meta_main_value: self.import_meta_main_value, lower_import_meta_main_for_node_js: self.lower_import_meta_main_for_node_js, + inline_import_meta_paths: self.inline_import_meta_paths, framework: self.framework, repl_mode: self.repl_mode, lower_toml_datetimes: self.lower_toml_datetimes, @@ -301,6 +306,7 @@ impl<'a> Options<'a> { transform_only: false, import_meta_main_value: None, lower_import_meta_main_for_node_js: false, + inline_import_meta_paths: false, framework: None, repl_mode: false, lower_toml_datetimes: loader == options::Loader::Toml, diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 5ec6076920e5..b47905909a91 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -1326,6 +1326,8 @@ pub struct Options<'a> { pub module_preload_ref: Ref, pub require_ref: Option, pub import_meta_ref: Ref, + /// Print `import.meta` as `E::ImportMeta::CJS_WRAPPER_ARG`, a parameter of the `@bun-cjs` wrapper. + pub inside_bun_cjs_wrapper: bool, pub hmr_ref: Ref, pub indent: Indentation, // allocator dropped — global mimalloc (this is an AST crate but Options.allocator is the global default) @@ -1408,6 +1410,7 @@ impl<'a> Default for Options<'a> { module_preload_ref: Ref::NONE, require_ref: None, import_meta_ref: Ref::NONE, + inside_bun_cjs_wrapper: false, hmr_ref: Ref::NONE, indent: Indentation::default(), source_map_handler: None, @@ -1553,9 +1556,12 @@ pub enum PrintResult { Err(crate::Error), } +#[derive(Default)] pub struct PrintResultSuccess { pub code: Box<[u8]>, pub source_map: Option, + /// `code` refers to `E::ImportMeta::CJS_WRAPPER_ARG` (see `Options::inside_bun_cjs_wrapper`). + pub uses_import_meta_arg: bool, } // do not make this a packed struct @@ -1677,6 +1683,7 @@ pub(crate) mod __gated_printer { pub(crate) stack_overflowed: bool, pub(crate) was_lazy_export: bool, + pub(crate) uses_import_meta_arg: bool, // Always carried; gated at call sites with MAY_HAVE_MODULE_INFO. pub(crate) module_info: Option<&'a mut analyze_transpiled_module::ModuleInfo>, @@ -3290,23 +3297,22 @@ pub(crate) mod __gated_printer { debug_assert!(self.options.hmr_ref.is_valid()); self.print_symbol(self.options.hmr_ref); self.print(b".importMeta"); - } else if !self.options.import_meta_ref.is_valid() { - // Most of the time, leave it in there - if let Some(mi) = self.module_info() { - mi.flags.contains_import_meta = true; - } - self.print(b"import.meta"); - } else { - // Note: The bundler will not hit this code path. The bundler will replace - // the ImportMeta AST node with a regular Identifier AST node. - // - // This is currently only used in Bun's runtime for CommonJS modules - // referencing import.meta + } else if self.options.import_meta_ref.is_valid() { + // The runtime's CommonJS wrapper (`WrapMode::BunCommonjs`). // // TODO: This assertion trips when using `import.meta` with `--format=cjs` debug_assert!(self.options.module_type == bundle_opts::Format::Cjs); self.print_symbol(self.options.import_meta_ref); + } else if self.options.inside_bun_cjs_wrapper { + self.print(E::ImportMeta::CJS_WRAPPER_ARG); + self.uses_import_meta_arg = true; + } else { + // Most of the time, leave it in there + if let Some(mi) = self.module_info() { + mi.flags.contains_import_meta = true; + } + self.print(b"import.meta"); } } ExprData::EImportMetaMain(data) => { @@ -7026,6 +7032,7 @@ pub(crate) mod __gated_printer { stack_check: bun_core::StackCheck::init(), stack_overflowed: false, was_lazy_export: false, + uses_import_meta_arg: false, module_info: None, } } @@ -8230,6 +8237,7 @@ pub(crate) fn print_with_writer_and_platform< PrintResult::Result(PrintResultSuccess { code: buffer.take_slice().into(), source_map, + uses_import_meta_arg: printer.uses_import_meta_arg, }) } diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 1d283ae545f7..8de162cacfd6 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -1,5 +1,6 @@ import { Database } from "bun:sqlite"; import { describe, expect } from "bun:test"; +import { readdirSync } from "node:fs"; import { itBundled } from "./expectBundled"; const nestedFunctions = /* js */ ` @@ -176,6 +177,163 @@ error: Hello World`, }, run: { stdout: "" }, }); + + // cjs output for bun (which --bytecode implies) gets the output file's import.meta + // as the sixth argument of the @bun-cjs wrapper, like esm output gets it natively. + // It used to inline the source file's paths from the build machine instead. + const importMetaFiles = { + "/entry.ts": /* js */ ` + import { basename, dirname } from "node:path"; + import { pathOfDep } from "./lib/dep.cjs"; + var $Bun_import_meta = "user variable"; + function shadowed() { + let $Bun_import_meta = "shadowed"; + return import.meta.file; + } + console.log( + import.meta.path === Bun.main, + import.meta.dir === dirname(Bun.main), + import.meta.file === basename(Bun.main), + import.meta.url === Bun.pathToFileURL(Bun.main).href, + import.meta.filename === Bun.main, + import.meta.dirname === dirname(Bun.main), + import.meta.main, + typeof import.meta, + import.meta.env.IMPORT_META_PROBE, + typeof import.meta.resolve, + shadowed() === basename(Bun.main), + $Bun_import_meta, + pathOfDep() === Bun.main, + ); + `, + "/lib/dep.cjs": /* js */ ` + exports.pathOfDep = () => import.meta.path; + `, + }; + const importMetaStdout = "true true true true true true true object from-env function true user variable true"; + const importMetaEnv = { IMPORT_META_PROBE: "from-env" }; + const expectBytecodeCacheHit = { + env: { ...importMetaEnv, BUN_JSC_verboseDiskCache: "1" }, + validate({ stderr }: { stderr: string }) { + expect(stderr).toContain("[Disk Cache] Cache hit for sourceCode"); + }, + }; + const bunCjsWrapper = (pragma: string, arg: string) => + `// @bun ${pragma}@bun-cjs\n(function(exports, require, module, __filename, __dirname${arg}) {`; + for (const variant of ["", "+minify", "+bytecode"] as const) { + const bytecode = variant === "+bytecode"; + const minify = variant === "+minify"; + itBundled(`bun/ImportMetaFormatCjs${variant}`, { + target: "bun", + format: "cjs", + minifySyntax: minify, + minifyWhitespace: minify, + minifyIdentifiers: minify, + bytecode, + // --bytecode writes a second file, which the CLI only allows with --outdir. + ...(bytecode ? { outdir: "/out" } : {}), + files: importMetaFiles, + onAfterBundle(api) { + const out = api.readFile(bytecode ? "/out/entry.js" : "/out.js"); + expect(out).toStartWith(bunCjsWrapper(bytecode ? "@bytecode " : "", ", $Bun_import_meta")); + expect(out).not.toContain("import.meta"); + // The build directory must not end up in the output. + expect(out).not.toContain(api.root); + }, + run: { stdout: importMetaStdout, env: importMetaEnv, ...(bytecode ? expectBytecodeCacheHit : {}) }, + }); + } + itBundled("bun/ImportMetaFormatCjsUnused", { + target: "bun", + format: "cjs", + files: { + "/entry.ts": /* js */ ` + import { value } from "./dep.cjs"; + console.log(value); + `, + "/dep.cjs": /* js */ ` + exports.value = "no import.meta here"; + `, + }, + onAfterBundle(api) { + // The runtime helpers linked into this chunk use import.meta in their + // source, but cjs output never keeps that part, so the wrapper does not + // take the argument. + expect(api.readFile("/out.js")).toStartWith(bunCjsWrapper("", "")); + }, + run: { stdout: "no import.meta here" }, + }); + // The sqlite loader builds its module out of `import.meta.require(...)` itself. + itBundled("bun/ImportMetaFormatCjsEmbeddedSqlite", { + target: "bun", + format: "cjs", + outfile: "", + outdir: "/out", + files: { + "/entry.ts": /* js */ ` + import db from './db.sqlite' with {type: "sqlite", embed: "true"}; + console.log(db.query("select message from messages LIMIT 1").get().message); + `, + "/db.sqlite": (() => { + const db = new Database(":memory:"); + db.exec("create table messages (message text)"); + db.exec("insert into messages values ('Hello from cjs!')"); + return db.serialize(); + })(), + }, + run: { stdout: "Hello from cjs!" }, + }); + // The browser chunk of an HTML import is loaded as a module script, so it + // keeps `import.meta`. Only the server chunk is wrapped. + itBundled("bun/ImportMetaFormatCjsHtmlImport", { + target: "bun", + format: "cjs", + outdir: "/out", + entryPoints: ["/server.ts"], + files: { + "/server.ts": /* js */ ` + import html from "./index.html"; + console.log(typeof html, import.meta.path === Bun.main); + `, + "/index.html": ``, + "/client.ts": /* js */ ` + console.log(typeof import.meta.env, import.meta); + `, + }, + onAfterBundle(api) { + expect(api.readFile("/out/server.js")).toStartWith(bunCjsWrapper("", ", $Bun_import_meta")); + const browserChunk = readdirSync(api.outdir).find(name => name !== "server.js" && name.endsWith(".js"))!; + const browser = api.readFile("/out/" + browserChunk); + expect(browser).toContain("typeof import.meta.env, import.meta"); + expect(browser).not.toContain("$Bun_import_meta"); + }, + run: { stdout: "object true" }, + }); + // `bun build --compile --bytecode` is the documented production command. + // https://github.com/oven-sh/bun/issues/21097 + itBundled("bun/ImportMetaCompileBytecode", { + compile: true, + bytecode: true, + files: { + "/entry.ts": /* js */ ` + import { basename, dirname } from "node:path"; + const slashes = (s) => s.replaceAll("\\\\", "/"); + console.log( + slashes(import.meta.path) === slashes(Bun.main), + slashes(import.meta.dir) === slashes(dirname(Bun.main)), + import.meta.file === basename(Bun.main), + import.meta.url === Bun.pathToFileURL(Bun.main).href, + import.meta.env.IMPORT_META_PROBE, + slashes(import.meta.dir), + ); + `, + }, + run: { + stdout: /^true true true true from-env (\/\$bunfs|[A-Z]:\/~BUN)\/root$/, + ...expectBytecodeCacheHit, + }, + }); + if (Bun.version.startsWith("1.4") || Bun.version.startsWith("1.3") || Bun.version.startsWith("1.2")) { for (const backend of ["api", "cli"] as const) { itBundled("bun/ExportsConditionsDevelopment" + backend.toUpperCase(), { From 2ab83036d462c2ada7dfee5c54c6f615c57e83f5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:07:14 +0000 Subject: [PATCH 2/5] ci: retrigger From 72e20f460a281f5c206c808c7e1873df59adb434 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:22:10 +0000 Subject: [PATCH 3/5] bundler: keep the content hash of cjs chunks that do not use import.meta The content hash of a chunk covers the length of every piece pushed to the output joiner. The @bun-cjs wrapper head was pushed in three pieces, so the hashed names and the sourcemap debugId of every bun-target cjs chunk changed while the output bytes stayed equal. Push the head as one piece, as before, when the chunk does not take the import.meta argument. Also test that a --bytecode executable starts the worker that was compiled in when the worker is named by a URL made from import.meta.url. Before, it ran the file at the source path on disk. --- .../linker_context/postProcessJSChunk.rs | 22 ++++++++++------- test/bundler/bundler_bun.test.ts | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index 414d32acc1a1..5cf82806725f 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -456,17 +456,23 @@ pub(crate) fn post_process_js_chunk( j.push_static(bytes); line_offset.advance(bytes); }; - push(if c.options.generate_bytecode_cache { - b"// @bun @bytecode @bun-cjs\n" - } else { - b"// @bun @bun-cjs\n" - }); - push(b"(function(exports, require, module, __filename, __dirname"); + let bytecode = c.options.generate_bytecode_cache; if chunk_uses_import_meta_arg(chunk) { - push(b", "); + push(if bytecode { + b"// @bun @bytecode @bun-cjs\n(function(exports, require, module, __filename, __dirname, " + } else { + b"// @bun @bun-cjs\n(function(exports, require, module, __filename, __dirname, " + }); push(E::ImportMeta::CJS_WRAPPER_ARG); + push(b") {"); + } else { + // One piece: `generate_isolated_hash` covers each piece's length, so a split moves `[hash]` names. + push(if bytecode { + b"// @bun @bytecode @bun-cjs\n(function(exports, require, module, __filename, __dirname) {" + } else { + b"// @bun @bun-cjs\n(function(exports, require, module, __filename, __dirname) {" + }); } - push(b") {"); } else if is_bun { if c.options.generate_bytecode_cache { j.push_static(b"// @bun @bytecode\n"); diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 8de162cacfd6..6aa597af509e 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -333,6 +333,30 @@ error: Hello World`, ...expectBytecodeCacheHit, }, }); + // The executable must start the worker that was compiled in. With the source + // path in import.meta.url it ran whatever was at that path when it started. + itBundled("bun/ImportMetaCompileBytecodeWorker", { + backend: "cli", + compile: true, + bytecode: true, + files: { + "/entry.ts": /* js */ ` + const worker = new Worker(new URL("./worker.ts", import.meta.url)); + worker.onmessage = e => { + console.log(e.data); + worker.terminate(); + }; + worker.onerror = e => console.log("error: " + e.message); + `, + "/worker.ts": /* js */ `postMessage("compiled in");`, + }, + entryPointsRaw: ["./entry.ts", "./worker.ts"], + outfile: "dist/out", + onAfterBundle(api) { + api.writeFile("/worker.ts", `postMessage("read from disk after the build");`); + }, + run: { stdout: "compiled in", file: "dist/out", setCwd: true }, + }); if (Bun.version.startsWith("1.4") || Bun.version.startsWith("1.3") || Bun.version.startsWith("1.2")) { for (const backend of ["api", "cli"] as const) { From bbb314932eaba3bd0771c5de96a1203e67084b23 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:03:23 +0000 Subject: [PATCH 4/5] bundler: decide import.meta path inlining from the build's target The parser decided from the file's own target. A `#!/usr/bin/env bun` entry in a node build has the bun target, so its import.meta paths were not inlined. When another entry imported it, its code also landed in that entry's chunk, which has no wrapper, and import.meta was printed as is. The browser files of a server build were decided from the browser target, so a cjs server build inlined the build machine's paths into browser chunks. Those chunks are module scripts, where import.meta is real. Inline only in the build's own graph, and only when the build's target is not bun. --- src/bundler/ParseTask.rs | 7 +++- test/bundler/bundler_bun.test.ts | 66 +++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 7397b44e4b8d..d6d4f1173eff 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -2651,9 +2651,12 @@ pub mod parse_worker { opts.lower_import_meta_main_for_node_js = true; } - // For bun, cjs output gets import.meta from the `@bun-cjs` wrapper instead. + // Not for bun (its cjs wrapper has import.meta) or a server build's browser files (module scripts). + let build_target = worker_ctx.transpiler().options.target; opts.inline_import_meta_paths = topts.framework.is_some() - || (output_format == options::Format::Cjs && !target.is_bun()); + || (output_format == options::Format::Cjs + && !build_target.is_bun() + && topts.target == build_target); opts.tree_shaking = if task.source_index.is_runtime() { true diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 6aa597af509e..4dfb80b0145d 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -283,31 +283,59 @@ error: Hello World`, }, run: { stdout: "Hello from cjs!" }, }); - // The browser chunk of an HTML import is loaded as a module script, so it - // keeps `import.meta`. Only the server chunk is wrapped. - itBundled("bun/ImportMetaFormatCjsHtmlImport", { - target: "bun", + // The browser chunk of an HTML import is a module script in any server + // build, so it keeps `import.meta`. Only bun's server chunk is wrapped. + for (const target of ["bun", "node"] as const) { + itBundled(`bun/ImportMetaFormatCjsHtmlImport${target === "bun" ? "" : "+node"}`, { + target, + format: "cjs", + outdir: "/out", + entryPoints: ["/server.ts"], + files: { + "/server.ts": /* js */ ` + import html from "./index.html"; + console.log(typeof html, import.meta.path === Bun.main); + `, + "/index.html": ``, + "/client.ts": /* js */ ` + console.log(typeof import.meta.env, import.meta.url, import.meta); + `, + }, + onAfterBundle(api) { + const server = api.readFile("/out/server.js"); + if (target === "bun") expect(server).toStartWith(bunCjsWrapper("", ", $Bun_import_meta")); + else expect(server).not.toContain("import.meta"); + const browserChunk = readdirSync(api.outdir).find(name => name !== "server.js" && name.endsWith(".js"))!; + const browser = api.readFile("/out/" + browserChunk); + expect(browser).toContain("typeof import.meta.env, import.meta.url, import.meta"); + expect(browser).not.toContain("$Bun_import_meta"); + expect(browser).not.toContain(api.root); + }, + ...(target === "bun" ? { run: { stdout: "object true" } } : {}), + }); + } + // An entry with a bun hashbang is wrapped even in a node build, and it can + // also be bundled into another entry's chunk, which is not. + itBundled("bun/ImportMetaFormatCjsHashbangEntryInNodeBuild", { + target: "node", format: "cjs", outdir: "/out", - entryPoints: ["/server.ts"], + entryPoints: ["/a.ts", "/b.ts"], files: { - "/server.ts": /* js */ ` - import html from "./index.html"; - console.log(typeof html, import.meta.path === Bun.main); - `, - "/index.html": ``, - "/client.ts": /* js */ ` - console.log(typeof import.meta.env, import.meta); - `, + "/a.ts": `#!/usr/bin/env bun\nexport const dir = import.meta.dir;\nconsole.log("a", typeof dir);\n`, + "/b.ts": `import { dir } from "./a.ts";\nconsole.log("b", typeof dir);\n`, }, onAfterBundle(api) { - expect(api.readFile("/out/server.js")).toStartWith(bunCjsWrapper("", ", $Bun_import_meta")); - const browserChunk = readdirSync(api.outdir).find(name => name !== "server.js" && name.endsWith(".js"))!; - const browser = api.readFile("/out/" + browserChunk); - expect(browser).toContain("typeof import.meta.env, import.meta"); - expect(browser).not.toContain("$Bun_import_meta"); + for (const file of ["/out/a.js", "/out/b.js"]) { + const out = api.readFile(file); + expect(out).not.toContain("import.meta"); + expect(out).not.toContain("$Bun_import_meta"); + } }, - run: { stdout: "object true" }, + run: [ + { file: "/out/a.js", stdout: "a string" }, + { file: "/out/b.js", stdout: "a string\nb string" }, + ], }); // `bun build --compile --bytecode` is the documented production command. // https://github.com/oven-sh/bun/issues/21097 From 042622d5dc1c8e2ec47a54feaaa88f32d1641f5d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:32:02 +0000 Subject: [PATCH 5/5] test: cover import.meta in a --compile --format=cjs executable The compiled cases only used --bytecode. Without it the same chunk is embedded as source and takes the other load path for a cjs entry. --- test/bundler/bundler_bun.test.ts | 47 +++++++++++++++++--------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 4dfb80b0145d..3f512703ac67 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -338,29 +338,32 @@ error: Hello World`, ], }); // `bun build --compile --bytecode` is the documented production command. + // `--compile --format=cjs` embeds the same chunk as source. // https://github.com/oven-sh/bun/issues/21097 - itBundled("bun/ImportMetaCompileBytecode", { - compile: true, - bytecode: true, - files: { - "/entry.ts": /* js */ ` - import { basename, dirname } from "node:path"; - const slashes = (s) => s.replaceAll("\\\\", "/"); - console.log( - slashes(import.meta.path) === slashes(Bun.main), - slashes(import.meta.dir) === slashes(dirname(Bun.main)), - import.meta.file === basename(Bun.main), - import.meta.url === Bun.pathToFileURL(Bun.main).href, - import.meta.env.IMPORT_META_PROBE, - slashes(import.meta.dir), - ); - `, - }, - run: { - stdout: /^true true true true from-env (\/\$bunfs|[A-Z]:\/~BUN)\/root$/, - ...expectBytecodeCacheHit, - }, - }); + for (const bytecode of [true, false]) { + itBundled(`bun/ImportMetaCompile${bytecode ? "Bytecode" : "FormatCjs"}`, { + compile: true, + ...(bytecode ? { bytecode } : { format: "cjs" as const }), + files: { + "/entry.ts": /* js */ ` + import { basename, dirname } from "node:path"; + const slashes = (s) => s.replaceAll("\\\\", "/"); + console.log( + slashes(import.meta.path) === slashes(Bun.main), + slashes(import.meta.dir) === slashes(dirname(Bun.main)), + import.meta.file === basename(Bun.main), + import.meta.url === Bun.pathToFileURL(Bun.main).href, + import.meta.env.IMPORT_META_PROBE, + slashes(import.meta.dir), + ); + `, + }, + run: { + stdout: /^true true true true from-env (\/\$bunfs|[A-Z]:\/~BUN)\/root$/, + ...(bytecode ? expectBytecodeCacheHit : { env: importMetaEnv }), + }, + }); + } // The executable must start the worker that was compiled in. With the source // path in import.meta.url it ran whatever was at that path when it started. itBundled("bun/ImportMetaCompileBytecodeWorker", {