Skip to content

Commit

Permalink
Add support for JS macros
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Oct 8, 2023
1 parent ce58970 commit cd392af
Show file tree
Hide file tree
Showing 7 changed files with 1,151 additions and 40 deletions.
45 changes: 23 additions & 22 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion crates/node-bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ dashmap = "5.4.0"
xxhash-rust = { version = "0.8.2", features = ["xxh3"] }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
napi = {version = "2.12.6", features = ["serde-json", "napi4"]}
napi = {version = "2.12.6", features = ["serde-json", "napi4", "napi5"]}
parcel-dev-dep-resolver = { path = "../../packages/utils/dev-dep-resolver" }
oxipng = "8.0.0"
mozjpeg-sys = "1.0.0"
libc = "0.2"
rayon = "1.7.0"
crossbeam-channel = "0.5.6"

[target.'cfg(target_arch = "wasm32")'.dependencies]
napi = {version = "2.12.6", features = ["serde-json"]}
Expand Down
222 changes: 208 additions & 14 deletions crates/node-bindings/src/transformer.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,221 @@
use napi::{Env, JsObject, JsUnknown, Result};
use napi::{Env, JsObject, JsUnknown};
use napi_derive::napi;

#[napi]
pub fn transform(opts: JsObject, env: Env) -> Result<JsUnknown> {
pub fn transform(opts: JsObject, env: Env) -> napi::Result<JsUnknown> {
let config: parcel_js_swc_core::Config = env.from_js_value(opts)?;

let result = parcel_js_swc_core::transform(config)?;
let result = parcel_js_swc_core::transform(config, None)?;
env.to_js_value(&result)
}

#[cfg(not(target_arch = "wasm32"))]
#[napi]
pub fn transform_async(opts: JsObject, env: Env) -> Result<JsObject> {
let config: parcel_js_swc_core::Config = env.from_js_value(opts)?;
let (deferred, promise) = env.create_deferred()?;
mod native_only {
use super::*;
use crossbeam_channel::{Receiver, Sender};
use napi::{
threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunctionCallMode},
JsBoolean, JsFunction, JsNumber, JsString, ValueType,
};
use parcel_js_swc_core::JsValue;
use std::sync::Arc;

// Allocate a single channel per thread to communicate with the JS thread.
thread_local! {
static CHANNEL: (Sender<Result<JsValue, String>>, Receiver<Result<JsValue, String>>) = crossbeam_channel::unbounded();
}

struct CallMacroMessage {
src: String,
export: String,
args: Vec<JsValue>,
}

#[napi]
pub fn transform_async(opts: JsObject, env: Env) -> napi::Result<JsObject> {
let call_macro_tsfn = if opts.has_named_property("callMacro")? {
let func = opts.get_named_property::<JsUnknown>("callMacro")?;
if let Ok(func) = func.try_into() {
Some(env.create_threadsafe_function(
&func,
0,
|ctx: ThreadSafeCallContext<CallMacroMessage>| {
let src = ctx.env.create_string(&ctx.value.src)?.into_unknown();
let export = ctx.env.create_string(&ctx.value.export)?.into_unknown();
let args = js_value_to_napi(JsValue::Array(ctx.value.args), ctx.env)?;
Ok(vec![src, export, args])
},
)?)
} else {
None
}
} else {
None
};

let config: parcel_js_swc_core::Config = env.from_js_value(opts)?;
let (deferred, promise) = env.create_deferred()?;

// Get around Env not being Send. See safety note below.
let unsafe_env = env.raw() as usize;

rayon::spawn(move || {
let res = parcel_js_swc_core::transform(
config,
if let Some(tsfn) = call_macro_tsfn {
Some(Arc::new(move |src, export, args| {
CHANNEL.with(|channel| {
// Call JS function to run the macro.
let tx = channel.0.clone();
tsfn.call_with_return_value(
Ok(CallMacroMessage { src, export, args }),
ThreadsafeFunctionCallMode::Blocking,
move |v: JsUnknown| {
// When the JS function returns, await the promise, and send the result
// through the channel back to the native thread.
// SAFETY: this function is called from the JS thread.
await_promise(unsafe { Env::from_raw(unsafe_env as _) }, v, tx)?;
Ok(())
},
);
// Lock the transformer thread until the JS thread returns a result.
channel.1.recv().expect("receive failure")
})
}))
} else {
None
},
);
match res {
Ok(result) => deferred.resolve(move |env| env.to_js_value(&result)),
Err(err) => deferred.reject(err.into()),
}
});

Ok(promise)
}

/// Convert a JsValue macro argument from the transformer to a napi value.
fn js_value_to_napi(value: JsValue, env: Env) -> napi::Result<napi::JsUnknown> {
match value {
JsValue::Undefined => Ok(env.get_undefined()?.into_unknown()),
JsValue::Null => Ok(env.get_null()?.into_unknown()),
JsValue::Bool(b) => Ok(env.get_boolean(b)?.into_unknown()),
JsValue::Number(n) => Ok(env.create_double(n)?.into_unknown()),
JsValue::String(s) => Ok(env.create_string_from_std(s)?.into_unknown()),
JsValue::Regex { source, flags } => {
let regexp_class: JsFunction = env.get_global()?.get_named_property("RegExp")?;
let source = env.create_string_from_std(source)?;
let flags = env.create_string_from_std(flags)?;
let re = regexp_class.new_instance(&[source, flags])?;
Ok(re.into_unknown())
}
JsValue::Array(arr) => {
let mut res = env.create_array(arr.len() as u32)?;
for (i, val) in arr.into_iter().enumerate() {
res.set(i as u32, js_value_to_napi(val, env)?)?;
}
Ok(res.coerce_to_object()?.into_unknown())
}
JsValue::Object(obj) => {
let mut res = env.create_object()?;
for (k, v) in obj {
res.set_named_property(&k, js_value_to_napi(v, env)?)?;
}
Ok(res.into_unknown())
}
}
}

/// Convert a napi value returned as a result of a macro to a JsValue for the transformer.
fn napi_to_js_value(value: napi::JsUnknown, env: Env) -> napi::Result<JsValue> {
match value.get_type()? {
ValueType::Undefined => Ok(JsValue::Undefined),
ValueType::Null => Ok(JsValue::Null),
ValueType::Number => Ok(JsValue::Number(
unsafe { value.cast::<JsNumber>() }.get_double()?,
)),
ValueType::Boolean => Ok(JsValue::Bool(
unsafe { value.cast::<JsBoolean>() }.get_value()?,
)),
ValueType::String => Ok(JsValue::String(
unsafe { value.cast::<JsString>() }
.into_utf8()?
.into_owned()?,
)),
ValueType::Object => {
let obj = unsafe { value.cast::<JsObject>() };
if obj.is_array()? {
let len = obj.get_array_length()?;
let mut arr = Vec::with_capacity(len as usize);
for i in 0..len {
let elem = napi_to_js_value(obj.get_element(i)?, env)?;
arr.push(elem);
}
Ok(JsValue::Array(arr))
} else {
let regexp_class: JsFunction = env.get_global()?.get_named_property("RegExp")?;
if obj.instanceof(regexp_class)? {
let source: JsString = obj.get_named_property("source")?;
let flags: JsString = obj.get_named_property("flags")?;
return Ok(JsValue::Regex {
source: source.into_utf8()?.into_owned()?,
flags: flags.into_utf8()?.into_owned()?,
});
}

let names = obj.get_property_names()?;
let len = names.get_array_length()?;
let mut props = Vec::with_capacity(len as usize);
for i in 0..len {
let prop = names.get_element::<JsString>(i)?;
let name = prop.into_utf8()?.into_owned()?;
let value = napi_to_js_value(obj.get_property(prop)?, env)?;
props.push((name, value));
}
Ok(JsValue::Object(props))
}
}
ValueType::Symbol | ValueType::External | ValueType::Function | ValueType::Unknown => {
Err(napi::Error::new(
napi::Status::GenericFailure,
"Could not convert value returned from macro to AST.",
))
}
}
}

rayon::spawn(move || {
let res = parcel_js_swc_core::transform(config);
match res {
Ok(result) => deferred.resolve(move |env| env.to_js_value(&result)),
Err(err) => deferred.reject(err.into()),
fn await_promise(
env: Env,
result: JsUnknown,
tx: Sender<Result<JsValue, String>>,
) -> napi::Result<()> {
// If the result is a promise, wait for it to resolve, and send the result to the channel.
// Otherwise, send the result immediately.
if result.is_promise()? {
let result: JsObject = result.try_into()?;
let then: JsFunction = result.get_named_property("then")?;
let tx2 = tx.clone();
let cb = env.create_function_from_closure("callback", move |ctx| {
let res = napi_to_js_value(ctx.get::<JsUnknown>(0)?, env)?;
tx.send(Ok(res)).expect("send failure");
ctx.env.get_undefined()
})?;
let eb = env.create_function_from_closure("error_callback", move |ctx| {
let res = ctx.get::<JsUnknown>(0)?;
let message = match napi_to_js_value(res, env)? {
JsValue::String(s) => s,
_ => "Unknown error".into(),
};
tx2.send(Err(message)).expect("send failure");
ctx.env.get_undefined()
})?;
then.call(Some(&result), &[cb, eb])?;
} else {
tx.send(Ok(napi_to_js_value(result, env)?))
.expect("send failure");
}
});

Ok(promise)
Ok(())
}
}
Loading

0 comments on commit cd392af

Please sign in to comment.