use ::std::cell::RefCell;
use ::std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use ::std::sync::{Arc, Mutex, OnceLock};
use bao_engine::context::RawValueRootGuard;
use mozjs::jsapi::*;
use mozjs::jsval::{JSVal, ObjectValue, UndefinedValue};
use mozjs::realm::AutoRealm;
use mozjs::rooted;
use mozjs::rust::wrappers2::{JS_NewPlainObject, NewArrayObject1};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct NativeMinify {
pub whitespace: bool,
pub syntax: bool,
pub identifiers: bool,
}
impl NativeMinify {
pub fn all() -> Self {
Self { whitespace: true, syntax: true, identifiers: true }
}
}
#[derive(Clone, Debug)]
pub struct NativeBuildLog {
pub level: String,
pub message: String,
}
#[derive(Clone, Debug, Default)]
pub struct NativeBuildConfig {
pub entrypoints: Vec<String>,
pub outdir: Option<String>,
pub root: Option<String>,
pub target: String,
pub format: String,
pub naming: Option<String>,
pub naming_entry: Option<String>,
pub naming_chunk: Option<String>,
pub naming_asset: Option<String>,
pub minify: NativeMinify,
pub sourcemap: String,
pub external: Vec<String>,
pub define: Vec<(String, String)>,
pub splitting: bool,
pub banner: Option<String>,
pub footer: Option<String>,
pub public_path: Option<String>,
pub jsx_runtime: Option<String>,
pub jsx_factory: Option<String>,
pub jsx_fragment: Option<String>,
pub jsx_import_source: Option<String>,
pub jsx_development: Option<bool>,
}
#[derive(Clone, Debug)]
pub struct NativeOutputFile {
pub path: String,
pub kind: String,
pub loader: String,
pub mime_type: String,
pub hash: u64,
pub bytes: Vec<u8>,
pub sourcemap_index: Option<usize>,
}
#[derive(Clone, Debug, Default)]
pub struct NativeBuildResult {
pub success: bool,
pub outputs: Vec<NativeOutputFile>,
pub logs: Vec<NativeBuildLog>,
}
pub type NativeBuildFn = fn(&NativeBuildConfig) -> NativeBuildResult;
static NATIVE_BUILD_IMPL: OnceLock<NativeBuildFn> = OnceLock::new();
pub fn install_native_build_impl(f: NativeBuildFn) {
let _ = NATIVE_BUILD_IMPL.set(f);
}
pub fn native_build_installed() -> bool {
NATIVE_BUILD_IMPL.get().is_some()
}
struct PendingBuild {
cx: *mut JSContext,
promise_root: Option<RawValueRootGuard>,
promise_val: JSVal,
outcome: Arc<Mutex<Option<NativeBuildResult>>>,
mini_loop_ptr: *const bun_event_loop::MiniEventLoop::MiniEventLoop<'static>,
concurrent_task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext,
has_schedule_callback: AtomicBool,
}
unsafe impl ::std::marker::Send for PendingBuild {}
thread_local! {
static PENDING: RefCell<Vec<*mut PendingBuild>> = const { RefCell::new(Vec::new()) };
}
pub fn has_pending() -> bool {
PENDING.with(|p| !p.borrow().is_empty())
}
fn resolve_tasklet_shim(ctx: *mut PendingBuild, _parent: *mut ()) {
unsafe { resolve_tasklet(ctx) };
}
pub unsafe fn start(cx: *mut JSContext, promise_val: JSVal, config: NativeBuildConfig) {
let promise_root = unsafe {
RawValueRootGuard::new(
cx,
::std::slice::from_ref(&promise_val),
c"BuildTasklet.promise",
)
};
let rooted_val = promise_root.as_ref().map_or(promise_val, |g| g.get(0));
let outcome: Arc<Mutex<Option<NativeBuildResult>>> = Arc::new(Mutex::new(None));
let pending = Box::new(PendingBuild {
cx,
promise_root,
promise_val: rooted_val,
outcome: Arc::clone(&outcome),
mini_loop_ptr: ::std::ptr::null(), concurrent_task:
bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext::default(),
has_schedule_callback: AtomicBool::new(false),
});
let pending_ptr = Box::into_raw(pending);
let loop_ptr: *const bun_event_loop::MiniEventLoop::MiniEventLoop<'static> =
crate::timers::with_event_loop(|loop_| loop_ as *const _);
unsafe {
(*pending_ptr).mini_loop_ptr = loop_ptr;
let _field_offset = ::std::mem::offset_of!(PendingBuild, concurrent_task);
(*pending_ptr)
.concurrent_task
.from(pending_ptr, resolve_tasklet_shim);
}
PENDING.with(|p| p.borrow_mut().push(pending_ptr));
let Some(driver) = NATIVE_BUILD_IMPL.get().copied() else {
let degraded = NativeBuildResult {
success: false,
outputs: Vec::new(),
logs: vec![NativeBuildLog {
level: "error".into(),
message: "Bun.build: native bundler is not installed in this binary \
(install via bao_bundler::build_api::install)"
.into(),
}],
};
complete_build(pending_ptr, degraded);
return;
};
let pending_token = pending_ptr as usize;
::std::thread::spawn(move || {
let result = driver(&config);
let pending_ptr = pending_token as *mut PendingBuild;
complete_build(pending_ptr, result);
});
}
fn complete_build(pending_ptr: *mut PendingBuild, result: NativeBuildResult) {
unsafe {
{
let mut slot = (*pending_ptr).outcome.lock().unwrap();
*slot = Some(result);
}
if (*pending_ptr)
.has_schedule_callback
.compare_exchange(false, true, AtomicOrdering::AcqRel, AtomicOrdering::Acquire)
.is_ok()
{
let loop_ptr = (*pending_ptr).mini_loop_ptr;
if !loop_ptr.is_null() {
let concurrent_task_ptr =
::std::ptr::addr_of_mut!((*pending_ptr).concurrent_task);
let _ = bun_event_loop::ConcurrentWakeup::enqueue_task_concurrent_cross_thread(
loop_ptr as *mut bun_event_loop::MiniEventLoop::MiniEventLoop<'static>,
::std::ptr::NonNull::new_unchecked(concurrent_task_ptr),
);
}
}
}
}
unsafe fn resolve_tasklet(this: *mut PendingBuild) {
unsafe {
(*this).has_schedule_callback.store(false, AtomicOrdering::Release);
}
let outcome = unsafe {
(*this)
.outcome
.lock()
.ok()
.and_then(|mut slot| slot.take())
.unwrap_or(NativeBuildResult {
success: false,
outputs: Vec::new(),
logs: vec![NativeBuildLog {
level: "error".into(),
message: "Bun.build: result slot was empty".into(),
}],
})
};
let cx = unsafe { (*this).cx };
let pending = unsafe { &*this };
let promise_val = pending
.promise_root
.as_ref()
.map_or(pending.promise_val, |g| g.get(0));
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let promise_obj = promise_val.to_object());
let promise_h = promise_obj.handle().into();
{
let mut realm = AutoRealm::new_from_handle(cx_ref, promise_obj.handle());
let realm_cx: &mut mozjs::context::JSContext = &mut realm;
let output_obj = build_output_js(cx, &outcome);
if !output_obj.is_null() {
rooted!(&in(realm_cx) let out_val = ObjectValue(output_obj));
unsafe { JS::ResolvePromise(cx, promise_h, out_val.handle().into()) };
} else {
unsafe {
reject_with_message(cx, promise_h, "Bun.build: failed to build the output object")
};
}
}
PENDING.with(|p| {
let mut guard = p.borrow_mut();
if let Some(pos) = guard.iter().position(|&ptr| ptr == this) {
guard.swap_remove(pos);
}
});
unsafe { drop(Box::from_raw(this)) };
mozjs_sys::jsapi::js::RunJobs(cx);
}
unsafe fn reject_with_message(cx: *mut JSContext, promise_h: Handle<*mut JSObject>, msg: &str) {
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
let c_msg = bun_core::ZBox::from_vec(msg.as_bytes().to_vec());
let js_str = JS_NewStringCopyZ(cx, c_msg.as_ptr());
rooted!(&in(cx_ref) let reason = if js_str.is_null() {
UndefinedValue()
} else {
mozjs::jsval::StringValue(&*js_str)
});
unsafe { JS::RejectPromise(cx, promise_h, reason.handle().into()) };
}
unsafe fn build_output_js(cx: *mut JSContext, result: &NativeBuildResult) -> *mut JSObject {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let out = JS_NewPlainObject(cx_ref));
if out.get().is_null() {
return ::std::ptr::null_mut();
}
let out_h = out.handle().into();
rooted!(&in(cx_ref) let ok_val = mozjs::jsval::BooleanValue(result.success));
JS_DefineProperty(
cx,
out_h,
c"success".as_ptr(),
ok_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let outputs_arr = NewArrayObject1(cx_ref, result.outputs.len()));
for (idx, file) in result.outputs.iter().enumerate() {
let obj = build_artifact_js(cx, file);
if obj.is_null() {
continue;
}
rooted!(&in(cx_ref) let obj_val = ObjectValue(obj));
JS_SetElement(cx, outputs_arr.handle().into(), idx as u32, obj_val.handle().into());
}
for (idx, file) in result.outputs.iter().enumerate() {
let Some(sm_idx) = file.sourcemap_index else { continue };
if sm_idx >= result.outputs.len() || sm_idx == idx {
continue;
}
rooted!(&in(cx_ref) let mut obj_val = UndefinedValue());
JS_GetElement(
cx,
outputs_arr.handle().into(),
idx as u32,
obj_val.handle_mut().into(),
);
rooted!(&in(cx_ref) let mut sm_val = UndefinedValue());
JS_GetElement(
cx,
outputs_arr.handle().into(),
sm_idx as u32,
sm_val.handle_mut().into(),
);
if !obj_val.get().is_object() || !sm_val.get().is_object() {
continue;
}
rooted!(&in(cx_ref) let obj_r = obj_val.get().to_object());
JS_DefineProperty(
cx,
obj_r.handle().into(),
c"sourcemap".as_ptr(),
sm_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let outputs_val = ObjectValue(outputs_arr.get()));
JS_DefineProperty(
cx,
out_h,
c"outputs".as_ptr(),
outputs_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let logs_arr = NewArrayObject1(cx_ref, result.logs.len()));
for (idx, log) in result.logs.iter().enumerate() {
rooted!(&in(cx_ref) let log_obj = JS_NewPlainObject(cx_ref));
if log_obj.get().is_null() {
continue;
}
let log_h = log_obj.handle().into();
define_string_prop(cx, log_h, c"level", &log.level);
define_string_prop(cx, log_h, c"message", &log.message);
rooted!(&in(cx_ref) let lv = ObjectValue(log_obj.get()));
JS_SetElement(cx, logs_arr.handle().into(), idx as u32, lv.handle().into());
}
rooted!(&in(cx_ref) let logs_val = ObjectValue(logs_arr.get()));
JS_DefineProperty(
cx,
out_h,
c"logs".as_ptr(),
logs_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
out.get()
}
unsafe fn define_string_prop(
cx: *mut JSContext,
obj: mozjs::jsapi::Handle<*mut JSObject>,
name: &::std::ffi::CStr,
value: &str,
) {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
let c_val = bun_core::ZBox::from_vec(value.as_bytes().to_vec());
let js_str = JS_NewStringCopyZ(cx, c_val.as_ptr());
if !js_str.is_null() {
rooted!(&in(cx_ref) let v = mozjs::jsval::StringValue(&*js_str));
JS_DefineProperty(cx, obj, name.as_ptr(), v.handle().into(), JSPROP_ENUMERATE as u32);
}
}
unsafe fn build_artifact_js(cx: *mut JSContext, file: &NativeOutputFile) -> *mut JSObject {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
let global = CurrentGlobalOrNull(cx);
if global.is_null() {
return ::std::ptr::null_mut();
}
rooted!(&in(cx_ref) let global_rooted = global);
rooted!(&in(cx_ref) let artifact = JS_NewPlainObject(cx_ref));
if artifact.get().is_null() {
return ::std::ptr::null_mut();
}
let art_h = artifact.handle().into();
let len = file.bytes.len();
let arr_obj = JS_NewUint8Array(cx, len);
if !arr_obj.is_null() {
rooted!(&in(cx_ref) let arr_root = arr_obj);
if len > 0 {
let mut is_shared = false;
let data_ptr = JS_GetUint8ArrayData(arr_root.get(), &mut is_shared, ::std::ptr::null());
if !data_ptr.is_null() {
::std::ptr::copy_nonoverlapping(file.bytes.as_ptr(), data_ptr, len);
}
}
rooted!(&in(cx_ref) let chunks_arr = NewArrayObject1(cx_ref, 1));
if !chunks_arr.get().is_null() {
rooted!(&in(cx_ref) let arr_val = ObjectValue(arr_root.get()));
JS_SetElement(
cx,
chunks_arr.handle().into(),
0,
arr_val.handle().into(),
);
rooted!(&in(cx_ref) let chunks_val = ObjectValue(chunks_arr.get()));
JS_DefineProperty(
cx,
art_h,
c"_chunks".as_ptr(),
chunks_val.handle().into(),
0, );
}
}
rooted!(&in(cx_ref) let size_val = mozjs::jsval::DoubleValue(len as f64));
JS_DefineProperty(
cx,
art_h,
c"size".as_ptr(),
size_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
define_string_prop(cx, art_h, c"type", &file.mime_type);
define_string_prop(cx, art_h, c"path", &file.path);
define_string_prop(cx, art_h, c"kind", &file.kind);
define_string_prop(cx, art_h, c"loader", &file.loader);
rooted!(&in(cx_ref) let hash_val = mozjs::jsval::DoubleValue(file.hash as f64));
JS_DefineProperty(
cx,
art_h,
c"hash".as_ptr(),
hash_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
rooted!(&in(cx_ref) let mut blob_val = UndefinedValue());
JS_GetProperty(
cx,
global_rooted.handle().into(),
c"Blob".as_ptr(),
blob_val.handle_mut().into(),
);
let mut has_blob_proto = false;
if blob_val.get().is_object() {
rooted!(&in(cx_ref) let blob_ctor = blob_val.get().to_object());
rooted!(&in(cx_ref) let mut proto_val = UndefinedValue());
JS_GetProperty(
cx,
blob_ctor.handle().into(),
c"prototype".as_ptr(),
proto_val.handle_mut().into(),
);
if proto_val.get().is_object() {
rooted!(&in(cx_ref) let proto = proto_val.get().to_object());
if JS_SetPrototype(cx, art_h, proto.handle().into()) {
has_blob_proto = true;
}
}
}
if !has_blob_proto {
JS_DefineFunction(
cx,
art_h,
c"text".as_ptr(),
Some(artifact_text_fallback),
0,
0,
);
JS_DefineFunction(
cx,
art_h,
c"arrayBuffer".as_ptr(),
Some(artifact_arraybuffer_fallback),
0,
0,
);
}
artifact.get()
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn artifact_text_fallback(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, 0);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
rooted!(&in(cx_ref) let promise = JS::NewPromiseObject(cx, HandleObject::null()));
if promise.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let bytes = read_first_chunk(cx, obj.get());
let text = String::from_utf8_lossy(&bytes);
let c_text = bun_core::ZBox::from_vec(text.as_bytes().to_vec());
let js_str = JS_NewStringCopyZ(cx, c_text.as_ptr());
if !js_str.is_null() {
rooted!(&in(cx_ref) let str_val = mozjs::jsval::StringValue(&*js_str));
JS::ResolvePromise(cx, promise.handle().into(), str_val.handle().into());
}
args.rval().set(ObjectValue(promise.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn artifact_arraybuffer_fallback(
cx: *mut JSContext,
_argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, 0);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
rooted!(&in(cx_ref) let promise = JS::NewPromiseObject(cx, HandleObject::null()));
if promise.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let bytes = read_first_chunk(cx, obj.get());
let arr_obj = JS_NewUint8Array(cx, bytes.len());
if !arr_obj.is_null() {
rooted!(&in(cx_ref) let arr_root = arr_obj);
if !bytes.is_empty() {
let mut is_shared = false;
let data_ptr =
JS_GetUint8ArrayData(arr_root.get(), &mut is_shared, ::std::ptr::null());
if !data_ptr.is_null() {
::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data_ptr, bytes.len());
}
}
rooted!(&in(cx_ref) let mut buffer_val = UndefinedValue());
JS_GetProperty(
cx,
arr_root.handle().into(),
c"buffer".as_ptr(),
buffer_val.handle_mut().into(),
);
if buffer_val.get().is_object() {
rooted!(&in(cx_ref) let bv = buffer_val.get());
JS::ResolvePromise(cx, promise.handle().into(), bv.handle().into());
}
}
args.rval().set(ObjectValue(promise.get()));
true
}
unsafe fn read_first_chunk(cx: *mut JSContext, obj: *mut JSObject) -> Vec<u8> {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_root = obj);
rooted!(&in(cx_ref) let mut chunks_val = UndefinedValue());
JS_GetProperty(
cx,
obj_root.handle().into(),
c"_chunks".as_ptr(),
chunks_val.handle_mut().into(),
);
if !chunks_val.get().is_object() {
return Vec::new();
}
rooted!(&in(cx_ref) let chunks = chunks_val.get().to_object());
rooted!(&in(cx_ref) let mut first_val = UndefinedValue());
JS_GetElement(cx, chunks.handle().into(), 0, first_val.handle_mut().into());
if !first_val.get().is_object() {
return Vec::new();
}
rooted!(&in(cx_ref) let arr = first_val.get().to_object());
rooted!(&in(cx_ref) let mut len_val = UndefinedValue());
JS_GetProperty(cx, arr.handle().into(), c"length".as_ptr(), len_val.handle_mut().into());
if !len_val.get().is_number() {
return Vec::new();
}
let len = len_val.get().to_number() as usize;
if len == 0 {
return Vec::new();
}
let mut is_shared = false;
let data_ptr = JS_GetUint8ArrayData(arr.get(), &mut is_shared, ::std::ptr::null());
if data_ptr.is_null() {
return Vec::new();
}
unsafe { ::std::slice::from_raw_parts(data_ptr, len) }.to_vec()
}