diff --git a/src/bundler/linker_context/convertStmtsForChunk.rs b/src/bundler/linker_context/convertStmtsForChunk.rs index 5035de8fbae6..bf0d075ce81b 100644 --- a/src/bundler/linker_context/convertStmtsForChunk.rs +++ b/src/bundler/linker_context/convertStmtsForChunk.rs @@ -2,15 +2,14 @@ use crate::BundledAst as JSAst; use crate::mal_prelude::*; use bun_alloc::Arena as Bump; use bun_ast::ImportRecordFlags; +use bun_ast::ImportRecordTag; use bun_ast::Loc; use bun_ast::{self as js_ast, Binding, Expr, ExprNodeList, Stmt}; use bun_ast::{B, E, G, S}; use bun_collections::VecExt; use bun_core::FeatureFlags; -use crate::EntryPoint; use crate::WrapKind; -use crate::chunk::Chunk; use crate::linker_context_mod::{LinkerContext, LinkerOptionsMode, StmtList, StmtListWhich}; use crate::options::Format; @@ -49,15 +48,15 @@ pub(crate) fn convert_stmts_for_chunk( source_index: u32, stmts: &mut StmtList, part_stmts: &[bun_ast::Stmt], - chunk: &mut Chunk, bump: &Bump, wrap: WrapKind, ast: &JSAst<'_>, ) -> Result<(), crate::Error> { let _ = bump; let should_extract_esm_stmts_for_wrap = wrap != WrapKind::None; - let should_strip_exports = c.options.mode != LinkerOptionsMode::Passthrough - || c.graph.files.items_entry_point_kind()[source_index as usize] != EntryPoint::Kind::None; + let is_entry_point = + c.graph.files.items_entry_point_kind()[source_index as usize].is_entry_point(); + let should_strip_exports = c.options.mode != LinkerOptionsMode::Passthrough || is_entry_point; let output_format = c.options.output_format; @@ -69,7 +68,7 @@ pub(crate) fn convert_stmts_for_chunk( // importing itself should not see the "__esModule" marker but a CommonJS module // importing us should see the "__esModule" marker. let mut module_exports_for_export: Option = None; - if output_format == Format::Cjs && chunk.is_entry_point() { + if output_format == Format::Cjs && is_entry_point { module_exports_for_export = Some(Expr::allocate( bump, E::Dot { @@ -169,16 +168,36 @@ pub(crate) fn convert_stmts_for_chunk( .flags .contains(ImportRecordFlags::CALLS_RUNTIME_RE_EXPORT_FN) { - // Turn this statement into "import * as ns from 'path'" - stmt = Stmt::alloc( - S::Import { - namespace_ref: s.namespace_ref, - import_record_index: s.import_record_index, - star_name_loc: stmt.loc, - ..Default::default() - }, - stmt.loc, - ); + // A "bun" import prints as an unhoisted var, too late for __reExport() + let is_bun_builtin = record.tag == ImportRecordTag::Bun; + let re_exported_module: Expr = if is_bun_builtin { + Expr::init( + E::RequireString { + import_record_index: s.import_record_index, + ..Default::default() + }, + stmt.loc, + ) + } else { + // Turn this statement into "import * as ns from 'path'" + stmt = Stmt::alloc( + S::Import { + namespace_ref: s.namespace_ref, + import_record_index: s.import_record_index, + star_name_loc: stmt.loc, + ..Default::default() + }, + stmt.loc, + ); + + Expr::init( + E::Identifier { + ref_: s.namespace_ref, + ..Default::default() + }, + stmt.loc, + ) + }; // Prefix this module with "__reExport(exports, ns, module.exports)" let export_star_ref = c.runtime_function(b"__reExport"); @@ -191,13 +210,7 @@ pub(crate) fn convert_stmts_for_chunk( }, stmt.loc, )); - args.push(Expr::init( - E::Identifier { - ref_: s.namespace_ref, - ..Default::default() - }, - stmt.loc, - )); + args.push(re_exported_module); if let Some(mod_) = module_exports_for_export { // Per the "__reExport(exports, ns, module.exports)" @@ -233,6 +246,11 @@ pub(crate) fn convert_stmts_for_chunk( stmt.loc, ))?; + if is_bun_builtin { + // There is no import statement left to keep + continue 'stmt_loop; + } + // Make sure these don't end up in the wrapper closure if should_extract_esm_stmts_for_wrap { stmts.append(StmtListWhich::OutsideWrapperPrefix, stmt); diff --git a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs index e41a1cb53ba8..86bcab4cf9e4 100644 --- a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs +++ b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs @@ -293,7 +293,6 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( source_index as u32, stmts, ns_part_stmts, - chunk, temp_arena, flags.wrap, &ast, @@ -494,7 +493,6 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( source_index as u32, stmts, part_stmts, - chunk, temp_arena, flags.wrap, &ast, diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index c712a54b64ef..500559281f91 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -1819,17 +1819,7 @@ pub(crate) mod __gated_printer { self.print_space(); self.print(b"="); self.print_space_before_identifier(); - match statement { - None => self.print_require_or_import_expr( - import.import_record_index, - false, - &[], - Expr::EMPTY, - Level::Lowest, - ExprFlag::none(), - ), - Some(s) => self.print(s), - } + self.print_internal_bun_module(import.import_record_index, statement); self.print_semicolon_after_statement(); self.print_indent(); } @@ -1838,27 +1828,27 @@ pub(crate) mod __gated_printer { self.print_semicolon_if_needed(); self.print(b"var "); self.print_symbol(default.ref_); - match statement { - None => { - self.print_equals(); - self.print_require_or_import_expr( - import.import_record_index, - false, - &[], - Expr::EMPTY, - Level::Lowest, - ExprFlag::none(), - ); - } - Some(s) => { - self.print_equals(); - self.print(s); - } + self.print_equals(); + self.print_internal_bun_module(import.import_record_index, statement); + self.print_semicolon_after_statement(); + } + + // The default export is the module object itself, not a "default" property on it. + let mut named_item_count: usize = 0; + for item in slice_of(import.items).iter() { + if item.alias.slice() != b"default" { + named_item_count += 1; + continue; } + self.print_semicolon_if_needed(); + self.print(b"var "); + self.print_symbol(item.name.ref_); + self.print_equals(); + self.print_internal_bun_import_value(import, statement); self.print_semicolon_after_statement(); } - if slice_of(import.items).len() > 0 { + if named_item_count > 0 { self.print_semicolon_if_needed(); self.print_whitespacer(ws!(b"var {")); @@ -1868,7 +1858,11 @@ pub(crate) mod __gated_printer { self.print_indent(); } - for (i, item) in slice_of(import.items).iter().enumerate() { + let mut i: usize = 0; + for item in slice_of(import.items).iter() { + if item.alias.slice() == b"default" { + continue; + } if i > 0 { self.print(b","); self.print_space(); @@ -1877,6 +1871,7 @@ pub(crate) mod __gated_printer { self.print_indent(); } } + i += 1; self.print_clause_item_as(item, ClauseItemAs::Var); } @@ -1888,26 +1883,40 @@ pub(crate) mod __gated_printer { } self.print_whitespacer(ws!(b"} = ")); + self.print_internal_bun_import_value(import, statement); + self.print_semicolon_after_statement(); + } + } - if import.star_name_loc.is_empty() && import.default_name.is_none() { - match statement { - None => self.print_require_or_import_expr( - import.import_record_index, - false, - &[], - Expr::EMPTY, - Level::Lowest, - ExprFlag::none(), - ), - Some(s) => self.print(s), - } - } else if let Some(name) = &import.default_name { - self.print_symbol(name.ref_); - } else { - self.print_symbol(import.namespace_ref); - } + fn print_internal_bun_import_value( + &mut self, + import: &S::Import, + statement: Option<&'static [u8]>, + ) { + if let Some(default) = &import.default_name { + self.print_symbol(default.ref_); + } else if !import.star_name_loc.is_empty() { + self.print_symbol(import.namespace_ref); + } else { + self.print_internal_bun_module(import.import_record_index, statement); + } + } - self.print_semicolon_after_statement(); + fn print_internal_bun_module( + &mut self, + import_record_index: u32, + statement: Option<&'static [u8]>, + ) { + match statement { + None => self.print_require_or_import_expr( + import_record_index, + false, + &[], + Expr::EMPTY, + Level::Lowest, + ExprFlag::none(), + ), + Some(s) => self.print(s), } } @@ -2494,7 +2503,18 @@ pub(crate) mod __gated_printer { return; } else if record.kind == ImportKind::Require || record.kind == ImportKind::Stmt { + // The Bun object has no "default" property; __toESM() adds it. + let wrap_with_to_esm = + record.flags.contains(ImportRecordFlags::WRAP_WITH_TO_ESM); + if wrap_with_to_esm { + self.print_space_before_identifier(); + self.print_symbol(self.options.to_esm_ref); + self.print(b"("); + } self.print(b"globalThis.Bun"); + if wrap_with_to_esm { + self.print(b")"); + } if wrap { self.print(b")"); } diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 3373c1402e99..3b4dc19491f5 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -51,7 +51,8 @@ bun_core::declare_scope!(cache, visible); /// Version 25: Every ModuleInfo record carries a trailing FetchParameters slot /// so ImportEntry/ExportEntry/StarExportEntry moduleRequestType matches JSC's /// after WebKit 90b2ecf79ae3 keyed m_loadedModules on (specifier, type). -const EXPECTED_VERSION: u32 = 25; +/// Version 26: `import { default as x } from "bun"` now binds x to the Bun object itself. +const EXPECTED_VERSION: u32 = 26; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 674da11059bb..0b105f79e6ef 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -38,6 +38,71 @@ describe("bundler", () => { }, run: { stdout: "RedisClient\nRedisClient\nRedisClient\n" }, }); + // A non-entry `export * from "bun"` is evaluated at runtime with __reExport(), + // which has to read the Bun object before the re-exporting module's body runs. + itBundled("bun/ReExportStarFromBunInNonEntryFile", { + target: "bun", + files: { + "/entry.ts": /* js */ ` + import { Glob, version } from "./re-export"; + console.log(typeof Glob, version === Bun.version); + `, + "/re-export.ts": `export * from "bun";`, + }, + run: { stdout: "function true" }, + }); + itBundled("bun/ReExportStarFromBunInNonEntryFileCJS", { + target: "bun", + format: "cjs", + files: { + "/entry.ts": /* js */ ` + import { Glob, version } from "./re-export"; + console.log(typeof Glob, version === Bun.version); + `, + "/re-export.ts": `export * from "bun";`, + }, + runtimeFiles: { + // The entry point exports nothing, so nothing may be copied onto its + // module.exports. (When the whole Bun object was, running the bundle + // directly made Bun treat the copied "fetch" as a server entry point.) + "/test.js": /* js */ ` + const entry = require("./out.js"); + console.log(typeof entry.fetch, Object.keys(entry).length); + `, + }, + run: { + file: "/test.js", + stdout: "function true\nundefined 0", + }, + }); + // The default export of "bun" is the Bun object itself. + itBundled("bun/ReExportDefaultFromBun", { + target: "bun", + files: { + "/entry.ts": /* js */ ` + import ReExported, { G } from "./re-export"; + import { default as Aliased } from "bun"; + console.log(ReExported === Bun, Aliased === Bun, typeof G); + `, + "/re-export.ts": `export { default, Glob as G } from "bun";`, + }, + run: { stdout: "true true function" }, + }); + // https://github.com/oven-sh/bun/issues/20670 (--bytecode defaults to this format) + itBundled("bun/ReExportDefaultFromBunCJS", { + target: "bun", + format: "cjs", + files: { + "/entry.ts": /* js */ ` + import ReExported from "./re-export"; + import Direct, { Glob } from "bun"; + import * as ns from "bun"; + console.log(ReExported === Bun, Direct === Bun, typeof Glob, ns.default === Bun, ns.Glob === Glob); + `, + "/re-export.ts": `export { default } from "bun";`, + }, + run: { stdout: "true true function true true" }, + }); itBundled("bun/embedded-sqlite-file", { target: "bun", outfile: "", diff --git a/test/bundler/bundler_cjs.test.ts b/test/bundler/bundler_cjs.test.ts index aee7fb02d9fd..0c4f4ceeb753 100644 --- a/test/bundler/bundler_cjs.test.ts +++ b/test/bundler/bundler_cjs.test.ts @@ -597,4 +597,31 @@ describe("bundler", () => { stdout: "loaded ok", }, }); + + // Test 29: export * from an external package in a file that is not the entry + // point. Only the entry point's own re-exports are mirrored onto module.exports; + // a re-exporting dependency bundled into the entry chunk must not be. + itBundled("cjs/__reExport_external_in_non_entry_file", { + files: { + "/entry.js": /* js */ ` + import { foo } from "./re-export.js"; + console.log(foo); + `, + "/re-export.js": `export * from "ext";`, + }, + runtimeFiles: { + "/node_modules/ext/index.js": /* js */ `module.exports = { foo: "foo", bar: "bar" };`, + "/test.js": /* js */ ` + const entry = require("./out.js"); + console.log(JSON.stringify(Object.keys(entry))); + `, + }, + external: ["ext"], + target: "node", + format: "cjs", + run: { + file: "/test.js", + stdout: "foo\n[]", + }, + }); }); diff --git a/test/bundler/bundler_minify.test.ts b/test/bundler/bundler_minify.test.ts index f6220592d6d9..421a3978b7f4 100644 --- a/test/bundler/bundler_minify.test.ts +++ b/test/bundler/bundler_minify.test.ts @@ -1251,6 +1251,24 @@ describe("bundler", () => { }, }); + itBundled("minify/BunImportDefaultAliasAndNamed", { + files: { + "/entry.js": /* js */ ` + import { default as bun, embeddedFiles } from "bun" + import { default as bunAgain } from "bun" + console.log(typeof embeddedFiles) + console.log(bun === Bun, bunAgain === Bun) + `, + }, + minifySyntax: true, + minifyWhitespace: true, + minifyIdentifiers: true, + target: "bun", + run: { + stdout: "object\ntrue true", + }, + }); + // https://github.com/oven-sh/bun/issues/31722 // An arrow whose body is a single `return ` collapses to a shorthand // expression body when minifying: `(a) => { return a; }` becomes `(a) => a`. diff --git a/test/bundler/esbuild/importstar.test.ts b/test/bundler/esbuild/importstar.test.ts index 89261b19282e..686263aeab47 100644 --- a/test/bundler/esbuild/importstar.test.ts +++ b/test/bundler/esbuild/importstar.test.ts @@ -1398,7 +1398,9 @@ describe("bundler", () => { }, run: { file: "/test.js", - stdout: '{"inner":{"b":456},"a":123,"b":456}', + // inner.js re-exports "b", but entry.js only exports the "inner" namespace, + // so "b" must not show up on the entry point's module.exports. + stdout: '{"inner":{"b":456},"a":123}', }, }); itBundled("importstar/ReExportStarEntryPointAndInnerFile", { diff --git a/test/js/bun/resolve/import-meta.test.js b/test/js/bun/resolve/import-meta.test.js index b2a3cc54b06b..c5d73e96196e 100644 --- a/test/js/bun/resolve/import-meta.test.js +++ b/test/js/bun/resolve/import-meta.test.js @@ -1,4 +1,4 @@ -import { spawnSync } from "bun"; +import { default as BunViaDefaultAlias, spawnSync } from "bun"; import { isModuleResolveFilenameSlowPathEnabled } from "bun:internal-for-testing"; import { expect, it, mock } from "bun:test"; import { bunEnv, bunExe, ospath } from "harness"; @@ -228,6 +228,11 @@ it('import("bun") works', async () => { expect(await import("bun")).toBe(Bun); }); +it('import { default as x } from "bun" is the Bun object', () => { + expect(BunViaDefaultAlias).toBe(Bun); + expect(spawnSync).toBe(Bun.spawnSync); +}); + it("require.resolve with empty options object", () => { expect(require.resolve(import.meta.path + String(""), {})).toBe(import.meta.path); });