diff --git a/packages/bun-build-mdx-rs/src/lib.rs b/packages/bun-build-mdx-rs/src/lib.rs index 4b93e6037f..b0859b97ee 100644 --- a/packages/bun-build-mdx-rs/src/lib.rs +++ b/packages/bun-build-mdx-rs/src/lib.rs @@ -1,55 +1,25 @@ -use bun_native_plugin::{define_bun_plugin, BunLoader, OnBeforeParse}; +use bun_native_plugin::{anyhow, bun, define_bun_plugin, BunLoader, Result}; use mdxjs::{compile, Options as CompileOptions}; use napi_derive::napi; -#[macro_use] -extern crate napi; - define_bun_plugin!("bun-mdx-rs"); -#[no_mangle] -pub extern "C" fn bun_mdx_rs( - args: *const bun_native_plugin::sys::OnBeforeParseArguments, - result: *mut bun_native_plugin::sys::OnBeforeParseResult, -) { - let args = unsafe { &*args }; - - let mut handle = match OnBeforeParse::from_raw(args, result) { - Ok(handle) => handle, - Err(_) => { - return; - } - }; - - let source_str = match handle.input_source_code() { - Ok(source_str) => source_str, - Err(_) => { - handle.log_error("Failed to fetch source code"); - return; - } - }; +#[bun] +pub fn bun_mdx_rs(handle: &mut OnBeforeParse) -> Result<()> { + let source_str = handle.input_source_code()?; let mut options = CompileOptions::gfm(); // Leave it as JSX for Bun to handle options.jsx = true; - let path = match handle.path() { - Ok(path) => path, - Err(e) => { - handle.log_error(&format!("Failed to get path: {:?}", e)); - return; - } - }; + let path = handle.path()?; options.filepath = Some(path.to_string()); - match compile(&source_str, &options) { - Ok(compiled) => { - handle.set_output_source_code(compiled, BunLoader::BUN_LOADER_JSX); - } - Err(_) => { - handle.log_error("Failed to compile MDX"); - return; - } - } + let jsx = compile(&source_str, &options) + .map_err(|e| anyhow::anyhow!("Failed to compile MDX: {:?}", e))?; + + handle.set_output_source_code(jsx, BunLoader::BUN_LOADER_JSX); + + Ok(()) } diff --git a/packages/bun-native-plugin-rs/Cargo.lock b/packages/bun-native-plugin-rs/Cargo.lock index 202700fa3a..0c786953f7 100644 --- a/packages/bun-native-plugin-rs/Cargo.lock +++ b/packages/bun-native-plugin-rs/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + [[package]] name = "bindgen" version = "0.70.1" @@ -37,11 +43,24 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bun-macro" +version = "0.1.0" +dependencies = [ + "anyhow", + "napi", + "quote", + "syn", +] + [[package]] name = "bun-native-plugin" version = "0.1.0" dependencies = [ + "anyhow", "bindgen", + "bun-macro", + "napi", ] [[package]] @@ -70,6 +89,25 @@ dependencies = [ "libloading", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "either" version = "1.13.0" @@ -125,6 +163,55 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "napi" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b" +dependencies = [ + "bitflags", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", +] + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + [[package]] name = "nom" version = "7.1.3" @@ -135,6 +222,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "prettyplease" version = "0.2.25" @@ -221,6 +314,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/packages/bun-native-plugin-rs/Cargo.toml b/packages/bun-native-plugin-rs/Cargo.toml index bf4d7b784b..55476cbd83 100644 --- a/packages/bun-native-plugin-rs/Cargo.toml +++ b/packages/bun-native-plugin-rs/Cargo.toml @@ -5,3 +5,13 @@ edition = "2021" [build-dependencies] bindgen = "0.70.1" + +[dependencies] +anyhow = "1.0.94" +bun-macro = { path = "./bun-macro" } +napi = { version = "2.14.1", default-features = false, features = ["napi4"] } + +[features] +default = ["napi"] +napi = [] + diff --git a/packages/bun-native-plugin-rs/README.md b/packages/bun-native-plugin-rs/README.md index f235849872..6c57a2c9d1 100644 --- a/packages/bun-native-plugin-rs/README.md +++ b/packages/bun-native-plugin-rs/README.md @@ -4,7 +4,7 @@ This crate provides a Rustified wrapper over the Bun's native bundler plugin C API. -Some advantages to _native_ bundler plugins as opposed to regular ones implemented in JS: +Some advantages to _native_ bundler plugins as opposed to regular ones implemented in JS are: - Native plugins take full advantage of Bun's parallelized bundler pipeline and run on multiple threads at the same time - Unlike JS, native plugins don't need to do the UTF-8 <-> UTF-16 source code string conversions @@ -30,61 +30,84 @@ Then install this crate: cargo add bun-native-plugin ``` -Now, inside the `lib.rs` file, expose a C ABI function which has the same function signature as the plugin lifecycle hook that you want to implement. +Now, inside the `lib.rs` file, we'll use the `bun_native_plugin::bun` proc macro to define a function which +will implement our native plugin. -For example, implementing `onBeforeParse`: +Here's an example implementing the `onBeforeParse` hook: ```rs -use bun_native_plugin::{define_bun_plugin, OnBeforeParse}; +use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader}; use napi_derive::napi; -/// Define with the name of the plugin +/// Define the plugin and its name define_bun_plugin!("replace-foo-with-bar"); -/// This is necessary for napi-rs to compile this into a proper NAPI module -#[napi] -pub fn register_bun_plugin() {} - -/// Use `no_mangle` so that we can reference this symbol by name later -/// when registering this native plugin in JS. +/// Here we'll implement `onBeforeParse` with code that replaces all occurences of +/// `foo` with `bar`. /// -/// Here we'll create a dummy plugin which replaces all occurences of -/// `foo` with `bar` -#[no_mangle] -pub extern "C" fn on_before_parse_plugin_impl( - args: *const bun_native_plugin::sys::OnBeforeParseArguments, - result: *mut bun_native_plugin::sys::OnBeforeParseResult, -) { - let args = unsafe { &*args }; - - // This returns a handle which is a safe wrapper over the raw - // C API. - let mut handle = OnBeforeParse::from_raw(args, result) { - Ok(handle) => handle, - Err(_) => { - // `OnBeforeParse::from_raw` handles error logging - // so it fine to return here. - return; - } - }; - - let input_source_code = match handle.input_source_code() { - Ok(source_str) => source_str, - Err(_) => { - // If we encounter an error, we must log it so that - // Bun knows this plugin failed. - handle.log_error("Failed to fetch source code!"); - return; - } - }; +/// We use the #[bun] macro to generate some of the boilerplate code. +/// +/// The argument of the function (`handle: &mut OnBeforeParse`) tells +/// the macro that this function implements the `onBeforeParse` hook. +#[bun] +pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> { + // Fetch the input source code. + let input_source_code = handle.input_source_code()?; + // Get the Loader for the file let loader = handle.output_loader(); - let output_source_code = source_str.replace("foo", "bar"); - handle.set_output_source_code(output_source_code, loader); + + + let output_source_code = input_source_code.replace("foo", "bar"); + + handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX); + + Ok(()) } ``` -Then compile this NAPI module. If you using napi-rs, the `package.json` should have a `build` script you can run: +Internally, the `#[bun]` macro wraps your code and declares a C ABI function which implements +the function signature of `onBeforeParse` plugins in Bun's C API for bundler plugins. + +Then it calls your code. The wrapper looks _roughly_ like this: + +```rs +pub extern "C" fn replace_foo_with_bar( + args: *const bun_native_plugin::sys::OnBeforeParseArguments, + result: *mut bun_native_plugin::sys::OnBeforeParseResult, +) { + // The actual code you wrote is inlined here + fn __replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> { + // Fetch the input source code. + let input_source_code = handle.input_source_code()?; + + // Get the Loader for the file + let loader = handle.output_loader(); + + + let output_source_code = input_source_code.replace("foo", "bar"); + + handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX); + + Ok(()) + } + + let args = unsafe { &*args }; + + let mut handle = OnBeforeParse::from_raw(args, result) { + Ok(handle) => handle, + Err(_) => { + return; + } + }; + + if let Err(e) = __replace_fo_with_bar(&handle) { + handle.log_err(&e.to_string()); + } +} +``` + +Now, let's compile this NAPI module. If you're using napi-rs, the `package.json` should have a `build` script you can run: ```bash bun run build @@ -107,7 +130,7 @@ const result = await Bun.build({ // We tell it to use function we implemented inside of our `lib.rs` code. build.onBeforeParse( { filter: /\.ts/ }, - { napiModule, symbol: "on_before_parse_plugin_impl" }, + { napiModule, symbol: "replace_foo_with_bar" }, ); }, }, @@ -119,19 +142,14 @@ const result = await Bun.build({ ### Error handling and panics -It is highly recommended to avoid panicking as this will crash the runtime. Instead, you must handle errors and log them: +In the case that the value of the `Result` your plugin function returns is an `Err(...)`, the error will be logged to Bun's bundler. -```rs -let input_source_code = match handle.input_source_code() { - Ok(source_str) => source_str, - Err(_) => { - // If we encounter an error, we must log it so that - // Bun knows this plugin failed. - handle.log_error("Failed to fetch source code!"); - return; - } -}; -``` +It is highly advised that you return all errors and avoid `.unwrap()`'ing or `.expecting()`'ing results. + +The `#[bun]` wrapper macro actually runs your code inside of a [`panic::catch_unwind`](https://doc.rust-lang.org/std/panic/fn.catch_unwind.html), +which may catch _some_ panics but **not guaranteed to catch all panics**. + +Therefore, it is recommended to **avoid panics at all costs**. ### Passing state to and from JS: `External` @@ -199,41 +217,16 @@ console.log("Total `foo`s encountered: ", pluginState.getFooCount()); Finally, from the native implementation of your plugin, you can extract the external: ```rs -pub extern "C" fn on_before_parse_plugin_impl( - args: *const bun_native_plugin::sys::OnBeforeParseArguments, - result: *mut bun_native_plugin::sys::OnBeforeParseResult, -) { - let args = unsafe { &*args }; - - let mut handle = OnBeforeParse::from_raw(args, result) { - Ok(handle) => handle, - Err(_) => { - // `OnBeforeParse::from_raw` handles error logging - // so it fine to return here. - return; - } - }; - - let plugin_state: &PluginState = +#[bun] +pub fn on_before_parse_plugin_impl(handle: &mut OnBeforeParse) { // This operation is only safe if you pass in an external when registering the plugin. // If you don't, this could lead to a segfault or access of undefined memory. - match unsafe { handle.external().and_then(|state| state.ok_or(Error::Unknown)) } { - Ok(state) => state, - Err(_) => { - handle.log_error("Failed to get external!"); - return; - } - }; + let plugin_state: &PluginState = + unsafe { handle.external().and_then(|state| state.ok_or(Error::Unknown))? }; // Fetch our source code again - let input_source_code = match handle.input_source_code() { - Ok(source_str) => source_str, - Err(_) => { - handle.log_error("Failed to fetch source code!"); - return; - } - }; + let input_source_code = handle.input_source_code()?; // Count the number of `foo`s and add it to our state let foo_count = source_code.matches("foo").count() as u32; @@ -243,6 +236,6 @@ pub extern "C" fn on_before_parse_plugin_impl( ### Concurrency -Your `extern "C"` plugin function can be called _on any thread_ at _any time_ and _multiple times at once_. +Your plugin function can be called _on any thread_ at _any time_ and possibly _multiple times at once_. -Therefore, you must design any state management to be threadsafe +Therefore, you must design any state management to be threadsafe. diff --git a/packages/bun-native-plugin-rs/bun-macro/Cargo.toml b/packages/bun-native-plugin-rs/bun-macro/Cargo.toml new file mode 100644 index 0000000000..f7491dee4b --- /dev/null +++ b/packages/bun-native-plugin-rs/bun-macro/Cargo.toml @@ -0,0 +1,14 @@ + +[package] +name = "bun-macro" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +napi = "2.16.13" +anyhow = "1.0.94" \ No newline at end of file diff --git a/packages/bun-native-plugin-rs/bun-macro/src/lib.rs b/packages/bun-native-plugin-rs/bun-macro/src/lib.rs new file mode 100644 index 0000000000..efedbef86e --- /dev/null +++ b/packages/bun-native-plugin-rs/bun-macro/src/lib.rs @@ -0,0 +1,54 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Ident, ItemFn}; + +#[proc_macro_attribute] +pub fn bun(_attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the input function + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = &input_fn.sig.ident; + let inner_fn_name = Ident::new(&format!("__{}", fn_name), fn_name.span()); + let fn_block = &input_fn.block; + + // Generate the wrapped function + let output = quote! { + #[no_mangle] + pub unsafe extern "C" fn #fn_name( + args_raw: *mut bun_native_plugin::sys::OnBeforeParseArguments, + result: *mut bun_native_plugin::sys::OnBeforeParseResult, + ) { + fn #inner_fn_name(handle: &mut bun_native_plugin::OnBeforeParse) -> Result<()> { + #fn_block + } + + let args_path = unsafe { (*args_raw).path_ptr }; + let args_path_len = unsafe { (*args_raw).path_len }; + let result_pointer = result; + + let result = std::panic::catch_unwind(|| { + let mut handle = match bun_native_plugin::OnBeforeParse::from_raw(args_raw, result) { + Ok(handle) => handle, + Err(_) => return, + }; + if let Err(e) = #inner_fn_name(&mut handle) { + handle.log_error(&format!("{:?}", e)); + } + }); + + if let Err(e) = result { + let msg_string = format!("Plugin crashed: {:?}", e); + let mut log_options = bun_native_plugin::log_from_message_and_level( + &msg_string, + bun_native_plugin::sys::BunLogLevel::BUN_LOG_LEVEL_ERROR, + args_path, + args_path_len, + ); + unsafe { + ((*result_pointer).log.unwrap())(args_raw, &mut log_options); + } + } + } + }; + + output.into() +} diff --git a/packages/bun-native-plugin-rs/src/lib.rs b/packages/bun-native-plugin-rs/src/lib.rs index 3e589e3bcd..1a8f85941c 100644 --- a/packages/bun-native-plugin-rs/src/lib.rs +++ b/packages/bun-native-plugin-rs/src/lib.rs @@ -244,10 +244,11 @@ //! Your `extern "C"` plugin function can be called _on any thread_ at _any time_ and _multiple times at once_. //! //! Therefore, you must design any state management to be threadsafe - #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] +pub use anyhow; +pub use bun_macro::bun; #[repr(transparent)] pub struct BunPluginName(*const c_char); @@ -261,7 +262,7 @@ impl BunPluginName { #[macro_export] macro_rules! define_bun_plugin { ($name:expr) => { - pub static BUN_PLUGIN_NAME_STRING: &str = $name; + pub static BUN_PLUGIN_NAME_STRING: &str = concat!($name, "\0"); #[no_mangle] pub static BUN_PLUGIN_NAME: bun_native_plugin::BunPluginName = @@ -279,7 +280,9 @@ use std::{ borrow::Cow, cell::UnsafeCell, ffi::{c_char, c_void}, + marker::PhantomData, str::Utf8Error, + sync::PoisonError, }; pub mod sys { @@ -323,7 +326,7 @@ impl Drop for SourceCodeContext { pub type BunLogLevel = sys::BunLogLevel; pub type BunLoader = sys::BunLoader; -fn get_from_raw_str<'a>(ptr: *const u8, len: usize) -> Result> { +fn get_from_raw_str<'a>(ptr: *const u8, len: usize) -> PluginResult> { let slice: &'a [u8] = unsafe { std::slice::from_raw_parts(ptr, len) }; // Windows allows invalid UTF-16 strings in the filesystem. These get converted to WTF-8 in Zig. @@ -351,9 +354,31 @@ pub enum Error { IncompatiblePluginVersion, ExternalTypeMismatch, Unknown, + LockPoisoned, } -pub type Result = std::result::Result; +pub type PluginResult = std::result::Result; +pub type Result = anyhow::Result; + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } + + fn description(&self) -> &str { + "description() is deprecated; use Display" + } + + fn cause(&self) -> Option<&dyn std::error::Error> { + self.source() + } +} impl From for Error { fn from(value: Utf8Error) -> Self { @@ -361,6 +386,12 @@ impl From for Error { } } +impl From> for Error { + fn from(_: PoisonError) -> Self { + Self::LockPoisoned + } +} + /// A safe handle for the arguments + result struct for the /// `OnBeforeParse` bundler lifecycle hook. /// @@ -370,9 +401,10 @@ impl From for Error { /// /// To initialize this struct, see the `from_raw` method. pub struct OnBeforeParse<'a> { - args_raw: &'a sys::OnBeforeParseArguments, + pub args_raw: *mut sys::OnBeforeParseArguments, result_raw: *mut sys::OnBeforeParseResult, compilation_context: *mut SourceCodeContext, + __phantom: PhantomData<&'a ()>, } impl<'a> OnBeforeParse<'a> { @@ -394,10 +426,10 @@ impl<'a> OnBeforeParse<'a> { /// } /// ``` pub fn from_raw( - args: &'a sys::OnBeforeParseArguments, + args: *mut sys::OnBeforeParseArguments, result: *mut sys::OnBeforeParseResult, - ) -> Result { - if args.__struct_size < std::mem::size_of::() + ) -> PluginResult { + if unsafe { (*args).__struct_size } < std::mem::size_of::() || unsafe { (*result).__struct_size } < std::mem::size_of::() { let message = "This plugin is not compatible with the current version of Bun."; @@ -405,8 +437,8 @@ impl<'a> OnBeforeParse<'a> { __struct_size: std::mem::size_of::(), message_ptr: message.as_ptr(), message_len: message.len(), - path_ptr: args.path_ptr, - path_len: args.path_len, + path_ptr: unsafe { (*args).path_ptr }, + path_len: unsafe { (*args).path_len }, source_line_text_ptr: std::ptr::null(), source_line_text_len: 0, level: BunLogLevel::BUN_LOG_LEVEL_ERROR as i8, @@ -426,15 +458,21 @@ impl<'a> OnBeforeParse<'a> { args_raw: args, result_raw: result, compilation_context: std::ptr::null_mut() as *mut _, + __phantom: Default::default(), }) } - pub fn path(&self) -> Result> { - get_from_raw_str(self.args_raw.path_ptr, self.args_raw.path_len) + pub fn path(&self) -> PluginResult> { + unsafe { get_from_raw_str((*self.args_raw).path_ptr, (*self.args_raw).path_len) } } - pub fn namespace(&self) -> Result> { - get_from_raw_str(self.args_raw.namespace_ptr, self.args_raw.namespace_len) + pub fn namespace(&self) -> PluginResult> { + unsafe { + get_from_raw_str( + (*self.args_raw).namespace_ptr, + (*self.args_raw).namespace_len, + ) + } } /// Get the external object from the `OnBeforeParse` arguments. @@ -485,12 +523,13 @@ impl<'a> OnBeforeParse<'a> { /// }, /// }; /// ``` - pub unsafe fn external(&self) -> Result> { - if self.args_raw.external.is_null() { + pub unsafe fn external(&self) -> PluginResult> { + if unsafe { (*self.args_raw).external.is_null() } { return Ok(None); } - let external: *mut TaggedObject = self.args_raw.external as *mut TaggedObject; + let external: *mut TaggedObject = + unsafe { (*self.args_raw).external as *mut TaggedObject }; unsafe { if (*external).type_id != TypeId::of::() { @@ -505,12 +544,13 @@ impl<'a> OnBeforeParse<'a> { /// /// This is unsafe as you must ensure that no other invocation of the plugin /// simultaneously holds a mutable reference to the external. - pub unsafe fn external_mut(&mut self) -> Result> { - if self.args_raw.external.is_null() { + pub unsafe fn external_mut(&mut self) -> PluginResult> { + if unsafe { (*self.args_raw).external.is_null() } { return Ok(None); } - let external: *mut TaggedObject = self.args_raw.external as *mut TaggedObject; + let external: *mut TaggedObject = + unsafe { (*self.args_raw).external as *mut TaggedObject }; unsafe { if (*external).type_id != TypeId::of::() { @@ -525,9 +565,12 @@ impl<'a> OnBeforeParse<'a> { /// /// On Windows, this function may return an `Err(Error::Utf8(...))` if the /// source code contains invalid UTF-8. - pub fn input_source_code(&self) -> Result> { + pub fn input_source_code(&self) -> PluginResult> { let fetch_result = unsafe { - ((*self.result_raw).fetchSourceCode.unwrap())(self.args_raw, self.result_raw) + ((*self.result_raw).fetchSourceCode.unwrap())( + self.args_raw as *const _, + self.result_raw, + ) }; if fetch_result != 0 { @@ -587,7 +630,7 @@ impl<'a> OnBeforeParse<'a> { } /// Set the output loader for the current file. - pub fn set_output_loader(&self, loader: BunLogLevel) { + pub fn set_output_loader(&self, loader: BunLoader) { // SAFETY: We don't hand out mutable references to `result_raw` so dereferencing it is safe. unsafe { (*self.result_raw).loader = loader as u8; @@ -606,22 +649,36 @@ impl<'a> OnBeforeParse<'a> { /// Log a message with the given level. pub fn log(&self, message: &str, level: BunLogLevel) { - let mut log_options = sys::BunLogOptions { - __struct_size: std::mem::size_of::(), - message_ptr: message.as_ptr(), - message_len: message.len(), - path_ptr: self.args_raw.path_ptr, - path_len: self.args_raw.path_len, - source_line_text_ptr: std::ptr::null(), - source_line_text_len: 0, - level: level as i8, - line: 0, - lineEnd: 0, - column: 0, - columnEnd: 0, - }; + let mut log_options = log_from_message_and_level( + message, + level, + unsafe { (*self.args_raw).path_ptr }, + unsafe { (*self.args_raw).path_len }, + ); unsafe { ((*self.result_raw).log.unwrap())(self.args_raw, &mut log_options); } } } + +pub fn log_from_message_and_level( + message: &str, + level: BunLogLevel, + path: *const u8, + path_len: usize, +) -> sys::BunLogOptions { + sys::BunLogOptions { + __struct_size: std::mem::size_of::(), + message_ptr: message.as_ptr(), + message_len: message.len(), + path_ptr: path as *const _, + path_len, + source_line_text_ptr: std::ptr::null(), + source_line_text_len: 0, + level: level as i8, + line: 0, + lineEnd: 0, + column: 0, + columnEnd: 0, + } +} diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index d4c21b6768..62cc3fa6c1 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -428,12 +428,23 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, EncodedJSValue exportsValue = JSC::JSValue::encode(exports); JSC::JSValue resultValue = JSValue::decode(napi_register_module_v1(globalObject, exportsValue)); - // TODO: think about the finalizer here - // currently we do not dealloc napi modules so we don't have to worry about it right now - auto* meta = new Bun::NapiModuleMeta(globalObject->m_pendingNapiModuleDlopenHandle); - Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, nullptr); - bool success = resultValue.getObject()->putDirect(vm, WebCore::builtinNames(vm).napiDlopenHandlePrivateName(), napi_external, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); - ASSERT(success); + if (auto resultObject = resultValue.getObject()) { +#if OS(DARWIN) || OS(LINUX) + // If this is a native bundler plugin we want to store the handle from dlopen + // as we are going to call `dlsym()` on it later to get the plugin implementation. + const char** pointer_to_plugin_name = (const char**)dlsym(handle, "BUN_PLUGIN_NAME"); +#elif OS(WINDOWS) + const char** pointer_to_plugin_name = (const char**)GetProcAddress(handle, "BUN_PLUGIN_NAME"); +#endif + if (pointer_to_plugin_name) { + // TODO: think about the finalizer here + // currently we do not dealloc napi modules so we don't have to worry about it right now + auto* meta = new Bun::NapiModuleMeta(globalObject->m_pendingNapiModuleDlopenHandle); + Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, nullptr); + bool success = resultObject->putDirect(vm, WebCore::builtinNames(vm).napiDlopenHandlePrivateName(), napi_external, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); + ASSERT(success); + } + } RETURN_IF_EXCEPTION(scope, {}); diff --git a/src/bun.js/bindings/JSBundlerPlugin.cpp b/src/bun.js/bindings/JSBundlerPlugin.cpp index c1ec1fd4b8..80b25b4373 100644 --- a/src/bun.js/bindings/JSBundlerPlugin.cpp +++ b/src/bun.js/bindings/JSBundlerPlugin.cpp @@ -38,6 +38,7 @@ namespace Bun { extern "C" int OnBeforeParsePlugin__isDone(void* context); +extern "C" void OnBeforeParseResult__reset(OnBeforeParseResult* result); #define WRAP_BUNDLER_PLUGIN(argName) jsDoubleNumber(bitwise_cast(reinterpret_cast(argName))) #define UNWRAP_BUNDLER_PLUGIN(callFrame) reinterpret_cast(bitwise_cast(callFrame->argument(0).asDouble())) @@ -61,21 +62,18 @@ void BundlerPlugin::NamespaceList::append(JSC::VM& vm, JSC::RegExp* filter, Stri if (nsGroup == nullptr) { namespaces.append(namespaceString); - groups.append(Vector {}); + groups.append(Vector {}); nsGroup = &groups.last(); index = namespaces.size() - 1; } - Yarr::RegularExpression regex( - StringView(filter->pattern()), - filter->flags()); - - nsGroup->append(WTFMove(regex)); + auto pattern = filter->pattern(); + auto filter_regexp = FilterRegExp(pattern, filter->flags()); + nsGroup->append(WTFMove(filter_regexp)); } static bool anyMatchesForNamespace(JSC::VM& vm, BundlerPlugin::NamespaceList& list, const BunString* namespaceStr, const BunString* path) { - constexpr bool usesPatternContextBuffer = false; if (list.fileNamespace.isEmpty() && list.namespaces.isEmpty()) return false; @@ -92,8 +90,7 @@ static bool anyMatchesForNamespace(JSC::VM& vm, BundlerPlugin::NamespaceList& li auto pathString = path->toWTFString(BunString::ZeroCopy); for (auto& filter : filters) { - Yarr::MatchingContextHolder regExpContext(vm, usesPatternContextBuffer, nullptr, Yarr::MatchFrom::CompilerThread); - if (filter.match(pathString) > -1) { + if (filter.match(vm, pathString)) { return true; } } @@ -243,18 +240,14 @@ void BundlerPlugin::NativePluginList::append(JSC::VM& vm, JSC::RegExp* filter, S if (nsGroup == nullptr) { namespaces.append(namespaceString); - groups.append(Vector {}); + groups.append(Vector {}); nsGroup = &groups.last(); index = namespaces.size() - 1; } - Yarr::RegularExpression regex( - StringView(filter->pattern()), - filter->flags()); - - NativeFilterRegexp nativeFilterRegexp = std::make_pair(regex, std::make_shared()); - - nsGroup->append(nativeFilterRegexp); + auto pattern = filter->pattern(); + auto filter_regexp = FilterRegExp(pattern, filter->flags()); + nsGroup->append(WTFMove(filter_regexp)); } if (index == std::numeric_limits::max()) { @@ -271,45 +264,54 @@ void BundlerPlugin::NativePluginList::append(JSC::VM& vm, JSC::RegExp* filter, S } } +bool BundlerPlugin::FilterRegExp::match(JSC::VM& vm, const String& path) +{ + WTF::Locker locker { lock }; + constexpr bool usesPatternContextBuffer = false; + Yarr::MatchingContextHolder regExpContext(vm, usesPatternContextBuffer, nullptr, Yarr::MatchFrom::CompilerThread); + return regex.match(path) != -1; +} + extern "C" void CrashHandler__setInsideNativePlugin(const char* plugin_name); -int BundlerPlugin::NativePluginList::call(JSC::VM& vm, BundlerPlugin* plugin, int* shouldContinue, void* bunContextPtr, const BunString* namespaceStr, const BunString* pathString, void* onBeforeParseArgs, void* onBeforeParseResult) +int BundlerPlugin::NativePluginList::call(JSC::VM& vm, BundlerPlugin* plugin, int* shouldContinue, void* bunContextPtr, const BunString* namespaceStr, const BunString* pathString, OnBeforeParseArguments* onBeforeParseArgs, OnBeforeParseResult* onBeforeParseResult) { unsigned index = 0; - const auto* group = this->group(namespaceStr->toWTFString(BunString::ZeroCopy), index); - if (group == nullptr) { + auto* groupPtr = this->group(namespaceStr->toWTFString(BunString::ZeroCopy), index); + if (groupPtr == nullptr) { return -1; } + auto& filters = *groupPtr; const auto& callbacks = index == std::numeric_limits::max() ? this->fileCallbacks : this->namespaceCallbacks[index]; - ASSERT_WITH_MESSAGE(callbacks.size() == group->size(), "Number of callbacks and filters must match"); + ASSERT_WITH_MESSAGE(callbacks.size() == filters.size(), "Number of callbacks and filters must match"); if (callbacks.isEmpty()) { return -1; } int count = 0; - constexpr bool usesPatternContextBuffer = false; const WTF::String& path = pathString->toWTFString(BunString::ZeroCopy); for (size_t i = 0, total = callbacks.size(); i < total && *shouldContinue; ++i) { - Yarr::MatchingContextHolder regExpContext(vm, usesPatternContextBuffer, nullptr, Yarr::MatchFrom::CompilerThread); - // Need to lock the mutex to access the regular expression - { - std::lock_guard lock(*group->at(i).second); - if (group->at(i).first.match(path) > -1) { - Bun::NapiExternal* external = callbacks[i].external; - if (external) { - ((OnBeforeParseArguments*)(onBeforeParseArgs))->external = external->value(); - } + if (i > 0) { + OnBeforeParseResult__reset(onBeforeParseResult); + } - JSBundlerPluginNativeOnBeforeParseCallback callback = callbacks[i].callback; - const char* name = callbacks[i].name ? callbacks[i].name : ""; - CrashHandler__setInsideNativePlugin(name); - callback(onBeforeParseArgs, onBeforeParseResult); - CrashHandler__setInsideNativePlugin(nullptr); - - count++; + if (filters[i].match(vm, path)) { + Bun::NapiExternal* external = callbacks[i].external; + if (external) { + onBeforeParseArgs->external = external->value(); + } else { + onBeforeParseArgs->external = nullptr; } + + JSBundlerPluginNativeOnBeforeParseCallback callback = callbacks[i].callback; + const char* name = callbacks[i].name ? callbacks[i].name : ""; + CrashHandler__setInsideNativePlugin(name); + callback(onBeforeParseArgs, onBeforeParseResult); + CrashHandler__setInsideNativePlugin(nullptr); + + count++; } if (OnBeforeParsePlugin__isDone(bunContextPtr)) { @@ -373,7 +375,7 @@ JSC_DEFINE_HOST_FUNCTION(jsBundlerPluginFunction_onBeforeParse, (JSC::JSGlobalOb #endif if (!on_before_parse_symbol_ptr) { - Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "Expected on_before_parse_symbol (3rd argument) to be a valid symbol"_s); + Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, makeString("Could not find the symbol \""_s, on_before_parse_symbol, "\" in the given napi module."_s)); return {}; } @@ -648,7 +650,7 @@ extern "C" int JSBundlerPlugin__callOnBeforeParsePlugins( const BunString* namespaceStr, const BunString* pathString, OnBeforeParseArguments* onBeforeParseArgs, - void* onBeforeParseResult, + OnBeforeParseResult* onBeforeParseResult, int* shouldContinue) { return plugin->plugin.onBeforeParse.call(plugin->vm(), &plugin->plugin, shouldContinue, bunContextPtr, namespaceStr, pathString, onBeforeParseArgs, onBeforeParseResult); diff --git a/src/bun.js/bindings/JSBundlerPlugin.h b/src/bun.js/bindings/JSBundlerPlugin.h index da28a8e485..7bef5769fa 100644 --- a/src/bun.js/bindings/JSBundlerPlugin.h +++ b/src/bun.js/bindings/JSBundlerPlugin.h @@ -1,5 +1,6 @@ #pragma once +#include "bun-native-bundler-plugin-api/bundler_plugin.h" #include "root.h" #include "headers-handwritten.h" #include @@ -10,7 +11,7 @@ typedef void (*JSBundlerPluginAddErrorCallback)(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue); typedef void (*JSBundlerPluginOnLoadAsyncCallback)(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue); typedef void (*JSBundlerPluginOnResolveAsyncCallback)(void*, void*, JSC::EncodedJSValue, JSC::EncodedJSValue, JSC::EncodedJSValue); -typedef void (*JSBundlerPluginNativeOnBeforeParseCallback)(void*, void*); +typedef void (*JSBundlerPluginNativeOnBeforeParseCallback)(const OnBeforeParseArguments*, OnBeforeParseResult*); namespace Bun { @@ -18,14 +19,38 @@ using namespace JSC; class BundlerPlugin final { public: + /// In native plugins, the regular expression could be called concurrently on multiple threads. + /// Therefore, we need a mutex to synchronize access. + class FilterRegExp { + public: + String m_pattern; + Yarr::RegularExpression regex; + WTF::Lock lock {}; + + FilterRegExp(FilterRegExp&& other) + : m_pattern(WTFMove(other.m_pattern)) + , regex(WTFMove(other.regex)) + { + } + + FilterRegExp(const String& pattern, OptionSet flags) + // Ensure it's safe for cross-thread usage. + : m_pattern(pattern.isolatedCopy()) + , regex(m_pattern, flags) + { + } + + bool match(JSC::VM& vm, const String& path); + }; + class NamespaceList { public: - Vector fileNamespace = {}; + Vector fileNamespace = {}; Vector namespaces = {}; - Vector> groups = {}; + Vector> groups = {}; BunPluginTarget target { BunPluginTargetBun }; - Vector* group(const String& namespaceStr, unsigned& index) + Vector* group(const String& namespaceStr, unsigned& index) { if (namespaceStr.isEmpty()) { index = std::numeric_limits::max(); @@ -46,10 +71,6 @@ public: void append(JSC::VM& vm, JSC::RegExp* filter, String& namespaceString, unsigned& index); }; - /// In native plugins, the regular expression could be called concurrently on multiple threads. - /// Therefore, we need a mutex to synchronize access. - typedef std::pair> NativeFilterRegexp; - struct NativePluginCallback { JSBundlerPluginNativeOnBeforeParseCallback callback; Bun::NapiExternal* external; @@ -65,18 +86,18 @@ public: public: using PerNamespaceCallbackList = Vector; - Vector fileNamespace = {}; + Vector fileNamespace = {}; Vector namespaces = {}; - Vector> groups = {}; + Vector> groups = {}; BunPluginTarget target { BunPluginTargetBun }; PerNamespaceCallbackList fileCallbacks = {}; Vector namespaceCallbacks = {}; - int call(JSC::VM& vm, BundlerPlugin* plugin, int* shouldContinue, void* bunContextPtr, const BunString* namespaceStr, const BunString* pathString, void* onBeforeParseArgs, void* onBeforeParseResult); + int call(JSC::VM& vm, BundlerPlugin* plugin, int* shouldContinue, void* bunContextPtr, const BunString* namespaceStr, const BunString* pathString, OnBeforeParseArguments* onBeforeParseArgs, OnBeforeParseResult* onBeforeParseResult); void append(JSC::VM& vm, JSC::RegExp* filter, String& namespaceString, JSBundlerPluginNativeOnBeforeParseCallback callback, const char* name, NapiExternal* external); - Vector* group(const String& namespaceStr, unsigned& index) + Vector* group(const String& namespaceStr, unsigned& index) { if (namespaceStr.isEmpty()) { index = std::numeric_limits::max(); diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 3059002802..66c4018634 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -4018,7 +4018,8 @@ pub const ParseTask = struct { const OnBeforeParseResultWrapper = struct { original_source: ?[]const u8 = null, loader: Loader, - impl: OnBeforeParseResult, + check: if (bun.Environment.isDebug) u32 else u0 = if (bun.Environment.isDebug) 42069 else 0, // Value to ensure OnBeforeParseResult is wrapped in this struct + result: OnBeforeParseResult, }; const OnBeforeParseResult = extern struct { @@ -4027,7 +4028,7 @@ pub const ParseTask = struct { source_len: usize = 0, loader: Loader, - fetch_source_code_fn: *const fn (*const OnBeforeParseArguments, *OnBeforeParseResult) callconv(.C) i32 = &fetchSourceCode, + fetch_source_code_fn: *const fn (*OnBeforeParseArguments, *OnBeforeParseResult) callconv(.C) i32 = &fetchSourceCode, user_context: ?*anyopaque = null, free_user_context: ?*const fn (?*anyopaque) callconv(.C) void = null, @@ -4036,9 +4037,15 @@ pub const ParseTask = struct { args_: ?*OnBeforeParseArguments, log_options_: ?*BunLogOptions, ) callconv(.C) void = &BunLogOptions.logFn, + + pub fn getWrapper(result: *OnBeforeParseResult) *OnBeforeParseResultWrapper { + const wrapper: *OnBeforeParseResultWrapper = @fieldParentPtr("result", result); + bun.debugAssert(wrapper.check == 42069); + return wrapper; + } }; - pub fn fetchSourceCode(args: *const OnBeforeParseArguments, result: *OnBeforeParseResult) callconv(.C) i32 { + pub fn fetchSourceCode(args: *OnBeforeParseArguments, result: *OnBeforeParseResult) callconv(.C) i32 { debug("fetchSourceCode", .{}); const this = args.context; if (this.log.errors > 0 or this.deferred_error != null or this.should_continue_running.* != 1) { @@ -4069,17 +4076,35 @@ pub const ParseTask = struct { result.source_len = entry.contents.len; result.free_user_context = null; result.user_context = null; + const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); + wrapper.original_source = entry.contents; return 0; } + pub export fn OnBeforeParseResult__reset(this: *OnBeforeParseResult) void { + const wrapper = this.getWrapper(); + this.loader = wrapper.loader; + if (wrapper.original_source) |src| { + this.source_ptr = src.ptr; + this.source_len = src.len; + } else { + this.source_ptr = null; + this.source_len = 0; + } + } + pub export fn OnBeforeParsePlugin__isDone(this: *OnBeforeParsePlugin) i32 { if (this.should_continue_running.* != 1) { return 1; } const result = this.result orelse return 1; + // The first plugin to set the source wins. + // But, we must check that they actually modified it + // since fetching the source stores it inside `result.source_ptr` if (result.source_ptr != null) { - return 1; + const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); + return @intFromBool(result.source_ptr.? != wrapper.original_source.?.ptr); } return 0; @@ -4096,10 +4121,14 @@ pub const ParseTask = struct { args.namespace_ptr = this.file_path.namespace.ptr; args.namespace_len = this.file_path.namespace.len; } - var result = OnBeforeParseResult{ + var wrapper = OnBeforeParseResultWrapper{ .loader = this.loader.*, + .result = OnBeforeParseResult{ + .loader = this.loader.*, + }, }; - this.result = &result; + + this.result = &wrapper.result; const count = plugin.callOnBeforeParsePlugins( this, if (bun.strings.eqlComptime(this.file_path.namespace, "file")) @@ -4109,15 +4138,15 @@ pub const ParseTask = struct { &bun.String.init(this.file_path.text), &args, - &result, + &wrapper.result, this.should_continue_running, ); if (comptime Environment.enable_logs) debug("callOnBeforeParsePlugins({s}:{s}) = {d}", .{ this.file_path.namespace, this.file_path.text, count }); if (count > 0) { if (this.deferred_error) |err| { - if (result.free_user_context) |free_user_context| { - free_user_context(result.user_context); + if (wrapper.result.free_user_context) |free_user_context| { + free_user_context(wrapper.result.user_context); } return err; @@ -4125,7 +4154,7 @@ pub const ParseTask = struct { // If the plugin sets the `free_user_context` function pointer, it _must_ set the `user_context` pointer. // Otherwise this is just invalid behavior. - if (result.user_context == null and result.free_user_context != null) { + if (wrapper.result.user_context == null and wrapper.result.free_user_context != null) { var msg = Logger.Msg{ .data = .{ .location = null, .text = bun.default_allocator.dupe( u8, "Native plugin set the `free_plugin_source_code_context` field without setting the `plugin_source_code_context` field.", @@ -4137,27 +4166,27 @@ pub const ParseTask = struct { } if (this.log.errors > 0) { - if (result.free_user_context) |free_user_context| { - free_user_context(result.user_context); + if (wrapper.result.free_user_context) |free_user_context| { + free_user_context(wrapper.result.user_context); } return error.SyntaxError; } - if (result.source_ptr) |ptr| { - if (result.free_user_context != null) { + if (wrapper.result.source_ptr) |ptr| { + if (wrapper.result.free_user_context != null) { this.task.external = CacheEntry.External{ - .ctx = result.user_context, - .function = result.free_user_context, + .ctx = wrapper.result.user_context, + .function = wrapper.result.free_user_context, }; } from_plugin.* = true; - this.loader.* = result.loader; + this.loader.* = wrapper.result.loader; return CacheEntry{ - .contents = ptr[0..result.source_len], + .contents = ptr[0..wrapper.result.source_len], .external = .{ - .ctx = result.user_context, - .function = result.free_user_context, + .ctx = wrapper.result.user_context, + .function = wrapper.result.free_user_context, }, }; } diff --git a/src/js/builtins/BundlerPlugin.ts b/src/js/builtins/BundlerPlugin.ts index ed2c5e653b..a4ded7976c 100644 --- a/src/js/builtins/BundlerPlugin.ts +++ b/src/js/builtins/BundlerPlugin.ts @@ -1,11 +1,4 @@ -import type { - BuildConfig, - BunPlugin, - OnLoadCallback, - OnResolveCallback, - PluginBuilder, - PluginConstraints, -} from "bun"; +import type { BuildConfig, BunPlugin, OnLoadCallback, OnResolveCallback, PluginBuilder, PluginConstraints } from "bun"; type AnyFunction = (...args: any[]) => any; interface BundlerPlugin { @@ -73,8 +66,10 @@ export function runSetupFunction( if (map === onBeforeParsePlugins) { isOnBeforeParse = true; // TODO: how to check if it a napi module here? - if (!callback) { - throw new TypeError("onBeforeParse `napiModule` must be a Napi module"); + if (!callback || !$isObject(callback) || !callback.$napiDlopenHandle) { + throw new TypeError( + "onBeforeParse `napiModule` must be a Napi module which exports the `BUN_PLUGIN_NAME` symbol.", + ); } if (typeof symbol !== "string") { @@ -134,7 +129,7 @@ export function runSetupFunction( const self = this; function onStart(callback) { - if(isBake) { + if (isBake) { throw new TypeError("onStart() is not supported in Bake yet"); } if (!$isCallable(callback)) { @@ -370,7 +365,14 @@ export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespa } } -export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespace, defaultLoaderId, isServerSide: boolean) { +export function runOnLoadPlugins( + this: BundlerPlugin, + internalID, + path, + namespace, + defaultLoaderId, + isServerSide: boolean, +) { const LOADERS_MAP = $LoaderLabelToId; const loaderName = $LoaderIdToLabel[defaultLoaderId]; @@ -411,15 +413,15 @@ export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespac } var { contents, loader = defaultLoader } = result as any; - if ((loader as any) === 'object') { - if (!('exports' in result)) { + if ((loader as any) === "object") { + if (!("exports" in result)) { throw new TypeError('onLoad plugin returning loader: "object" must have "exports" property'); } try { contents = JSON.stringify(result.exports); - loader = 'json'; + loader = "json"; } catch (e) { - throw new TypeError('When using Bun.build, onLoad plugin must return a JSON-serializable object: ' + e) ; + throw new TypeError("When using Bun.build, onLoad plugin must return a JSON-serializable object: " + e); } } diff --git a/test/bundler/native-plugin.test.ts b/test/bundler/native-plugin.test.ts index 83ef6acaaf..10c67d8ce5 100644 --- a/test/bundler/native-plugin.test.ts +++ b/test/bundler/native-plugin.test.ts @@ -2,9 +2,12 @@ import { BunFile, Loader, plugin } from "bun"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; import path, { dirname, join, resolve } from "path"; import source from "./native_plugin.cc" with { type: "file" }; +import notAPlugin from "./not_native_plugin.cc" with { type: "file" }; import bundlerPluginHeader from "../../packages/bun-native-bundler-plugin-api/bundler_plugin.h" with { type: "file" }; -import { bunEnv, bunExe, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, makeTree, tempDirWithFiles } from "harness"; import { itBundled } from "bundler/expectBundled"; +import os from "os"; +import fs from "fs"; describe("native-plugins", async () => { const cwd = process.cwd(); @@ -15,6 +18,7 @@ describe("native-plugins", async () => { const files = { "bun-native-bundler-plugin-api/bundler_plugin.h": await Bun.file(bundlerPluginHeader).text(), "plugin.cc": await Bun.file(source).text(), + "not_a_plugin.cc": await Bun.file(notAPlugin).text(), "package.json": JSON.stringify({ "name": "fake-plugin", "module": "index.ts", @@ -48,12 +52,19 @@ values;`, "target_name": "xXx123_foo_counter_321xXx", "sources": [ "plugin.cc" ], "include_dirs": [ "." ] + }, + { + "target_name": "not_a_plugin", + "sources": [ "not_a_plugin.cc" ], + "include_dirs": [ "." ] } ] }`, }; tempdir = tempDirWithFiles("native-plugins", files); + + await makeTree(tempdir, files); outdir = path.join(tempdir, "dist"); console.log("tempdir", tempdir); @@ -491,6 +502,54 @@ const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] expect(compilationCtxFreedCount).toBe(0); }); + it("should fail gracefully when passing something that is NOT a bunler plugin", async () => { + const not_plugins = [require(path.join(tempdir, "build/Release/not_a_plugin.node")), 420, "hi", {}]; + + for (const napiModule of not_plugins) { + try { + await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "not_a_plugin", + setup(build) { + build.onBeforeParse({ filter: /\.ts/ }, { napiModule, symbol: "plugin_impl" }); + }, + }, + ], + }); + expect.unreachable(); + } catch (e) { + expect(e.toString()).toContain( + "onBeforeParse `napiModule` must be a Napi module which exports the `BUN_PLUGIN_NAME` symbol.", + ); + } + } + }); + + it("should fail gracefully when can't find the symbol", async () => { + const napiModule = require(path.join(tempdir, "build/Release/xXx123_foo_counter_321xXx.node")); + + try { + await Bun.build({ + outdir, + entrypoints: [path.join(tempdir, "index.ts")], + plugins: [ + { + name: "not_a_plugin", + setup(build) { + build.onBeforeParse({ filter: /\.ts/ }, { napiModule, symbol: "OOGA_BOOGA_420" }); + }, + }, + ], + }); + expect.unreachable(); + } catch (e) { + expect(e.toString()).toContain('TypeError: Could not find the symbol "OOGA_BOOGA_420" in the given napi module.'); + } + }); + it("should use result of the first plugin that runs and doesn't execute the others", async () => { const filter = /\.ts/; diff --git a/test/bundler/native_plugin.cc b/test/bundler/native_plugin.cc index b48eec7dac..51b13fd07d 100644 --- a/test/bundler/native_plugin.cc +++ b/test/bundler/native_plugin.cc @@ -19,7 +19,7 @@ #include #endif -BUN_PLUGIN_EXPORT const char *BUN_PLUGIN_NAME = "native_plugin_test"; +extern "C" BUN_PLUGIN_EXPORT const char *BUN_PLUGIN_NAME = "native_plugin_test"; struct External { std::atomic foo_count; diff --git a/test/bundler/not_native_plugin.cc b/test/bundler/not_native_plugin.cc new file mode 100644 index 0000000000..1de24320d9 --- /dev/null +++ b/test/bundler/not_native_plugin.cc @@ -0,0 +1,27 @@ +/* + */ +#include +#include +#include +#include + +#ifdef _WIN32 +#define BUN_PLUGIN_EXPORT __declspec(dllexport) +#else +#define BUN_PLUGIN_EXPORT +#endif + +napi_value HelloWorld(napi_env env, napi_callback_info info) { + napi_value result; + napi_create_string_utf8(env, "hello world", NAPI_AUTO_LENGTH, &result); + return result; +} + +napi_value Init(napi_env env, napi_value exports) { + napi_value fn; + napi_create_function(env, nullptr, 0, HelloWorld, nullptr, &fn); + napi_set_named_property(env, exports, "helloWorld", fn); + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/harness.ts b/test/harness.ts index 0921b1dcc0..eb55b7b682 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -152,25 +152,29 @@ export type DirectoryTree = { | ((opts: { root: string }) => Awaitable); }; -export function tempDirWithFiles(basename: string, files: DirectoryTree): string { - async function makeTree(base: string, tree: DirectoryTree) { - for (const [name, raw_contents] of Object.entries(tree)) { - const contents = typeof raw_contents === "function" ? await raw_contents({ root: base }) : raw_contents; - const joined = join(base, name); - if (name.includes("/")) { - const dir = dirname(name); - if (dir !== name && dir !== ".") { - fs.mkdirSync(join(base, dir), { recursive: true }); - } +export async function makeTree(base: string, tree: DirectoryTree) { + const isDirectoryTree = (value: string | DirectoryTree | Buffer): value is DirectoryTree => + typeof value === "object" && value && typeof value?.byteLength === "undefined"; + + for (const [name, raw_contents] of Object.entries(tree)) { + const contents = typeof raw_contents === "function" ? await raw_contents({ root: base }) : raw_contents; + const joined = join(base, name); + if (name.includes("/")) { + const dir = dirname(name); + if (dir !== name && dir !== ".") { + fs.mkdirSync(join(base, dir), { recursive: true }); } - if (typeof contents === "object" && contents && typeof contents?.byteLength === "undefined") { - fs.mkdirSync(joined); - makeTree(joined, contents); - continue; - } - fs.writeFileSync(joined, contents); } + if (isDirectoryTree(contents)) { + fs.mkdirSync(joined); + makeTree(joined, contents); + continue; + } + fs.writeFileSync(joined, contents); } +} + +export function tempDirWithFiles(basename: string, files: DirectoryTree): string { const base = fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()), basename + "_")); makeTree(base, files); return base;