diff --git a/Cargo.lock b/Cargo.lock index 47e3d6b62f11..113b663e6459 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2886,6 +2886,7 @@ dependencies = [ "once_cell", "serde", "serde_json", + "tokio", ] [[package]] @@ -6012,6 +6013,27 @@ dependencies = [ "swc_visit", ] +[[package]] +name = "swc_interop_babel" +version = "0.1.0" +dependencies = [ + "napi", + "serde", + "serde_json", + "swc_interop_nodejs", +] + +[[package]] +name = "swc_interop_nodejs" +version = "0.1.0" +dependencies = [ + "napi", + "serde", + "serde_json", + "tokio", + "tracing", +] + [[package]] name = "swc_macros_common" version = "1.0.0" diff --git a/crates/swc_interop_babel/Cargo.toml b/crates/swc_interop_babel/Cargo.toml new file mode 100644 index 000000000000..3cbc2285d4b2 --- /dev/null +++ b/crates/swc_interop_babel/Cargo.toml @@ -0,0 +1,17 @@ +[package] +authors = ["강동윤 "] +description = "General interop for Babel" +documentation = "https://rustdoc.swc.rs/swc_interop_babel/" +edition = { workspace = true } +license = { workspace = true } +name = "swc_interop_babel" +repository = { workspace = true } +version = "0.1.0" + + +[dependencies] +napi = { workspace = true, features = ["napi4"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } + +swc_interop_nodejs = { version = "0.1.0", path = "../swc_interop_nodejs" } diff --git a/crates/swc_interop_babel/src/lib.rs b/crates/swc_interop_babel/src/lib.rs new file mode 100644 index 000000000000..1bba7e1508ae --- /dev/null +++ b/crates/swc_interop_babel/src/lib.rs @@ -0,0 +1 @@ +pub mod transform; diff --git a/crates/swc_interop_babel/src/transform.rs b/crates/swc_interop_babel/src/transform.rs new file mode 100644 index 000000000000..d92b4c6d2219 --- /dev/null +++ b/crates/swc_interop_babel/src/transform.rs @@ -0,0 +1,26 @@ +use napi::JsFunction; +use serde::{Deserialize, Serialize}; +use swc_interop_nodejs::{js_hook::JsHook, types::AsJsonString}; + +pub struct JsTrasnform { + f: JsHook, AsJsonString>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TransformOutput { + pub code: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub map: Option, +} + +impl JsTrasnform { + pub fn new(env: &napi::Env, f: &JsFunction) -> napi::Result { + Ok(Self { + f: JsHook::new(env, f)?, + }) + } + + pub async fn transform(&self, input: TransformOutput) -> napi::Result { + Ok(self.f.call(AsJsonString(input)).await?.0) + } +} diff --git a/crates/swc_interop_nodejs/Cargo.toml b/crates/swc_interop_nodejs/Cargo.toml new file mode 100644 index 000000000000..40c2aeae628b --- /dev/null +++ b/crates/swc_interop_nodejs/Cargo.toml @@ -0,0 +1,17 @@ +[package] +authors = ["강동윤 "] +description = "General interop for Node.js" +documentation = "https://rustdoc.swc.rs/swc_interop_nodejs/" +edition = { workspace = true } +license = { workspace = true } +name = "swc_interop_nodejs" +repository = { workspace = true } +version = "0.1.0" + + +[dependencies] +napi = { workspace = true, features = ["napi4", "tokio_rt", "serde-json"] } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/swc_interop_nodejs/src/js_hook.rs b/crates/swc_interop_nodejs/src/js_hook.rs new file mode 100644 index 000000000000..9e33b731cc38 --- /dev/null +++ b/crates/swc_interop_nodejs/src/js_hook.rs @@ -0,0 +1,52 @@ +use std::marker::PhantomData; + +use napi::{ + bindgen_prelude::Promise, + threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction}, + JsFunction, +}; +use tracing::trace; + +use crate::types::{JsInput, JsOutput}; + +pub struct JsHook +where + I: JsInput, + O: JsOutput, +{ + f: ThreadsafeFunction, + _marker: PhantomData, +} + +impl JsHook +where + I: JsInput, + O: JsOutput, +{ + pub fn new(env: &napi::Env, f: &JsFunction) -> napi::Result { + Ok(Self { + f: env.create_threadsafe_function(f, 0, |cx: ThreadSafeCallContext| { + let arg = cx.value.into_js(&cx.env)?.into_unknown(); + + if cfg!(debug_assertions) { + trace!("Converted to js value"); + } + Ok(vec![arg]) + })?, + _marker: PhantomData, + }) + } + + #[tracing::instrument(skip_all, fields(perf = "JsHook::call"))] + pub async fn call(&self, input: I) -> napi::Result { + if cfg!(debug_assertions) { + trace!("Calling js function"); + } + + let result: Promise = self.f.call_async(Ok(input)).await?; + + let res = result.await?; + + Ok(res) + } +} diff --git a/crates/swc_interop_nodejs/src/lib.rs b/crates/swc_interop_nodejs/src/lib.rs new file mode 100644 index 000000000000..2a1ff769004e --- /dev/null +++ b/crates/swc_interop_nodejs/src/lib.rs @@ -0,0 +1,2 @@ +pub mod js_hook; +pub mod types; diff --git a/crates/swc_interop_nodejs/src/types.rs b/crates/swc_interop_nodejs/src/types.rs new file mode 100644 index 000000000000..445551938a71 --- /dev/null +++ b/crates/swc_interop_nodejs/src/types.rs @@ -0,0 +1,86 @@ +use std::fmt::Debug; + +use napi::{bindgen_prelude::FromNapiValue, Env, JsUnknown}; +use serde::{de::DeserializeOwned, Serialize}; + +pub trait JsInput: 'static + Send + Debug { + fn into_js(self, env: &Env) -> napi::Result; +} + +impl JsInput for String { + fn into_js(self, env: &Env) -> napi::Result { + Ok(env.create_string(&self)?.into_unknown()) + } +} + +impl JsInput for Vec { + fn into_js(self, env: &Env) -> napi::Result { + let mut arr = env.create_array_with_length(self.len())?; + + for (idx, s) in self.into_iter().enumerate() { + arr.set_element(idx as _, s.into_js(env)?)?; + } + + Ok(arr.into_unknown()) + } +} + +impl JsInput for Vec { + fn into_js(self, env: &Env) -> napi::Result { + Ok(env.create_buffer_with_data(self)?.into_unknown()) + } +} + +impl JsInput for (A, B) +where + A: JsInput, + B: JsInput, +{ + fn into_js(self, env: &Env) -> napi::Result { + let mut arr = env.create_array(2)?; + arr.set(0, self.0.into_js(env)?)?; + arr.set(1, self.1.into_js(env)?)?; + + Ok(arr.coerce_to_object()?.into_unknown()) + } +} + +/// Seems like Vec is buggy +pub trait JsOutput: 'static + FromNapiValue + Send + Debug {} + +impl JsOutput for String {} + +impl JsOutput for bool {} + +/// Note: This type stringifies the output json string, because it's faster. +#[derive(Debug, Default)] +pub struct AsJsonString(pub T) +where + T: 'static + Send + Debug; + +impl FromNapiValue for AsJsonString +where + T: 'static + Send + Debug + DeserializeOwned, +{ + unsafe fn from_napi_value( + env_raw: napi::sys::napi_env, + napi_val: napi::sys::napi_value, + ) -> napi::Result { + let env = Env::from_raw(env_raw); + let json: String = env.from_js_value(JsUnknown::from_napi_value(env_raw, napi_val)?)?; + let t = serde_json::from_str(&json)?; + Ok(Self(t)) + } +} + +impl JsOutput for AsJsonString where T: 'static + Send + Debug + DeserializeOwned {} + +impl JsInput for AsJsonString +where + T: 'static + Send + Debug + Serialize, +{ + fn into_js(self, env: &napi::Env) -> napi::Result { + let json = serde_json::to_string(&self.0)?; + Ok(env.create_string(json.as_str())?.into_unknown()) + } +}