use bun_alloc::Arena;
use bun_bundler::bundle_v2::BundleV2;
use bun_ast::Loader;
use bun_bundler::options::{Format, LoaderExt, OutputFile};
use bun_bundler::output_file::{SavedFile, Value as OutputValue};
use bun_bundler::transpiler::Transpiler;
use bun_bundler::BundleThread::BuildResult;
use bun_event_loop::AnyEventLoop;
use bun_options_types::schema::api;
use bun_runtime::bun_build::{
NativeBuildConfig, NativeBuildLog, NativeBuildResult, NativeOutputFile,
};
pub fn install() {
bun_runtime::bun_build::install_native_build_impl(run_bundle);
}
#[doc(hidden)]
pub fn run_bundle_for_test(config: &NativeBuildConfig) -> NativeBuildResult {
run_bundle(config)
}
fn parse_target(s: &str) -> api::Target {
match s {
"bun" => api::Target::Bun,
"node" => api::Target::Node,
_ => api::Target::Browser,
}
}
fn parse_format(s: &str) -> Format {
match s {
"cjs" => Format::Cjs,
"iife" => Format::Iife,
_ => Format::Esm,
}
}
fn parse_sourcemap(s: &str) -> api::SourceMapMode {
match s {
"linked" => api::SourceMapMode::Linked,
"inline" => api::SourceMapMode::Inline,
"external" => api::SourceMapMode::External,
_ => api::SourceMapMode::None,
}
}
fn run_bundle(config: &NativeBuildConfig) -> NativeBuildResult {
let cwd = ::std::env::current_dir()
.ok()
.and_then(|p| p.into_os_string().into_string().ok())
.unwrap_or_else(|| ".".to_string());
let arena = Arena::new();
let mut log = bun_ast::Log::init();
let mut opts = api::TransformOptions {
entry_points: config
.entrypoints
.iter()
.map(|e| Box::from(e.as_bytes()))
.collect(),
target: Some(parse_target(&config.target)),
external: config
.external
.iter()
.map(|e| Box::from(e.as_bytes()))
.collect(),
source_map: Some(parse_sourcemap(&config.sourcemap)),
define: (!config.define.is_empty()).then(|| api::StringMap {
keys: config.define.iter().map(|(k, _)| Box::from(k.as_bytes())).collect(),
values: config.define.iter().map(|(_, v)| Box::from(v.as_bytes())).collect(),
}),
write: Some(false),
output_dir: None,
absolute_working_dir: Some(Box::from(cwd.as_bytes())),
..Default::default()
};
if let Some(root) = &config.root {
opts.absolute_working_dir = Some(Box::from(root.as_bytes()));
}
let log_ptr: *mut bun_ast::Log = &mut log;
let mut transpiler = match Transpiler::init(&arena, log_ptr, opts, None) {
Ok(t) => t,
Err(e) => {
return NativeBuildResult {
success: false,
outputs: Vec::new(),
logs: vec![NativeBuildLog {
level: "error".into(),
message: format!("Bun.build: failed to configure the bundler: {}", e),
}],
};
}
};
apply_api_overrides(&mut transpiler, config);
if let Err(e) = transpiler.configure_defines() {
return NativeBuildResult {
success: false,
outputs: Vec::new(),
logs: vec![NativeBuildLog {
level: "error".into(),
message: format!("Bun.build: failed to configure defines: {}", e),
}],
};
}
let mut any_loop: AnyEventLoop<'static> = AnyEventLoop::init();
let event_loop = core::ptr::NonNull::from(&mut any_loop);
let mut reachable_files_count = 0usize;
let mut minify_duration = 0u64;
let mut source_code_size = 0u64;
let build: ::std::result::Result<BuildResult, bun_core::Error> =
BundleV2::generate_from_cli(
unsafe { core::mem::transmute(&mut transpiler) },
unsafe { core::mem::transmute(&arena) },
Some(event_loop),
false,
&mut reachable_files_count,
&mut minify_duration,
&mut source_code_size,
None,
);
let mut result = match build {
Ok(br) => br,
Err(e) => {
core::mem::forget(any_loop);
let mut logs = collect_logs(&log);
logs.push(NativeBuildLog {
level: "error".into(),
message: format!("Bun.build: build failed: {}", e),
});
return NativeBuildResult { success: false, outputs: Vec::new(), logs };
}
};
let outputs = map_outputs(&mut result.output_files);
let logs = collect_logs(&log);
let success = !log.has_errors();
let mut native = NativeBuildResult { success, outputs, logs };
if let Some(outdir) = &config.outdir {
write_outputs_to_disk(outdir, &native.outputs, &mut native.logs);
}
native
}
fn apply_api_overrides(transpiler: &mut Transpiler<'_>, config: &NativeBuildConfig) {
let options = &mut transpiler.options;
let output_dir: Box<[u8]> = match &config.outdir {
Some(dir) => Box::from(dir.as_bytes()),
None => Box::default(),
};
options.output_dir = output_dir.clone();
transpiler.resolver.opts.output_dir = output_dir;
options.minify_whitespace = config.minify.whitespace;
options.minify_syntax = config.minify.syntax;
options.minify_identifiers = config.minify.identifiers;
options.output_format = parse_format(&config.format);
options.code_splitting = config.splitting;
const DEFAULT_ENTRY_NAMING: &[u8] = b"[dir]/[name].[ext]";
const DEFAULT_CHUNK_NAMING: &[u8] = b"[name]-[hash].[ext]";
const DEFAULT_ASSET_NAMING: &[u8] = b"[name]-[hash].[ext]";
options.entry_naming = match (&config.naming, &config.naming_entry) {
(Some(n), _) | (None, Some(n)) => Box::from(n.as_bytes()),
(None, None) => Box::from(DEFAULT_ENTRY_NAMING),
};
options.chunk_naming = match &config.naming_chunk {
Some(n) => Box::from(n.as_bytes()),
None => Box::from(DEFAULT_CHUNK_NAMING),
};
options.asset_naming = match &config.naming_asset {
Some(n) => Box::from(n.as_bytes()),
None => Box::from(DEFAULT_ASSET_NAMING),
};
if let Some(banner) = &config.banner {
options.banner = std::borrow::Cow::Owned(banner.as_bytes().to_vec());
}
if let Some(footer) = &config.footer {
options.footer = std::borrow::Cow::Owned(footer.as_bytes().to_vec());
}
if let Some(pp) = &config.public_path {
options.public_path = Box::from(pp.as_bytes());
}
if let Some(root) = &config.root {
options.root_dir = Box::from(root.as_bytes());
}
if config.jsx_runtime.is_some()
|| config.jsx_factory.is_some()
|| config.jsx_fragment.is_some()
|| config.jsx_import_source.is_some()
|| config.jsx_development.is_some()
{
let mut pragma = options.jsx.clone();
if let Some(runtime) = &config.jsx_runtime {
pragma.runtime = match runtime.as_str() {
"classic" => bun_options_types::jsx::Runtime::Classic,
_ => bun_options_types::jsx::Runtime::Automatic,
};
}
if let Some(factory) = &config.jsx_factory {
pragma.factory = member_list_from_dotted(factory);
}
if let Some(fragment) = &config.jsx_fragment {
pragma.fragment = member_list_from_dotted(fragment);
}
let _ = &config.jsx_import_source;
if let Some(development) = config.jsx_development {
pragma.development = development;
}
options.jsx = pragma.clone();
transpiler.resolver.opts.jsx = pragma;
}
}
fn member_list_from_dotted(dotted: &str) -> bun_options_types::jsx::MemberList {
bun_options_types::jsx::MemberList::Owned(
dotted
.split('.')
.map(|part| Box::from(part.as_bytes()))
.collect(),
)
}
fn map_outputs(output_files: &mut [OutputFile]) -> Vec<NativeOutputFile> {
let mut out = Vec::with_capacity(output_files.len());
for of in output_files.iter_mut() {
let path = String::from_utf8_lossy(&of.dest_path).into_owned();
let kind: &'static str = <&str>::from(of.output_kind);
let loader: &'static str = <&str>::from(of.loader);
let mime = mime_for_loader(of.loader, &of.dest_path);
let bytes = match &of.value {
OutputValue::Buffer { bytes } => bytes.to_vec(),
OutputValue::Saved(saved) => read_saved_bytes(saved, &path),
_ => read_src_bytes(of.src_path.text),
};
let sourcemap_index = (of.source_map_index != u32::MAX)
.then_some(of.source_map_index as usize);
out.push(NativeOutputFile {
path,
kind: kind.to_string(),
loader: loader.to_string(),
mime_type: mime,
hash: of.hash,
bytes,
sourcemap_index,
});
}
out
}
fn mime_for_loader(loader: Loader, dest_path: &[u8]) -> String {
let mime = loader.to_mime_type(&[dest_path]);
String::from_utf8_lossy(&mime.value).into_owned()
}
fn read_saved_bytes(_saved: &SavedFile, rel_path: &str) -> Vec<u8> {
let p = ::std::path::Path::new(rel_path);
::std::fs::read(p).unwrap_or_default()
}
fn read_src_bytes(src_text: &[u8]) -> Vec<u8> {
if src_text.is_empty() {
return Vec::new();
}
let path = ::std::path::Path::new(::std::str::from_utf8(src_text).unwrap_or(""));
::std::fs::read(path).unwrap_or_default()
}
fn collect_logs(log: &bun_ast::Log) -> Vec<NativeBuildLog> {
let mut out = Vec::new();
for msg in log.msgs.iter() {
let level = match msg.kind {
bun_ast::Kind::Err => "error",
bun_ast::Kind::Warn => "warn",
_ => "info",
};
let text = String::from_utf8_lossy(&msg.data.text).into_owned();
if text.is_empty() {
continue;
}
out.push(NativeBuildLog { level: level.to_string(), message: text });
}
out
}
fn write_outputs_to_disk(outdir: &str, outputs: &[NativeOutputFile], logs: &mut Vec<NativeBuildLog>) {
let root = ::std::path::Path::new(outdir);
for file in outputs {
if file.path.is_empty() {
continue;
}
let dest = root.join(&file.path);
if let Some(parent) = dest.parent() {
if let Err(e) = ::std::fs::create_dir_all(parent) {
logs.push(NativeBuildLog {
level: "error".into(),
message: format!("Bun.build: failed to create {}: {}", parent.display(), e),
});
continue;
}
}
if let Err(e) = ::std::fs::write(&dest, &file.bytes) {
logs.push(NativeBuildLog {
level: "error".into(),
message: format!("Bun.build: failed to write {}: {}", dest.display(), e),
});
}
}
}