Skip to content

Commit

Permalink
feat(rust-plugin): pass PluginContext to JS (#628)
Browse files Browse the repository at this point in the history
<!-- Thank you for contributing! -->

### Description

<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->

### Test Plan

<!-- e.g. is there anything you'd like reviewers to focus on? -->

---
  • Loading branch information
hyf0 authored Mar 21, 2024
1 parent e2df671 commit e4d33fd
Show file tree
Hide file tree
Showing 10 changed files with 80 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use rolldown_plugin::SharedPluginContext;

#[napi_derive::napi]
pub struct BindingPluginContext {
#[allow(dead_code)]
inner: SharedPluginContext,
}

impl From<SharedPluginContext> for BindingPluginContext {
fn from(inner: SharedPluginContext) -> Self {
Self { inner }
}
}
1 change: 1 addition & 0 deletions crates/rolldown_binding/src/options/plugin/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod binding_plugin_context;
mod plugin;
mod plugin_adapter;

Expand Down
13 changes: 7 additions & 6 deletions crates/rolldown_binding/src/options/plugin/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ use napi::{
use rolldown_error::BuildError;
use serde::Deserialize;

use crate::{
types::binding_outputs::BindingOutputs, types::binding_rendered_module::BindingRenderedModule,
use crate::types::{
binding_outputs::BindingOutputs, binding_rendered_module::BindingRenderedModule,
js_async_callback::JsAsyncCallback,
};

// Using `Either3<Promise<T>, T, UnknownReturnValue>` in callback functions to handle the
// unknown return value from JavaScript explicit and avoid unexpected panics.
use super::binding_plugin_context::BindingPluginContext;

#[napi_derive::napi(object, object_to_js = false)]
#[derive(Deserialize, Default, Derivative)]
#[serde(rename_all = "camelCase")]
Expand All @@ -23,8 +24,8 @@ pub struct PluginOptions {

#[derivative(Debug = "ignore")]
#[serde(skip_deserializing)]
#[napi(ts_type = "() => Promise<void>")]
pub build_start: Option<ThreadsafeFunction<(), Either<Promise<()>, UnknownReturnValue>, false>>,
#[napi(ts_type = "(ctx: BindingPluginContext) => Promise<void>")]
pub build_start: Option<JsAsyncCallback<BindingPluginContext, ()>>,

#[derivative(Debug = "ignore")]
#[serde(skip_deserializing)]
Expand Down
18 changes: 4 additions & 14 deletions crates/rolldown_binding/src/options/plugin/plugin_adapter.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{borrow::Cow, ops::Deref};
use std::{borrow::Cow, ops::Deref, sync::Arc};

use crate::utils::js_async_callback_ext::JsAsyncCallbackExt;
use futures::TryFutureExt;
use napi::bindgen_prelude::{Either, Either3, Error, Status};
use rolldown_plugin::Plugin;
Expand Down Expand Up @@ -31,23 +32,12 @@ impl Plugin for PluginAdapter {
Cow::Owned(self.name.clone())
}

#[allow(clippy::redundant_closure_for_method_calls)]
async fn build_start(
&self,
_ctx: &rolldown_plugin::SharedPluginContext,
ctx: &rolldown_plugin::SharedPluginContext,
) -> rolldown_plugin::HookNoopReturn {
if let Some(cb) = &self.build_start {
cb.call_async(())
.and_then(|start| async {
match start {
Either::A(p) => {
let result = p.await?;
Ok(result)
}
Either::B(_) => Ok(()),
}
})
.await?;
cb.call_async_normalized(Arc::clone(ctx).into()).await?;
}
Ok(())
}
Expand Down
11 changes: 11 additions & 0 deletions crates/rolldown_binding/src/types/js_async_callback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use napi::{
bindgen_prelude::{Either3, Promise},
threadsafe_function::{ThreadsafeFunction, UnknownReturnValue},
};

/// This represents a JavaScript function, whose return value is `Promise<T> | T`
pub type JsAsyncCallback<Args, Ret> = ThreadsafeFunction<Args, JsAsyncCallbackReturn<Ret>, false>;

// Explicitly using `UnknownReturnValue` in `Either3<Promise<T>, T, UnknownReturnValue>` to get control in rust while
// receiving unknown return value from JS side. This avoids unexpected inner panics.
pub type JsAsyncCallbackReturn<Ret> = Either3<Promise<Ret>, Ret, UnknownReturnValue>;
1 change: 1 addition & 0 deletions crates/rolldown_binding/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod binding_output_asset;
pub mod binding_output_chunk;
pub mod binding_outputs;
pub mod binding_rendered_module;
pub mod js_async_callback;
38 changes: 38 additions & 0 deletions crates/rolldown_binding/src/utils/js_async_callback_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::any::type_name;

use futures::Future;
use napi::bindgen_prelude::{Either3, FromNapiValue};

use crate::types::js_async_callback::{JsAsyncCallback, JsAsyncCallbackReturn};

pub trait JsAsyncCallbackExt<Args, Ret>: Send {
fn call_async_normalized(
&self,
args: Args,
) -> impl Future<Output = Result<Ret, napi::Error>> + Send;
}

impl<Args: Send, Ret: FromNapiValue + Send> JsAsyncCallbackExt<Args, Ret>
for JsAsyncCallback<Args, Ret>
where
JsAsyncCallbackReturn<Ret>: Send + FromNapiValue,
{
/// Call the hook and normalize the returned `Either3<Promise<Ret>, Ret, UnknownReturnValue>` to `Result<Ret, napi::Error>`.
#[allow(clippy::manual_async_fn)]
fn call_async_normalized(
&self,
args: Args,
) -> impl Future<Output = Result<Ret, napi::Error>> + Send {
async move {
let ret = self.call_async(args).await?;
match ret {
Either3::A(p) => p.await,
Either3::B(v) => Ok(v),
Either3::C(_) => Err(napi::Error::new(
napi::Status::InvalidArg,
format!("Unknown return value. Cannot convert to `{}`.", type_name::<Ret>()),
)),
}
}
}
}
1 change: 1 addition & 0 deletions crates/rolldown_binding/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use napi::Env;
use rolldown_tracing::try_init_tracing_with_chrome_layer;
pub mod js_async_callback_ext;
pub mod normalize_binding_options;

pub fn try_init_custom_trace_subscriber(mut napi_env: Env) {
Expand Down
4 changes: 3 additions & 1 deletion packages/rolldown/src/binding.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */

export class BindingPluginContext {}

export class Bundler {
constructor(
inputOptions: BindingInputOptions,
Expand Down Expand Up @@ -90,7 +92,7 @@ export interface HookResolveIdArgsOptions {

export interface PluginOptions {
name: string
buildStart?: () => Promise<void>
buildStart?: (ctx: BindingPluginContext) => Promise<void>
resolveId?: (
specifier: string,
importer?: string,
Expand Down
1 change: 1 addition & 0 deletions packages/rolldown/src/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,5 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}

module.exports.BindingPluginContext = nativeBinding.BindingPluginContext
module.exports.Bundler = nativeBinding.Bundler

0 comments on commit e4d33fd

Please sign in to comment.