Skip to content

Commit

Permalink
feat: Add (experimental) nodejs interop crates (#9974)
Browse files Browse the repository at this point in the history
**Description:**

I'm experimenting with various options.
  • Loading branch information
kdy1 authored Jan 29, 2025
1 parent c05c5a9 commit 37e0ea5
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 0 deletions.
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions crates/swc_interop_babel/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
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" }
1 change: 1 addition & 0 deletions crates/swc_interop_babel/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod transform;
26 changes: 26 additions & 0 deletions crates/swc_interop_babel/src/transform.rs
Original file line number Diff line number Diff line change
@@ -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<TransformOutput>, AsJsonString<TransformOutput>>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TransformOutput {
pub code: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub map: Option<String>,
}

impl JsTrasnform {
pub fn new(env: &napi::Env, f: &JsFunction) -> napi::Result<Self> {
Ok(Self {
f: JsHook::new(env, f)?,
})
}

pub async fn transform(&self, input: TransformOutput) -> napi::Result<TransformOutput> {
Ok(self.f.call(AsJsonString(input)).await?.0)
}
}
17 changes: 17 additions & 0 deletions crates/swc_interop_nodejs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
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 }
52 changes: 52 additions & 0 deletions crates/swc_interop_nodejs/src/js_hook.rs
Original file line number Diff line number Diff line change
@@ -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<I, O>
where
I: JsInput,
O: JsOutput,
{
f: ThreadsafeFunction<I>,
_marker: PhantomData<O>,
}

impl<I, O> JsHook<I, O>
where
I: JsInput,
O: JsOutput,
{
pub fn new(env: &napi::Env, f: &JsFunction) -> napi::Result<Self> {
Ok(Self {
f: env.create_threadsafe_function(f, 0, |cx: ThreadSafeCallContext<I>| {
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<O> {
if cfg!(debug_assertions) {
trace!("Calling js function");
}

let result: Promise<O> = self.f.call_async(Ok(input)).await?;

let res = result.await?;

Ok(res)
}
}
2 changes: 2 additions & 0 deletions crates/swc_interop_nodejs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod js_hook;
pub mod types;
86 changes: 86 additions & 0 deletions crates/swc_interop_nodejs/src/types.rs
Original file line number Diff line number Diff line change
@@ -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<JsUnknown>;
}

impl JsInput for String {
fn into_js(self, env: &Env) -> napi::Result<JsUnknown> {
Ok(env.create_string(&self)?.into_unknown())
}
}

impl JsInput for Vec<String> {
fn into_js(self, env: &Env) -> napi::Result<JsUnknown> {
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<u8> {
fn into_js(self, env: &Env) -> napi::Result<JsUnknown> {
Ok(env.create_buffer_with_data(self)?.into_unknown())
}
}

impl<A, B> JsInput for (A, B)
where
A: JsInput,
B: JsInput,
{
fn into_js(self, env: &Env) -> napi::Result<JsUnknown> {
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<u8> 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<T>(pub T)
where
T: 'static + Send + Debug;

impl<T> FromNapiValue for AsJsonString<T>
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<Self> {
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<T> JsOutput for AsJsonString<T> where T: 'static + Send + Debug + DeserializeOwned {}

impl<T> JsInput for AsJsonString<T>
where
T: 'static + Send + Debug + Serialize,
{
fn into_js(self, env: &napi::Env) -> napi::Result<napi::JsUnknown> {
let json = serde_json::to_string(&self.0)?;
Ok(env.create_string(json.as_str())?.into_unknown())
}
}

0 comments on commit 37e0ea5

Please sign in to comment.