diff --git a/src/ast/e.rs b/src/ast/e.rs index 520c12f6f077..ebbad5998e32 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 bbf0c81e36df..afa39c028b58 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -2366,6 +2366,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, @@ -2377,6 +2383,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>, @@ -2456,6 +2463,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 7bb770e37fea..d6d4f1173eff 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -2651,6 +2651,13 @@ pub mod parse_worker { opts.lower_import_meta_main_for_node_js = true; } + // 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 + && !build_target.is_bun() + && topts.target == build_target); + 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 faea2b612aeb..e8d65615b15c 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -8052,10 +8052,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..5cf82806725f 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,33 @@ 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); + }; + let bytecode = c.options.generate_bytecode_cache; + if chunk_uses_import_meta_arg(chunk) { + 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) {" + }); + } + } 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 +771,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 +1282,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 4e21dd4c3083..aeb1fec47fb3 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 842cecff4791..92962775d7d8 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1480,6 +1480,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 1e01b7280c76..60bcdc446c5a 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}; @@ -612,11 +612,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 7da4223ff592..1ef54f16f087 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -9248,7 +9248,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 e1c242d36853..bbde99af97a3 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -140,6 +140,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 @@ -187,6 +190,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, @@ -274,6 +278,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, @@ -348,6 +353,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 4040862321d0..4291836c35df 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -1324,6 +1324,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) @@ -1409,6 +1411,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, @@ -1554,9 +1557,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 @@ -1678,6 +1684,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>, @@ -3285,23 +3292,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) => { @@ -7001,6 +7007,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, } } @@ -8106,6 +8113,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..3f512703ac67 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,218 @@ 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 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: ["/a.ts", "/b.ts"], + files: { + "/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) { + 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: [ + { 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. + // `--compile --format=cjs` embeds the same chunk as source. + // https://github.com/oven-sh/bun/issues/21097 + 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", { + 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) { itBundled("bun/ExportsConditionsDevelopment" + backend.toUpperCase(), {