Native plugin follow up (#15632)

Co-authored-by: zackradisic <zackradisic@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
Zack Radisic
2024-12-11 17:51:21 -08:00
committed by GitHub
parent 2e0f229722
commit 113b62be82
16 changed files with 630 additions and 278 deletions

View File

@@ -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(())
}

View File

@@ -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"

View File

@@ -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 = []

View File

@@ -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.

View File

@@ -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"

View File

@@ -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()
}

View File

@@ -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<Cow<'a, str>> {
fn get_from_raw_str<'a>(ptr: *const u8, len: usize) -> PluginResult<Cow<'a, str>> {
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<T> = std::result::Result<T, Error>;
pub type PluginResult<T> = std::result::Result<T, Error>;
pub type Result<T> = anyhow::Result<T>;
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<Utf8Error> for Error {
fn from(value: Utf8Error) -> Self {
@@ -361,6 +386,12 @@ impl From<Utf8Error> for Error {
}
}
impl<Guard> From<PoisonError<Guard>> for Error {
fn from(_: PoisonError<Guard>) -> Self {
Self::LockPoisoned
}
}
/// A safe handle for the arguments + result struct for the
/// `OnBeforeParse` bundler lifecycle hook.
///
@@ -370,9 +401,10 @@ impl From<Utf8Error> 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<Self> {
if args.__struct_size < std::mem::size_of::<sys::OnBeforeParseArguments>()
) -> PluginResult<Self> {
if unsafe { (*args).__struct_size } < std::mem::size_of::<sys::OnBeforeParseArguments>()
|| unsafe { (*result).__struct_size } < std::mem::size_of::<sys::OnBeforeParseResult>()
{
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::<sys::BunLogOptions>(),
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<Cow<'_, str>> {
get_from_raw_str(self.args_raw.path_ptr, self.args_raw.path_len)
pub fn path(&self) -> PluginResult<Cow<'_, str>> {
unsafe { get_from_raw_str((*self.args_raw).path_ptr, (*self.args_raw).path_len) }
}
pub fn namespace(&self) -> Result<Cow<'_, str>> {
get_from_raw_str(self.args_raw.namespace_ptr, self.args_raw.namespace_len)
pub fn namespace(&self) -> PluginResult<Cow<'_, str>> {
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<T: 'static + Sync>(&self) -> Result<Option<&'static T>> {
if self.args_raw.external.is_null() {
pub unsafe fn external<T: 'static + Sync>(&self) -> PluginResult<Option<&'static T>> {
if unsafe { (*self.args_raw).external.is_null() } {
return Ok(None);
}
let external: *mut TaggedObject<T> = self.args_raw.external as *mut TaggedObject<T>;
let external: *mut TaggedObject<T> =
unsafe { (*self.args_raw).external as *mut TaggedObject<T> };
unsafe {
if (*external).type_id != TypeId::of::<T>() {
@@ -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<T: 'static + Sync>(&mut self) -> Result<Option<&mut T>> {
if self.args_raw.external.is_null() {
pub unsafe fn external_mut<T: 'static + Sync>(&mut self) -> PluginResult<Option<&mut T>> {
if unsafe { (*self.args_raw).external.is_null() } {
return Ok(None);
}
let external: *mut TaggedObject<T> = self.args_raw.external as *mut TaggedObject<T>;
let external: *mut TaggedObject<T> =
unsafe { (*self.args_raw).external as *mut TaggedObject<T> };
unsafe {
if (*external).type_id != TypeId::of::<T>() {
@@ -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<Cow<'_, str>> {
pub fn input_source_code(&self) -> PluginResult<Cow<'_, str>> {
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::<sys::BunLogOptions>(),
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::<sys::BunLogOptions>(),
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,
}
}

View File

@@ -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, {});

View File

@@ -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<double>(reinterpret_cast<uintptr_t>(argName)))
#define UNWRAP_BUNDLER_PLUGIN(callFrame) reinterpret_cast<void*>(bitwise_cast<uintptr_t>(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<Yarr::RegularExpression> {});
groups.append(Vector<FilterRegExp> {});
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<NativeFilterRegexp> {});
groups.append(Vector<FilterRegExp> {});
nsGroup = &groups.last();
index = namespaces.size() - 1;
}
Yarr::RegularExpression regex(
StringView(filter->pattern()),
filter->flags());
NativeFilterRegexp nativeFilterRegexp = std::make_pair(regex, std::make_shared<std::mutex>());
nsGroup->append(nativeFilterRegexp);
auto pattern = filter->pattern();
auto filter_regexp = FilterRegExp(pattern, filter->flags());
nsGroup->append(WTFMove(filter_regexp));
}
if (index == std::numeric_limits<unsigned>::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<unsigned>::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<std::mutex> 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 : "<unknown>";
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 : "<unknown>";
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);

View File

@@ -1,5 +1,6 @@
#pragma once
#include "bun-native-bundler-plugin-api/bundler_plugin.h"
#include "root.h"
#include "headers-handwritten.h"
#include <JavaScriptCore/JSGlobalObject.h>
@@ -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<Yarr::Flags> 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<Yarr::RegularExpression> fileNamespace = {};
Vector<FilterRegExp> fileNamespace = {};
Vector<String> namespaces = {};
Vector<Vector<Yarr::RegularExpression>> groups = {};
Vector<Vector<FilterRegExp>> groups = {};
BunPluginTarget target { BunPluginTargetBun };
Vector<Yarr::RegularExpression>* group(const String& namespaceStr, unsigned& index)
Vector<FilterRegExp>* group(const String& namespaceStr, unsigned& index)
{
if (namespaceStr.isEmpty()) {
index = std::numeric_limits<unsigned>::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<Yarr::RegularExpression, std::shared_ptr<std::mutex>> NativeFilterRegexp;
struct NativePluginCallback {
JSBundlerPluginNativeOnBeforeParseCallback callback;
Bun::NapiExternal* external;
@@ -65,18 +86,18 @@ public:
public:
using PerNamespaceCallbackList = Vector<NativePluginCallback>;
Vector<NativeFilterRegexp> fileNamespace = {};
Vector<FilterRegExp> fileNamespace = {};
Vector<String> namespaces = {};
Vector<Vector<NativeFilterRegexp>> groups = {};
Vector<Vector<FilterRegExp>> groups = {};
BunPluginTarget target { BunPluginTargetBun };
PerNamespaceCallbackList fileCallbacks = {};
Vector<PerNamespaceCallbackList> 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<NativeFilterRegexp>* group(const String& namespaceStr, unsigned& index)
Vector<FilterRegExp>* group(const String& namespaceStr, unsigned& index)
{
if (namespaceStr.isEmpty()) {
index = std::numeric_limits<unsigned>::max();

View File

@@ -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,
},
};
}

View File

@@ -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);
}
}

View File

@@ -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/;

View File

@@ -19,7 +19,7 @@
#include <unistd.h>
#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<size_t> foo_count;

View File

@@ -0,0 +1,27 @@
/*
*/
#include <bun-native-bundler-plugin-api/bundler_plugin.h>
#include <cstdlib>
#include <cstring>
#include <node_api.h>
#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)

View File

@@ -152,25 +152,29 @@ export type DirectoryTree = {
| ((opts: { root: string }) => Awaitable<string | Buffer | DirectoryTree>);
};
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;