Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 50 additions & 23 deletions src/bundler/linker_context/convertStmtsForChunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand All @@ -68,8 +67,12 @@ pub(crate) fn convert_stmts_for_chunk(
// one must have the "__esModule" marker. This is done because an ES module
// importing itself should not see the "__esModule" marker but a CommonJS module
// importing us should see the "__esModule" marker.
//
// This is about the file being converted, not the chunk it lands in: a
// non-entry file bundled into the entry chunk must not leak its re-exports
// onto the entry's "module.exports".
Comment thread
robobun marked this conversation as resolved.
Outdated
let mut module_exports_for_export: Option<Expr> = 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 {
Expand Down Expand Up @@ -169,16 +172,41 @@ 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,
);
let is_bun_builtin = record.tag == ImportRecordTag::Bun;
let re_exported_module: Expr = if is_bun_builtin {
// The printer lowers an import of "bun" to a plain
// `var ns = globalThis.Bun`, which unlike an import
// statement is not hoisted above the "__reExport()"
// call emitted below, so reference the module directly
// (printed as `globalThis.Bun`) instead of going
// through a namespace binding.
Comment thread
robobun marked this conversation as resolved.
Outdated
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");
Expand All @@ -191,13 +219,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)"
Expand Down Expand Up @@ -233,6 +255,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);
Expand Down
2 changes: 0 additions & 2 deletions src/bundler/linker_context/generateCodeForFileInChunkJS.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
122 changes: 75 additions & 47 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -1838,27 +1828,30 @@ 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 module's default export is the module object itself, so
// `import { default as bun } from "bun"` (and the `import` that the
// bundler turns `export { default } from "bun"` into) binds the whole
// module rather than reading a "default" property off of it.
Comment thread
robobun marked this conversation as resolved.
Outdated
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 {"));

Expand All @@ -1868,7 +1861,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();
Expand All @@ -1877,6 +1874,7 @@ pub(crate) mod __gated_printer {
self.print_indent();
}
}
i += 1;
self.print_clause_item_as(item, ClauseItemAs::Var);
}

Expand All @@ -1888,26 +1886,43 @@ 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);
}
/// Prints the module object that the bindings of `import` are read from:
/// the binding declared by an earlier `var` of the same statement when
/// there is one, otherwise the module expression itself.
Comment thread
robobun marked this conversation as resolved.
Outdated
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),
}
}

Expand Down Expand Up @@ -2494,7 +2509,20 @@ pub(crate) mod __gated_printer {
return;
} else if record.kind == ImportKind::Require || record.kind == ImportKind::Stmt
{
// The linker asks for __toESM() when the import needs ESM
// namespace semantics (a default import or `import *`); the
// bare Bun object has no "default" property.
Comment thread
robobun marked this conversation as resolved.
Outdated
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")");
}
Comment thread
claude[bot] marked this conversation as resolved.
if wrap {
self.print(b")");
}
Expand Down
64 changes: 64 additions & 0 deletions test/bundler/bundler_bun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,70 @@ 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" },
});
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: "",
Expand Down
Loading