From db5a8edde99597d0aa5352935f9a053f132f1caf Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 23 Feb 2024 13:37:34 -0500 Subject: [PATCH] Create a reusable lightningcss-napi crate --- Cargo.lock | 46 +- Cargo.toml | 1 + napi/Cargo.toml | 25 + {node => napi}/src/at_rule_parser.rs | 5 +- napi/src/lib.rs | 1188 +++++++++++++++++++++ {node => napi}/src/threadsafe_function.rs | 0 {node => napi}/src/transformer.rs | 0 node/Cargo.toml | 10 +- node/src/lib.rs | 1184 +------------------- 9 files changed, 1258 insertions(+), 1201 deletions(-) create mode 100644 napi/Cargo.toml rename {node => napi}/src/at_rule_parser.rs (97%) create mode 100644 napi/src/lib.rs rename {node => napi}/src/threadsafe_function.rs (100%) rename {node => napi}/src/transformer.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index e3c7211a..97cbc6b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -802,6 +802,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "lightningcss-napi" +version = "0.1.0" +dependencies = [ + "crossbeam-channel", + "cssparser", + "lightningcss", + "napi", + "parcel_sourcemap", + "rayon", + "serde", + "serde-detach", + "serde_bytes", + "smallvec", +] + [[package]] name = "lightningcss_c_bindings" version = "0.1.0" @@ -816,19 +832,11 @@ dependencies = [ name = "lightningcss_node" version = "0.1.0" dependencies = [ - "crossbeam-channel", - "cssparser", "jemallocator", - "lightningcss", + "lightningcss-napi", "napi", "napi-build", "napi-derive", - "parcel_sourcemap", - "rayon", - "serde", - "serde-detach", - "serde_bytes", - "smallvec", ] [[package]] @@ -903,23 +911,23 @@ checksum = "ebd4419172727423cf30351406c54f6cc1b354a2cfb4f1dba3e6cd07f6d5522b" [[package]] name = "napi-derive" -version = "2.14.0" +version = "2.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01328a2da5c52a77fe839ec8576ddf063529cf23db8958b1030005675f32c45" +checksum = "e56bd9f0bd84c1f138c5cb22bbf394f75d796b24dad689599ca94cf94e61cc21" dependencies = [ "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] name = "napi-derive-backend" -version = "1.0.53" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2470ee27b89da8973defd10dec6def6b412f2873ecc173c9cdc7f44e324d83dc" +checksum = "d03b8f403a37007cad225039fc0323b961bb40d697eea744140920ebb689ff1d" dependencies = [ "convert_case", "once_cell", @@ -927,7 +935,7 @@ dependencies = [ "quote", "regex", "semver", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] @@ -1448,9 +1456,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "semver" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" @@ -1716,9 +1724,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "uuid" diff --git a/Cargo.toml b/Cargo.toml index b1ec1ec5..6806ca01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "node", + "napi", "selectors", "c", "derive", diff --git a/napi/Cargo.toml b/napi/Cargo.toml new file mode 100644 index 00000000..4531913f --- /dev/null +++ b/napi/Cargo.toml @@ -0,0 +1,25 @@ +[package] +authors = ["Devon Govett "] +name = "lightningcss-napi" +version = "0.1.0" +description = "Node-API bindings for Lightning CSS" +license = "MPL-2.0" +repository = "https://github.com/parcel-bundler/lightningcss" +edition = "2021" + +[features] +default = [] +visitor = ["lightningcss/visitor"] +bundler = ["dep:crossbeam-channel", "dep:rayon"] + +[dependencies] +serde = { version = "1.0.123", features = ["derive"] } +serde_bytes = "0.11.5" +cssparser = "0.33.0" +lightningcss = { path = "../", features = ["nodejs", "serde"] } +parcel_sourcemap = { version = "2.1.1", features = ["json"] } +serde-detach = "0.0.1" +smallvec = { version = "1.7.0", features = ["union"] } +napi = {version = "=2.10.3", default-features = false, features = ["napi4", "napi5", "compat-mode", "serde-json"]} +crossbeam-channel = { version = "0.5.6", optional = true } +rayon = { version = "1.5.1", optional = true } diff --git a/node/src/at_rule_parser.rs b/napi/src/at_rule_parser.rs similarity index 97% rename from node/src/at_rule_parser.rs rename to napi/src/at_rule_parser.rs index 111bd106..919eda2d 100644 --- a/node/src/at_rule_parser.rs +++ b/napi/src/at_rule_parser.rs @@ -11,7 +11,6 @@ use lightningcss::{ string::CowArcStr, syntax::{ParsedComponent, SyntaxString}, }, - visitor::{Visit, VisitTypes, Visitor}, }; use serde::{Deserialize, Deserializer, Serialize}; @@ -198,6 +197,10 @@ impl<'i> ToCss for AtRule<'i> { } } +#[cfg(feature = "visitor")] +use lightningcss::visitor::{Visit, VisitTypes, Visitor}; + +#[cfg(feature = "visitor")] impl<'i, V: Visitor<'i, AtRule<'i>>> Visit<'i, AtRule<'i>, V> for AtRule<'i> { const CHILD_TYPES: VisitTypes = VisitTypes::empty(); diff --git a/napi/src/lib.rs b/napi/src/lib.rs new file mode 100644 index 00000000..8127b18e --- /dev/null +++ b/napi/src/lib.rs @@ -0,0 +1,1188 @@ +#[cfg(feature = "bundler")] +use at_rule_parser::AtRule; +use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser}; +use lightningcss::bundler::BundleErrorKind; +#[cfg(feature = "bundler")] +use lightningcss::bundler::{Bundler, SourceProvider}; +use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError}; +use lightningcss::dependencies::{Dependency, DependencyOptions}; +use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind}; +use lightningcss::stylesheet::{ + MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet, +}; +use lightningcss::targets::{Browsers, Features, Targets}; +use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; +use napi::{CallContext, Env, JsObject, JsUnknown}; +use parcel_sourcemap::SourceMap; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, RwLock}; + +mod at_rule_parser; +#[cfg(feature = "bundler")] +#[cfg(not(target_arch = "wasm32"))] +mod threadsafe_function; +#[cfg(feature = "visitor")] +mod transformer; + +#[cfg(feature = "visitor")] +use transformer::JsVisitor; + +#[cfg(not(feature = "visitor"))] +struct JsVisitor; + +#[cfg(feature = "visitor")] +use lightningcss::visitor::Visit; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct TransformResult<'i> { + #[serde(with = "serde_bytes")] + code: Vec, + #[serde(with = "serde_bytes")] + map: Option>, + exports: Option, + references: Option, + dependencies: Option>, + warnings: Vec>, +} + +impl<'i> TransformResult<'i> { + fn into_js(self, env: Env) -> napi::Result { + // Manually construct buffers so we avoid a copy and work around + // https://github.com/napi-rs/napi-rs/issues/1124. + let mut obj = env.create_object()?; + let buf = env.create_buffer_with_data(self.code)?; + obj.set_named_property("code", buf.into_raw())?; + obj.set_named_property( + "map", + if let Some(map) = self.map { + let buf = env.create_buffer_with_data(map)?; + buf.into_raw().into_unknown() + } else { + env.get_null()?.into_unknown() + }, + )?; + obj.set_named_property("exports", env.to_js_value(&self.exports)?)?; + obj.set_named_property("references", env.to_js_value(&self.references)?)?; + obj.set_named_property("dependencies", env.to_js_value(&self.dependencies)?)?; + obj.set_named_property("warnings", env.to_js_value(&self.warnings)?)?; + Ok(obj.into_unknown()) + } +} + +#[cfg(feature = "visitor")] +fn get_visitor(env: Env, opts: &JsObject) -> Option { + if let Ok(visitor) = opts.get_named_property::("visitor") { + Some(JsVisitor::new(env, visitor)) + } else { + None + } +} + +#[cfg(not(feature = "visitor"))] +fn get_visitor(_env: Env, _opts: &JsObject) -> Option { + None +} + +pub fn transform(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let mut visitor = get_visitor(*ctx.env, &opts); + + let config: Config = ctx.env.from_js_value(opts)?; + let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; + let res = compile(code, &config, &mut visitor); + + match res { + Ok(res) => res.into_js(*ctx.env), + Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), + } +} + +pub fn transform_style_attribute(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let mut visitor = get_visitor(*ctx.env, &opts); + + let config: AttrConfig = ctx.env.from_js_value(opts)?; + let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; + let res = compile_attr(code, &config, &mut visitor); + + match res { + Ok(res) => res.into_js(ctx), + Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), + } +} + +#[cfg(feature = "bundler")] +#[cfg(not(target_arch = "wasm32"))] +mod bundle { + use super::*; + use crossbeam_channel::{self, Receiver, Sender}; + use lightningcss::bundler::FileProvider; + use napi::{Env, JsFunction, JsString, NapiRaw}; + use std::path::{Path, PathBuf}; + use std::str::FromStr; + use std::sync::Mutex; + use threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}; + + pub fn bundle(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let mut visitor = get_visitor(*ctx.env, &opts); + + let config: BundleConfig = ctx.env.from_js_value(opts)?; + let fs = FileProvider::new(); + + // This is pretty silly, but works around a rust limitation that you cannot + // explicitly annotate lifetime bounds on closures. + fn annotate<'i, 'o, F>(f: F) -> F + where + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, + { + f + } + + let res = compile_bundle( + &fs, + &config, + visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), + ); + + match res { + Ok(res) => res.into_js(*ctx.env), + Err(err) => Err(err.into_js_error(*ctx.env, None)?), + } + } + + // A SourceProvider which calls JavaScript functions to resolve and read files. + struct JsSourceProvider { + resolve: Option>, + read: Option>, + inputs: Mutex>, + } + + unsafe impl Sync for JsSourceProvider {} + unsafe impl Send for JsSourceProvider {} + + // Allocate a single channel per thread to communicate with the JS thread. + thread_local! { + static CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); + } + + impl SourceProvider for JsSourceProvider { + type Error = napi::Error; + + fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { + let source = if let Some(read) = &self.read { + CHANNEL.with(|channel| { + let message = ReadMessage { + file: file.to_str().unwrap().to_owned(), + tx: channel.0.clone(), + }; + + read.call(message, ThreadsafeFunctionCallMode::Blocking); + channel.1.recv().unwrap() + }) + } else { + Ok(std::fs::read_to_string(file)?) + }; + + match source { + Ok(source) => { + // cache the result + let ptr = Box::into_raw(Box::new(source)); + self.inputs.lock().unwrap().push(ptr); + // SAFETY: this is safe because the pointer is not dropped + // until the JsSourceProvider is, and we never remove from the + // list of pointers stored in the vector. + Ok(unsafe { &*ptr }) + } + Err(e) => Err(e), + } + } + + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + if let Some(resolve) = &self.resolve { + return CHANNEL.with(|channel| { + let message = ResolveMessage { + specifier: specifier.to_owned(), + originating_file: originating_file.to_str().unwrap().to_owned(), + tx: channel.0.clone(), + }; + + resolve.call(message, ThreadsafeFunctionCallMode::Blocking); + let result = channel.1.recv().unwrap(); + match result { + Ok(result) => Ok(PathBuf::from_str(&result).unwrap()), + Err(e) => Err(e), + } + }); + } + + Ok(originating_file.with_file_name(specifier)) + } + } + + struct ResolveMessage { + specifier: String, + originating_file: String, + tx: Sender>, + } + + struct ReadMessage { + file: String, + tx: Sender>, + } + + struct VisitMessage { + stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>, + tx: Sender>, + } + + fn await_promise(env: Env, result: JsUnknown, tx: Sender>) -> 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 = ctx.get::(0)?.into_utf8()?; + let s = res.into_owned()?; + tx.send(Ok(s)).unwrap(); + ctx.env.get_undefined() + })?; + let eb = env.create_function_from_closure("error_callback", move |ctx| { + let res = ctx.get::(0)?; + tx2.send(Err(napi::Error::from(res))).unwrap(); + ctx.env.get_undefined() + })?; + then.call(Some(&result), &[cb, eb])?; + } else { + let result: JsString = result.try_into()?; + let utf8 = result.into_utf8()?; + let s = utf8.into_owned()?; + tx.send(Ok(s)).unwrap(); + } + + Ok(()) + } + + fn resolve_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { + let specifier = ctx.env.create_string(&ctx.value.specifier)?; + let originating_file = ctx.env.create_string(&ctx.value.originating_file)?; + let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?; + await_promise(ctx.env, result, ctx.value.tx) + } + + fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { + match res { + Ok(_) => Ok(()), + Err(e) => { + tx.send(Err(e)).expect("send error"); + Ok(()) + } + } + } + + fn resolve_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { + let tx = ctx.value.tx.clone(); + handle_error(tx, resolve_on_js_thread(ctx)) + } + + fn read_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { + let file = ctx.env.create_string(&ctx.value.file)?; + let result = ctx.callback.unwrap().call(None, &[file])?; + await_promise(ctx.env, result, ctx.value.tx) + } + + fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { + let tx = ctx.value.tx.clone(); + handle_error(tx, read_on_js_thread(ctx)) + } + + pub fn bundle_async(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let visitor = get_visitor(*ctx.env, &opts); + + let config: BundleConfig = ctx.env.from_js_value(&opts)?; + + if let Ok(resolver) = opts.get_named_property::("resolver") { + let read = if resolver.has_named_property("read")? { + let read = resolver.get_named_property::("read")?; + Some(ThreadsafeFunction::create( + ctx.env.raw(), + unsafe { read.raw() }, + 0, + read_on_js_thread_wrapper, + )?) + } else { + None + }; + + let resolve = if resolver.has_named_property("resolve")? { + let resolve = resolver.get_named_property::("resolve")?; + Some(ThreadsafeFunction::create( + ctx.env.raw(), + unsafe { resolve.raw() }, + 0, + resolve_on_js_thread_wrapper, + )?) + } else { + None + }; + + let provider = JsSourceProvider { + resolve, + read, + inputs: Mutex::new(Vec::new()), + }; + + run_bundle_task(provider, config, visitor, *ctx.env) + } else { + let provider = FileProvider::new(); + run_bundle_task(provider, config, visitor, *ctx.env) + } + } + + // Runs bundling on a background thread managed by rayon. This is similar to AsyncTask from napi-rs, however, + // because we call back into the JS thread, which might call other tasks in the node threadpool (e.g. fs.readFile), + // we may end up deadlocking if the number of rayon threads exceeds node's threadpool size. Therefore, we must + // run bundling from a thread not managed by Node. + fn run_bundle_task( + provider: P, + config: BundleConfig, + visitor: Option, + env: Env, + ) -> napi::Result + where + P::Error: IntoJsError, + { + let (deferred, promise) = env.create_deferred()?; + + let tsfn = if let Some(mut visitor) = visitor { + Some(ThreadsafeFunction::create( + env.raw(), + std::ptr::null_mut(), + 0, + move |ctx: ThreadSafeCallContext| { + if let Err(err) = ctx.value.stylesheet.visit(&mut visitor) { + ctx.value.tx.send(Err(err)).expect("send error"); + return Ok(()); + } + ctx.value.tx.send(Ok(Default::default())).expect("send error"); + Ok(()) + }, + )?) + } else { + None + }; + + // Run bundling task in rayon threadpool. + rayon::spawn(move || { + let res = compile_bundle( + unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) }, + &config, + tsfn.map(move |tsfn| { + move |stylesheet: &mut StyleSheet| { + CHANNEL.with(|channel| { + let message = VisitMessage { + // SAFETY: we immediately lock the thread until we get a response, + // so stylesheet cannot be dropped in that time. + stylesheet: unsafe { + std::mem::transmute::< + &'_ mut StyleSheet<'_, '_, AtRule>, + &'static mut StyleSheet<'static, 'static, AtRule>, + >(stylesheet) + }, + tx: channel.0.clone(), + }; + + tsfn.call(message, ThreadsafeFunctionCallMode::Blocking); + channel.1.recv().expect("recv error").map(|_| ()) + }) + } + }), + ); + + deferred.resolve(move |env| match res { + Ok(v) => v.into_js(env), + Err(err) => Err(err.into_js_error(env, None)?), + }); + }); + + Ok(promise) + } +} + +#[cfg(feature = "bundler")] +#[cfg(target_arch = "wasm32")] +mod bundle { + use super::*; + use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref}; + use std::cell::UnsafeCell; + use std::path::{Path, PathBuf}; + use std::str::FromStr; + + pub fn bundle(ctx: CallContext) -> napi::Result { + let opts = ctx.get::(0)?; + let mut visitor = get_visitor(*ctx.env, &opts); + + let resolver = opts.get_named_property::("resolver")?; + let read = resolver.get_named_property::("read")?; + let resolve = if resolver.has_named_property("resolve")? { + let resolve = resolver.get_named_property::("resolve")?; + Some(ctx.env.create_reference(resolve)?) + } else { + None + }; + let config: BundleConfig = ctx.env.from_js_value(opts)?; + + let provider = JsSourceProvider { + env: ctx.env.clone(), + resolve, + read: ctx.env.create_reference(read)?, + inputs: UnsafeCell::new(Vec::new()), + }; + + // This is pretty silly, but works around a rust limitation that you cannot + // explicitly annotate lifetime bounds on closures. + fn annotate<'i, 'o, F>(f: F) -> F + where + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, + { + f + } + + let res = compile_bundle( + &provider, + &config, + visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), + ); + + match res { + Ok(res) => res.into_js(*ctx.env), + Err(err) => Err(err.into_js_error(*ctx.env, None)?), + } + } + + struct JsSourceProvider { + env: Env, + resolve: Option>, + read: Ref<()>, + inputs: UnsafeCell>, + } + + impl Drop for JsSourceProvider { + fn drop(&mut self) { + if let Some(resolve) = &mut self.resolve { + drop(resolve.unref(self.env)); + } + drop(self.read.unref(self.env)); + } + } + + unsafe impl Sync for JsSourceProvider {} + unsafe impl Send for JsSourceProvider {} + + // This relies on Binaryen's Asyncify transform to allow Rust to call async JS functions from sync code. + // See the comments in async.mjs for more details about how this works. + extern "C" { + fn await_promise_sync( + promise: napi::sys::napi_value, + result: *mut napi::sys::napi_value, + error: *mut napi::sys::napi_value, + ); + } + + fn get_result(env: Env, mut value: JsUnknown) -> napi::Result { + if value.is_promise()? { + let mut result = std::ptr::null_mut(); + let mut error = std::ptr::null_mut(); + unsafe { await_promise_sync(value.raw(), &mut result, &mut error) }; + if !error.is_null() { + let error = unsafe { JsUnknown::from_raw(env.raw(), error)? }; + return Err(napi::Error::from(error)); + } + if result.is_null() { + return Err(napi::Error::new(napi::Status::GenericFailure, "No result".into())); + } + + value = unsafe { JsUnknown::from_raw(env.raw(), result)? }; + } + + value.try_into() + } + + impl SourceProvider for JsSourceProvider { + type Error = napi::Error; + + fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { + let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?; + let file = self.env.create_string(file.to_str().unwrap())?; + let source: JsUnknown = read.call(None, &[file])?; + let source = get_result(self.env, source)?.into_utf8()?.into_owned()?; + + // cache the result + let ptr = Box::into_raw(Box::new(source)); + let inputs = unsafe { &mut *self.inputs.get() }; + inputs.push(ptr); + // SAFETY: this is safe because the pointer is not dropped + // until the JsSourceProvider is, and we never remove from the + // list of pointers stored in the vector. + Ok(unsafe { &*ptr }) + } + + fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { + if let Some(resolve) = &self.resolve { + let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?; + let specifier = self.env.create_string(specifier)?; + let originating_file = self.env.create_string(originating_file.to_str().unwrap())?; + let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?; + let result = get_result(self.env, result)?.into_utf8()?; + Ok(PathBuf::from_str(result.as_str()?).unwrap()) + } else { + Ok(originating_file.with_file_name(specifier)) + } + } + } +} + +#[cfg(feature = "bundler")] +pub use bundle::*; + +// --------------------------------------------- + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Config { + pub filename: Option, + pub project_root: Option, + #[serde(with = "serde_bytes")] + pub code: Vec, + pub targets: Option, + #[serde(default)] + pub include: u32, + #[serde(default)] + pub exclude: u32, + pub minify: Option, + pub source_map: Option, + pub input_source_map: Option, + pub drafts: Option, + pub non_standard: Option, + pub css_modules: Option, + pub analyze_dependencies: Option, + pub pseudo_classes: Option, + pub unused_symbols: Option>, + pub error_recovery: Option, + pub custom_at_rules: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum AnalyzeDependenciesOption { + Bool(bool), + Config(AnalyzeDependenciesConfig), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AnalyzeDependenciesConfig { + preserve_imports: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum CssModulesOption { + Bool(bool), + Config(CssModulesConfig), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CssModulesConfig { + pattern: Option, + dashed_idents: Option, +} + +#[cfg(feature = "bundler")] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct BundleConfig { + pub filename: String, + pub project_root: Option, + pub targets: Option, + #[serde(default)] + pub include: u32, + #[serde(default)] + pub exclude: u32, + pub minify: Option, + pub source_map: Option, + pub drafts: Option, + pub non_standard: Option, + pub css_modules: Option, + pub analyze_dependencies: Option, + pub pseudo_classes: Option, + pub unused_symbols: Option>, + pub error_recovery: Option, + pub custom_at_rules: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct OwnedPseudoClasses { + pub hover: Option, + pub active: Option, + pub focus: Option, + pub focus_visible: Option, + pub focus_within: Option, +} + +impl<'a> Into> for &'a OwnedPseudoClasses { + fn into(self) -> PseudoClasses<'a> { + PseudoClasses { + hover: self.hover.as_deref(), + active: self.active.as_deref(), + focus: self.focus.as_deref(), + focus_visible: self.focus_visible.as_deref(), + focus_within: self.focus_within.as_deref(), + } + } +} + +#[derive(Serialize, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct Drafts { + #[serde(default)] + custom_media: bool, +} + +#[derive(Serialize, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct NonStandard { + #[serde(default)] + deep_selector_combinator: bool, +} + +fn compile<'i>( + code: &'i str, + config: &Config, + #[allow(unused_variables)] visitor: &mut Option, +) -> Result, CompileError<'i, napi::Error>> { + let drafts = config.drafts.as_ref(); + let non_standard = config.non_standard.as_ref(); + let warnings = Some(Arc::new(RwLock::new(Vec::new()))); + + let filename = config.filename.clone().unwrap_or_default(); + let project_root = config.project_root.as_ref().map(|p| p.as_ref()); + let mut source_map = if config.source_map.unwrap_or_default() { + let mut sm = SourceMap::new(project_root.unwrap_or("/")); + sm.add_source(&filename); + sm.set_source_content(0, code)?; + Some(sm) + } else { + None + }; + + let res = { + let mut flags = ParserFlags::empty(); + flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); + flags.set( + ParserFlags::DEEP_SELECTOR_COMBINATOR, + matches!(non_standard, Some(v) if v.deep_selector_combinator), + ); + + let mut stylesheet = StyleSheet::parse_with( + &code, + ParserOptions { + filename: filename.clone(), + flags, + css_modules: if let Some(css_modules) = &config.css_modules { + match css_modules { + CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), + CssModulesOption::Bool(false) => None, + CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { + pattern: if let Some(pattern) = c.pattern.as_ref() { + match lightningcss::css_modules::Pattern::parse(pattern) { + Ok(p) => p, + Err(e) => return Err(CompileError::PatternError(e)), + } + } else { + Default::default() + }, + dashed_idents: c.dashed_idents.unwrap_or_default(), + }), + } + } else { + None + }, + source_index: 0, + error_recovery: config.error_recovery.unwrap_or_default(), + warnings: warnings.clone(), + }, + &mut CustomAtRuleParser { + configs: config.custom_at_rules.clone().unwrap_or_default(), + }, + )?; + + #[cfg(feature = "visitor")] + if let Some(visitor) = visitor.as_mut() { + stylesheet.visit(visitor).map_err(CompileError::JsError)?; + } + + let targets = Targets { + browsers: config.targets, + include: Features::from_bits_truncate(config.include), + exclude: Features::from_bits_truncate(config.exclude), + }; + + stylesheet.minify(MinifyOptions { + targets, + unused_symbols: config.unused_symbols.clone().unwrap_or_default(), + })?; + + stylesheet.to_css(PrinterOptions { + minify: config.minify.unwrap_or_default(), + source_map: source_map.as_mut(), + project_root, + targets, + analyze_dependencies: if let Some(d) = &config.analyze_dependencies { + match d { + AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), + AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { + remove_imports: !c.preserve_imports, + }), + _ => None, + } + } else { + None + }, + pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), + })? + }; + + let map = if let Some(mut source_map) = source_map { + if let Some(input_source_map) = &config.input_source_map { + if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) { + let _ = source_map.extends(&mut sm); + } + } + + source_map.to_json(None).ok() + } else { + None + }; + + Ok(TransformResult { + code: res.code.into_bytes(), + map: map.map(|m| m.into_bytes()), + exports: res.exports, + references: res.references, + dependencies: res.dependencies, + warnings: warnings.map_or(Vec::new(), |w| { + Arc::try_unwrap(w) + .unwrap() + .into_inner() + .unwrap() + .into_iter() + .map(|w| w.into()) + .collect() + }), + }) +} + +#[cfg(feature = "bundler")] +fn compile_bundle< + 'i, + 'o, + P: SourceProvider, + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, +>( + fs: &'i P, + config: &'o BundleConfig, + visit: Option, +) -> Result, CompileError<'i, P::Error>> { + use std::path::Path; + + let project_root = config.project_root.as_ref().map(|p| p.as_ref()); + let mut source_map = if config.source_map.unwrap_or_default() { + Some(SourceMap::new(project_root.unwrap_or("/"))) + } else { + None + }; + let warnings = Some(Arc::new(RwLock::new(Vec::new()))); + + let res = { + let drafts = config.drafts.as_ref(); + let non_standard = config.non_standard.as_ref(); + let mut flags = ParserFlags::empty(); + flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); + flags.set( + ParserFlags::DEEP_SELECTOR_COMBINATOR, + matches!(non_standard, Some(v) if v.deep_selector_combinator), + ); + + let parser_options = ParserOptions { + flags, + css_modules: if let Some(css_modules) = &config.css_modules { + match css_modules { + CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), + CssModulesOption::Bool(false) => None, + CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { + pattern: if let Some(pattern) = c.pattern.as_ref() { + match lightningcss::css_modules::Pattern::parse(pattern) { + Ok(p) => p, + Err(e) => return Err(CompileError::PatternError(e)), + } + } else { + Default::default() + }, + dashed_idents: c.dashed_idents.unwrap_or_default(), + }), + } + } else { + None + }, + error_recovery: config.error_recovery.unwrap_or_default(), + warnings: warnings.clone(), + filename: String::new(), + source_index: 0, + }; + + let mut at_rule_parser = CustomAtRuleParser { + configs: config.custom_at_rules.clone().unwrap_or_default(), + }; + + let mut bundler = + Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser); + let mut stylesheet = bundler.bundle(Path::new(&config.filename))?; + + if let Some(visit) = visit { + visit(&mut stylesheet).map_err(CompileError::JsError)?; + } + + let targets = Targets { + browsers: config.targets, + include: Features::from_bits_truncate(config.include), + exclude: Features::from_bits_truncate(config.exclude), + }; + + stylesheet.minify(MinifyOptions { + targets, + unused_symbols: config.unused_symbols.clone().unwrap_or_default(), + })?; + + stylesheet.to_css(PrinterOptions { + minify: config.minify.unwrap_or_default(), + source_map: source_map.as_mut(), + project_root, + targets, + analyze_dependencies: if let Some(d) = &config.analyze_dependencies { + match d { + AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), + AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { + remove_imports: !c.preserve_imports, + }), + _ => None, + } + } else { + None + }, + pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), + })? + }; + + let map = if let Some(source_map) = &mut source_map { + source_map.to_json(None).ok() + } else { + None + }; + + Ok(TransformResult { + code: res.code.into_bytes(), + map: map.map(|m| m.into_bytes()), + exports: res.exports, + references: res.references, + dependencies: res.dependencies, + warnings: warnings.map_or(Vec::new(), |w| { + Arc::try_unwrap(w) + .unwrap() + .into_inner() + .unwrap() + .into_iter() + .map(|w| w.into()) + .collect() + }), + }) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AttrConfig { + pub filename: Option, + #[serde(with = "serde_bytes")] + pub code: Vec, + pub targets: Option, + #[serde(default)] + pub include: u32, + #[serde(default)] + pub exclude: u32, + #[serde(default)] + pub minify: bool, + #[serde(default)] + pub analyze_dependencies: bool, + #[serde(default)] + pub error_recovery: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct AttrResult<'i> { + #[serde(with = "serde_bytes")] + code: Vec, + dependencies: Option>, + warnings: Vec>, +} + +impl<'i> AttrResult<'i> { + fn into_js(self, ctx: CallContext) -> napi::Result { + // Manually construct buffers so we avoid a copy and work around + // https://github.com/napi-rs/napi-rs/issues/1124. + let mut obj = ctx.env.create_object()?; + let buf = ctx.env.create_buffer_with_data(self.code)?; + obj.set_named_property("code", buf.into_raw())?; + obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?; + obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?; + Ok(obj.into_unknown()) + } +} + +fn compile_attr<'i>( + code: &'i str, + config: &AttrConfig, + #[allow(unused_variables)] visitor: &mut Option, +) -> Result, CompileError<'i, napi::Error>> { + let warnings = if config.error_recovery { + Some(Arc::new(RwLock::new(Vec::new()))) + } else { + None + }; + let res = { + let filename = config.filename.clone().unwrap_or_default(); + let mut attr = StyleAttribute::parse( + &code, + ParserOptions { + filename, + error_recovery: config.error_recovery, + warnings: warnings.clone(), + ..ParserOptions::default() + }, + )?; + + #[cfg(feature = "visitor")] + if let Some(visitor) = visitor.as_mut() { + attr.visit(visitor).unwrap(); + } + + let targets = Targets { + browsers: config.targets, + include: Features::from_bits_truncate(config.include), + exclude: Features::from_bits_truncate(config.exclude), + }; + + attr.minify(MinifyOptions { + targets, + ..MinifyOptions::default() + }); + attr.to_css(PrinterOptions { + minify: config.minify, + source_map: None, + project_root: None, + targets, + analyze_dependencies: if config.analyze_dependencies { + Some(DependencyOptions::default()) + } else { + None + }, + pseudo_classes: None, + })? + }; + Ok(AttrResult { + code: res.code.into_bytes(), + dependencies: res.dependencies, + warnings: warnings.map_or(Vec::new(), |w| { + Arc::try_unwrap(w) + .unwrap() + .into_inner() + .unwrap() + .into_iter() + .map(|w| w.into()) + .collect() + }), + }) +} + +enum CompileError<'i, E: std::error::Error> { + ParseError(Error>), + MinifyError(Error), + PrinterError(Error), + SourceMapError(parcel_sourcemap::SourceMapError), + BundleError(Error>), + PatternError(PatternParseError), + #[cfg(feature = "visitor")] + JsError(napi::Error), +} + +impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + CompileError::ParseError(err) => err.kind.fmt(f), + CompileError::MinifyError(err) => err.kind.fmt(f), + CompileError::PrinterError(err) => err.kind.fmt(f), + CompileError::BundleError(err) => err.kind.fmt(f), + CompileError::PatternError(err) => err.fmt(f), + CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this + #[cfg(feature = "visitor")] + CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f), + } + } +} + +impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> { + fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result { + let reason = self.to_string(); + let data = match &self { + CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?, + CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?, + CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?, + CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?, + _ => env.get_null()?.into_unknown(), + }; + + let (js_error, loc) = match self { + CompileError::BundleError(Error { + loc, + kind: BundleErrorKind::ResolverError(e), + }) => { + // Add location info to existing JS error if available. + (e.into_js_error(env)?, loc) + } + CompileError::ParseError(Error { loc, .. }) + | CompileError::PrinterError(Error { loc, .. }) + | CompileError::MinifyError(Error { loc, .. }) + | CompileError::BundleError(Error { loc, .. }) => { + // Generate an error with location information. + let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; + let reason = env.create_string_from_std(reason)?; + let obj = syntax_error.new_instance(&[reason])?; + (obj.into_unknown(), loc) + } + _ => return Ok(self.into()), + }; + + if js_error.get_type()? == napi::ValueType::Object { + let mut obj: JsObject = unsafe { js_error.cast() }; + if let Some(loc) = loc { + let line = env.create_int32((loc.line + 1) as i32)?; + let col = env.create_int32(loc.column as i32)?; + let filename = env.create_string_from_std(loc.filename)?; + obj.set_named_property("fileName", filename)?; + if let Some(code) = code { + let source = env.create_string(code)?; + obj.set_named_property("source", source)?; + } + let mut loc = env.create_object()?; + loc.set_named_property("line", line)?; + loc.set_named_property("column", col)?; + obj.set_named_property("loc", loc)?; + } + obj.set_named_property("data", data)?; + Ok(obj.into_unknown().into()) + } else { + Ok(js_error.into()) + } + } +} + +trait IntoJsError { + fn into_js_error(self, env: Env) -> napi::Result; +} + +impl IntoJsError for std::io::Error { + fn into_js_error(self, env: Env) -> napi::Result { + let reason = self.to_string(); + let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; + let reason = env.create_string_from_std(reason)?; + let obj = syntax_error.new_instance(&[reason])?; + Ok(obj.into_unknown()) + } +} + +impl IntoJsError for napi::Error { + fn into_js_error(self, env: Env) -> napi::Result { + unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) } + } +} + +impl<'i, E: std::error::Error> From>> for CompileError<'i, E> { + fn from(e: Error>) -> CompileError<'i, E> { + CompileError::ParseError(e) + } +} + +impl<'i, E: std::error::Error> From> for CompileError<'i, E> { + fn from(err: Error) -> CompileError<'i, E> { + CompileError::MinifyError(err) + } +} + +impl<'i, E: std::error::Error> From> for CompileError<'i, E> { + fn from(err: Error) -> CompileError<'i, E> { + CompileError::PrinterError(err) + } +} + +impl<'i, E: std::error::Error> From for CompileError<'i, E> { + fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> { + CompileError::SourceMapError(e) + } +} + +impl<'i, E: std::error::Error> From>> for CompileError<'i, E> { + fn from(e: Error>) -> CompileError<'i, E> { + CompileError::BundleError(e) + } +} + +impl<'i, E: std::error::Error> From> for napi::Error { + fn from(e: CompileError<'i, E>) -> napi::Error { + match e { + CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()), + CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()), + #[cfg(feature = "visitor")] + CompileError::JsError(e) => e, + _ => napi::Error::new(napi::Status::GenericFailure, e.to_string()), + } + } +} + +#[derive(Serialize)] +struct Warning<'i> { + message: String, + #[serde(flatten)] + data: ParserError<'i>, + loc: Option, +} + +impl<'i> From>> for Warning<'i> { + fn from(mut e: Error>) -> Self { + // Convert to 1-based line numbers. + if let Some(loc) = &mut e.loc { + loc.line += 1; + } + Warning { + message: e.kind.to_string(), + data: e.kind, + loc: e.loc, + } + } +} diff --git a/node/src/threadsafe_function.rs b/napi/src/threadsafe_function.rs similarity index 100% rename from node/src/threadsafe_function.rs rename to napi/src/threadsafe_function.rs diff --git a/node/src/transformer.rs b/napi/src/transformer.rs similarity index 100% rename from node/src/transformer.rs rename to napi/src/transformer.rs diff --git a/node/Cargo.toml b/node/Cargo.toml index 5b26eea7..19a6783e 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -9,17 +9,9 @@ publish = false crate-type = ["cdylib"] [dependencies] -serde = { version = "1.0.123", features = ["derive"] } -serde_bytes = "0.11.5" -cssparser = "0.33.0" -lightningcss = { path = "../", features = ["nodejs", "serde", "visitor"] } -parcel_sourcemap = { version = "2.1.1", features = ["json"] } -serde-detach = "0.0.1" -smallvec = { version = "1.7.0", features = ["union"] } +lightningcss-napi = { version = "0.1.0", path = "../napi", features = ["bundler", "visitor"] } napi = {version = "=2.10.3", default-features = false, features = ["napi4", "napi5", "compat-mode", "serde-json"]} napi-derive = "2" -crossbeam-channel = "0.5.6" -rayon = "1.5.1" [target.'cfg(target_os = "macos")'.dependencies] jemallocator = { version = "0.3.2", features = ["disable_initial_exec_tls"] } diff --git a/node/src/lib.rs b/node/src/lib.rs index cb0d7858..16030b51 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -2,570 +2,38 @@ #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; -use at_rule_parser::{AtRule, CustomAtRuleConfig, CustomAtRuleParser}; -use lightningcss::bundler::{BundleErrorKind, Bundler, FileProvider, SourceProvider}; -use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError}; -use lightningcss::dependencies::{Dependency, DependencyOptions}; -use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind}; -use lightningcss::stylesheet::{ - MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet, -}; -use lightningcss::targets::{Browsers, Features, Targets}; -use lightningcss::visitor::Visit; -use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; -use parcel_sourcemap::SourceMap; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::{Arc, Mutex, RwLock}; - -use transformer::JsVisitor; - -mod at_rule_parser; -#[cfg(not(target_arch = "wasm32"))] -mod threadsafe_function; -mod transformer; - -use napi::{CallContext, Env, JsObject, JsUnknown}; +use napi::{CallContext, JsObject, JsUnknown}; use napi_derive::{js_function, module_exports}; -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct TransformResult<'i> { - #[serde(with = "serde_bytes")] - code: Vec, - #[serde(with = "serde_bytes")] - map: Option>, - exports: Option, - references: Option, - dependencies: Option>, - warnings: Vec>, -} - -impl<'i> TransformResult<'i> { - fn into_js(self, env: Env) -> napi::Result { - // Manually construct buffers so we avoid a copy and work around - // https://github.com/napi-rs/napi-rs/issues/1124. - let mut obj = env.create_object()?; - let buf = env.create_buffer_with_data(self.code)?; - obj.set_named_property("code", buf.into_raw())?; - obj.set_named_property( - "map", - if let Some(map) = self.map { - let buf = env.create_buffer_with_data(map)?; - buf.into_raw().into_unknown() - } else { - env.get_null()?.into_unknown() - }, - )?; - obj.set_named_property("exports", env.to_js_value(&self.exports)?)?; - obj.set_named_property("references", env.to_js_value(&self.references)?)?; - obj.set_named_property("dependencies", env.to_js_value(&self.dependencies)?)?; - obj.set_named_property("warnings", env.to_js_value(&self.warnings)?)?; - Ok(obj.into_unknown()) - } -} - #[js_function(1)] fn transform(ctx: CallContext) -> napi::Result { - use transformer::JsVisitor; - - let opts = ctx.get::(0)?; - let mut visitor = if let Ok(visitor) = opts.get_named_property::("visitor") { - Some(JsVisitor::new(*ctx.env, visitor)) - } else { - None - }; - - let config: Config = ctx.env.from_js_value(opts)?; - let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; - let res = compile(code, &config, &mut visitor); - - match res { - Ok(res) => res.into_js(*ctx.env), - Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), - } + lightningcss_napi::transform(ctx) } #[js_function(1)] fn transform_style_attribute(ctx: CallContext) -> napi::Result { - use transformer::JsVisitor; - - let opts = ctx.get::(0)?; - let mut visitor = if let Ok(visitor) = opts.get_named_property::("visitor") { - Some(JsVisitor::new(*ctx.env, visitor)) - } else { - None - }; - - let config: AttrConfig = ctx.env.from_js_value(opts)?; - let code = unsafe { std::str::from_utf8_unchecked(&config.code) }; - let res = compile_attr(code, &config, &mut visitor); - - match res { - Ok(res) => res.into_js(ctx), - Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?), - } + lightningcss_napi::transform_style_attribute(ctx) } -#[cfg(not(target_arch = "wasm32"))] -mod bundle { - use super::*; - use crossbeam_channel::{self, Receiver, Sender}; - use napi::{Env, JsFunction, JsString, NapiRaw}; - use threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}; - - #[js_function(1)] - pub fn bundle(ctx: CallContext) -> napi::Result { - use transformer::JsVisitor; - - let opts = ctx.get::(0)?; - let mut visitor = if let Ok(visitor) = opts.get_named_property::("visitor") { - Some(JsVisitor::new(*ctx.env, visitor)) - } else { - None - }; - - let config: BundleConfig = ctx.env.from_js_value(opts)?; - let fs = FileProvider::new(); - - // This is pretty silly, but works around a rust limitation that you cannot - // explicitly annotate lifetime bounds on closures. - fn annotate<'i, 'o, F>(f: F) -> F - where - F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, - { - f - } - - let res = compile_bundle( - &fs, - &config, - visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), - ); - - match res { - Ok(res) => res.into_js(*ctx.env), - Err(err) => Err(err.into_js_error(*ctx.env, None)?), - } - } - - // A SourceProvider which calls JavaScript functions to resolve and read files. - struct JsSourceProvider { - resolve: Option>, - read: Option>, - inputs: Mutex>, - } - - unsafe impl Sync for JsSourceProvider {} - unsafe impl Send for JsSourceProvider {} - - // Allocate a single channel per thread to communicate with the JS thread. - thread_local! { - static CHANNEL: (Sender>, Receiver>) = crossbeam_channel::unbounded(); - } - - impl SourceProvider for JsSourceProvider { - type Error = napi::Error; - - fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { - let source = if let Some(read) = &self.read { - CHANNEL.with(|channel| { - let message = ReadMessage { - file: file.to_str().unwrap().to_owned(), - tx: channel.0.clone(), - }; - - read.call(message, ThreadsafeFunctionCallMode::Blocking); - channel.1.recv().unwrap() - }) - } else { - Ok(std::fs::read_to_string(file)?) - }; - - match source { - Ok(source) => { - // cache the result - let ptr = Box::into_raw(Box::new(source)); - self.inputs.lock().unwrap().push(ptr); - // SAFETY: this is safe because the pointer is not dropped - // until the JsSourceProvider is, and we never remove from the - // list of pointers stored in the vector. - Ok(unsafe { &*ptr }) - } - Err(e) => Err(e), - } - } - - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { - if let Some(resolve) = &self.resolve { - return CHANNEL.with(|channel| { - let message = ResolveMessage { - specifier: specifier.to_owned(), - originating_file: originating_file.to_str().unwrap().to_owned(), - tx: channel.0.clone(), - }; - - resolve.call(message, ThreadsafeFunctionCallMode::Blocking); - let result = channel.1.recv().unwrap(); - match result { - Ok(result) => Ok(PathBuf::from_str(&result).unwrap()), - Err(e) => Err(e), - } - }); - } - - Ok(originating_file.with_file_name(specifier)) - } - } - - struct ResolveMessage { - specifier: String, - originating_file: String, - tx: Sender>, - } - - struct ReadMessage { - file: String, - tx: Sender>, - } - - struct VisitMessage { - stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>, - tx: Sender>, - } - - fn await_promise(env: Env, result: JsUnknown, tx: Sender>) -> 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 = ctx.get::(0)?.into_utf8()?; - let s = res.into_owned()?; - tx.send(Ok(s)).unwrap(); - ctx.env.get_undefined() - })?; - let eb = env.create_function_from_closure("error_callback", move |ctx| { - let res = ctx.get::(0)?; - tx2.send(Err(napi::Error::from(res))).unwrap(); - ctx.env.get_undefined() - })?; - then.call(Some(&result), &[cb, eb])?; - } else { - let result: JsString = result.try_into()?; - let utf8 = result.into_utf8()?; - let s = utf8.into_owned()?; - tx.send(Ok(s)).unwrap(); - } - - Ok(()) - } - - fn resolve_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { - let specifier = ctx.env.create_string(&ctx.value.specifier)?; - let originating_file = ctx.env.create_string(&ctx.value.originating_file)?; - let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?; - await_promise(ctx.env, result, ctx.value.tx) - } - - fn handle_error(tx: Sender>, res: napi::Result<()>) -> napi::Result<()> { - match res { - Ok(_) => Ok(()), - Err(e) => { - tx.send(Err(e)).expect("send error"); - Ok(()) - } - } - } - - fn resolve_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { - let tx = ctx.value.tx.clone(); - handle_error(tx, resolve_on_js_thread(ctx)) - } - - fn read_on_js_thread(ctx: ThreadSafeCallContext) -> napi::Result<()> { - let file = ctx.env.create_string(&ctx.value.file)?; - let result = ctx.callback.unwrap().call(None, &[file])?; - await_promise(ctx.env, result, ctx.value.tx) - } - - fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext) -> napi::Result<()> { - let tx = ctx.value.tx.clone(); - handle_error(tx, read_on_js_thread(ctx)) - } - - #[js_function(1)] - pub fn bundle_async(ctx: CallContext) -> napi::Result { - use transformer::JsVisitor; - - let opts = ctx.get::(0)?; - let visitor = if let Ok(visitor) = opts.get_named_property::("visitor") { - let visitor = JsVisitor::new(*ctx.env, visitor); - Some(visitor) - } else { - None - }; - - let config: BundleConfig = ctx.env.from_js_value(&opts)?; - - if let Ok(resolver) = opts.get_named_property::("resolver") { - let read = if resolver.has_named_property("read")? { - let read = resolver.get_named_property::("read")?; - Some(ThreadsafeFunction::create( - ctx.env.raw(), - unsafe { read.raw() }, - 0, - read_on_js_thread_wrapper, - )?) - } else { - None - }; - - let resolve = if resolver.has_named_property("resolve")? { - let resolve = resolver.get_named_property::("resolve")?; - Some(ThreadsafeFunction::create( - ctx.env.raw(), - unsafe { resolve.raw() }, - 0, - resolve_on_js_thread_wrapper, - )?) - } else { - None - }; - - let provider = JsSourceProvider { - resolve, - read, - inputs: Mutex::new(Vec::new()), - }; - - run_bundle_task(provider, config, visitor, *ctx.env) - } else { - let provider = FileProvider::new(); - run_bundle_task(provider, config, visitor, *ctx.env) - } - } - - // Runs bundling on a background thread managed by rayon. This is similar to AsyncTask from napi-rs, however, - // because we call back into the JS thread, which might call other tasks in the node threadpool (e.g. fs.readFile), - // we may end up deadlocking if the number of rayon threads exceeds node's threadpool size. Therefore, we must - // run bundling from a thread not managed by Node. - fn run_bundle_task( - provider: P, - config: BundleConfig, - visitor: Option, - env: Env, - ) -> napi::Result - where - P::Error: IntoJsError, - { - let (deferred, promise) = env.create_deferred()?; - - let tsfn = if let Some(mut visitor) = visitor { - Some(ThreadsafeFunction::create( - env.raw(), - std::ptr::null_mut(), - 0, - move |ctx: ThreadSafeCallContext| { - if let Err(err) = ctx.value.stylesheet.visit(&mut visitor) { - ctx.value.tx.send(Err(err)).expect("send error"); - return Ok(()); - } - ctx.value.tx.send(Ok(Default::default())).expect("send error"); - Ok(()) - }, - )?) - } else { - None - }; - - // Run bundling task in rayon threadpool. - rayon::spawn(move || { - let res = compile_bundle( - unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) }, - &config, - tsfn.map(move |tsfn| { - move |stylesheet: &mut StyleSheet| { - CHANNEL.with(|channel| { - let message = VisitMessage { - // SAFETY: we immediately lock the thread until we get a response, - // so stylesheet cannot be dropped in that time. - stylesheet: unsafe { - std::mem::transmute::< - &'_ mut StyleSheet<'_, '_, AtRule>, - &'static mut StyleSheet<'static, 'static, AtRule>, - >(stylesheet) - }, - tx: channel.0.clone(), - }; - - tsfn.call(message, ThreadsafeFunctionCallMode::Blocking); - channel.1.recv().expect("recv error").map(|_| ()) - }) - } - }), - ); - - deferred.resolve(move |env| match res { - Ok(v) => v.into_js(env), - Err(err) => Err(err.into_js_error(env, None)?), - }); - }); - - Ok(promise) - } +#[js_function(1)] +pub fn bundle(ctx: CallContext) -> napi::Result { + lightningcss_napi::bundle(ctx) } -#[cfg(target_arch = "wasm32")] -mod bundle { - use super::*; - use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref}; - use std::cell::UnsafeCell; - - #[js_function(1)] - pub fn bundle(ctx: CallContext) -> napi::Result { - use transformer::JsVisitor; - - let opts = ctx.get::(0)?; - let mut visitor = if let Ok(visitor) = opts.get_named_property::("visitor") { - Some(JsVisitor::new(*ctx.env, visitor)) - } else { - None - }; - - let resolver = opts.get_named_property::("resolver")?; - let read = resolver.get_named_property::("read")?; - let resolve = if resolver.has_named_property("resolve")? { - let resolve = resolver.get_named_property::("resolve")?; - Some(ctx.env.create_reference(resolve)?) - } else { - None - }; - let config: BundleConfig = ctx.env.from_js_value(opts)?; - - let provider = JsSourceProvider { - env: ctx.env.clone(), - resolve, - read: ctx.env.create_reference(read)?, - inputs: UnsafeCell::new(Vec::new()), - }; - - // This is pretty silly, but works around a rust limitation that you cannot - // explicitly annotate lifetime bounds on closures. - fn annotate<'i, 'o, F>(f: F) -> F - where - F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, - { - f - } - - let res = compile_bundle( - &provider, - &config, - visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), - ); - - match res { - Ok(res) => res.into_js(*ctx.env), - Err(err) => Err(err.into_js_error(*ctx.env, None)?), - } - } - - struct JsSourceProvider { - env: Env, - resolve: Option>, - read: Ref<()>, - inputs: UnsafeCell>, - } - - impl Drop for JsSourceProvider { - fn drop(&mut self) { - if let Some(resolve) = &mut self.resolve { - drop(resolve.unref(self.env)); - } - drop(self.read.unref(self.env)); - } - } - - unsafe impl Sync for JsSourceProvider {} - unsafe impl Send for JsSourceProvider {} - - // This relies on Binaryen's Asyncify transform to allow Rust to call async JS functions from sync code. - // See the comments in async.mjs for more details about how this works. - extern "C" { - fn await_promise_sync( - promise: napi::sys::napi_value, - result: *mut napi::sys::napi_value, - error: *mut napi::sys::napi_value, - ); - } - - fn get_result(env: Env, mut value: JsUnknown) -> napi::Result { - if value.is_promise()? { - let mut result = std::ptr::null_mut(); - let mut error = std::ptr::null_mut(); - unsafe { await_promise_sync(value.raw(), &mut result, &mut error) }; - if !error.is_null() { - let error = unsafe { JsUnknown::from_raw(env.raw(), error)? }; - return Err(napi::Error::from(error)); - } - if result.is_null() { - return Err(napi::Error::new(napi::Status::GenericFailure, "No result".into())); - } - - value = unsafe { JsUnknown::from_raw(env.raw(), result)? }; - } - - value.try_into() - } - - impl SourceProvider for JsSourceProvider { - type Error = napi::Error; - - fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> { - let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?; - let file = self.env.create_string(file.to_str().unwrap())?; - let mut source: JsUnknown = read.call(None, &[file])?; - let source = get_result(self.env, source)?.into_utf8()?.into_owned()?; - - // cache the result - let ptr = Box::into_raw(Box::new(source)); - let inputs = unsafe { &mut *self.inputs.get() }; - inputs.push(ptr); - // SAFETY: this is safe because the pointer is not dropped - // until the JsSourceProvider is, and we never remove from the - // list of pointers stored in the vector. - Ok(unsafe { &*ptr }) - } - - fn resolve(&self, specifier: &str, originating_file: &Path) -> Result { - if let Some(resolve) = &self.resolve { - let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?; - let specifier = self.env.create_string(specifier)?; - let originating_file = self.env.create_string(originating_file.to_str().unwrap())?; - let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?; - let result = get_result(self.env, result)?.into_utf8()?; - Ok(PathBuf::from_str(result.as_str()?).unwrap()) - } else { - Ok(originating_file.with_file_name(specifier)) - } - } - } +#[cfg(not(target_arch = "wasm32"))] +#[js_function(1)] +pub fn bundle_async(ctx: CallContext) -> napi::Result { + lightningcss_napi::bundle_async(ctx) } #[cfg_attr(not(target_arch = "wasm32"), module_exports)] fn init(mut exports: JsObject) -> napi::Result<()> { exports.create_named_method("transform", transform)?; exports.create_named_method("transformStyleAttribute", transform_style_attribute)?; - exports.create_named_method("bundle", bundle::bundle)?; - + exports.create_named_method("bundle", bundle)?; #[cfg(not(target_arch = "wasm32"))] { - exports.create_named_method("bundleAsync", bundle::bundle_async)?; + exports.create_named_method("bundleAsync", bundle_async)?; } Ok(()) @@ -603,631 +71,3 @@ pub extern "C" fn napi_wasm_malloc(size: usize) -> *mut u8 { std::process::abort(); } - -// --------------------------------------------- - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Config { - pub filename: Option, - pub project_root: Option, - #[serde(with = "serde_bytes")] - pub code: Vec, - pub targets: Option, - #[serde(default)] - pub include: u32, - #[serde(default)] - pub exclude: u32, - pub minify: Option, - pub source_map: Option, - pub input_source_map: Option, - pub drafts: Option, - pub non_standard: Option, - pub css_modules: Option, - pub analyze_dependencies: Option, - pub pseudo_classes: Option, - pub unused_symbols: Option>, - pub error_recovery: Option, - pub custom_at_rules: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum AnalyzeDependenciesOption { - Bool(bool), - Config(AnalyzeDependenciesConfig), -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct AnalyzeDependenciesConfig { - preserve_imports: bool, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum CssModulesOption { - Bool(bool), - Config(CssModulesConfig), -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CssModulesConfig { - pattern: Option, - dashed_idents: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct BundleConfig { - pub filename: String, - pub project_root: Option, - pub targets: Option, - #[serde(default)] - pub include: u32, - #[serde(default)] - pub exclude: u32, - pub minify: Option, - pub source_map: Option, - pub drafts: Option, - pub non_standard: Option, - pub css_modules: Option, - pub analyze_dependencies: Option, - pub pseudo_classes: Option, - pub unused_symbols: Option>, - pub error_recovery: Option, - pub custom_at_rules: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct OwnedPseudoClasses { - pub hover: Option, - pub active: Option, - pub focus: Option, - pub focus_visible: Option, - pub focus_within: Option, -} - -impl<'a> Into> for &'a OwnedPseudoClasses { - fn into(self) -> PseudoClasses<'a> { - PseudoClasses { - hover: self.hover.as_deref(), - active: self.active.as_deref(), - focus: self.focus.as_deref(), - focus_visible: self.focus_visible.as_deref(), - focus_within: self.focus_within.as_deref(), - } - } -} - -#[derive(Serialize, Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -struct Drafts { - #[serde(default)] - custom_media: bool, -} - -#[derive(Serialize, Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -struct NonStandard { - #[serde(default)] - deep_selector_combinator: bool, -} - -fn compile<'i>( - code: &'i str, - config: &Config, - visitor: &mut Option, -) -> Result, CompileError<'i, napi::Error>> { - let drafts = config.drafts.as_ref(); - let non_standard = config.non_standard.as_ref(); - let warnings = Some(Arc::new(RwLock::new(Vec::new()))); - - let filename = config.filename.clone().unwrap_or_default(); - let project_root = config.project_root.as_ref().map(|p| p.as_ref()); - let mut source_map = if config.source_map.unwrap_or_default() { - let mut sm = SourceMap::new(project_root.unwrap_or("/")); - sm.add_source(&filename); - sm.set_source_content(0, code)?; - Some(sm) - } else { - None - }; - - let res = { - let mut flags = ParserFlags::empty(); - flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); - flags.set( - ParserFlags::DEEP_SELECTOR_COMBINATOR, - matches!(non_standard, Some(v) if v.deep_selector_combinator), - ); - - let mut stylesheet = StyleSheet::parse_with( - &code, - ParserOptions { - filename: filename.clone(), - flags, - css_modules: if let Some(css_modules) = &config.css_modules { - match css_modules { - CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), - CssModulesOption::Bool(false) => None, - CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { - pattern: if let Some(pattern) = c.pattern.as_ref() { - match lightningcss::css_modules::Pattern::parse(pattern) { - Ok(p) => p, - Err(e) => return Err(CompileError::PatternError(e)), - } - } else { - Default::default() - }, - dashed_idents: c.dashed_idents.unwrap_or_default(), - }), - } - } else { - None - }, - source_index: 0, - error_recovery: config.error_recovery.unwrap_or_default(), - warnings: warnings.clone(), - }, - &mut CustomAtRuleParser { - configs: config.custom_at_rules.clone().unwrap_or_default(), - }, - )?; - - if let Some(visitor) = visitor.as_mut() { - stylesheet.visit(visitor).map_err(CompileError::JsError)?; - } - - let targets = Targets { - browsers: config.targets, - include: Features::from_bits_truncate(config.include), - exclude: Features::from_bits_truncate(config.exclude), - }; - - stylesheet.minify(MinifyOptions { - targets, - unused_symbols: config.unused_symbols.clone().unwrap_or_default(), - })?; - - stylesheet.to_css(PrinterOptions { - minify: config.minify.unwrap_or_default(), - source_map: source_map.as_mut(), - project_root, - targets, - analyze_dependencies: if let Some(d) = &config.analyze_dependencies { - match d { - AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), - AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { - remove_imports: !c.preserve_imports, - }), - _ => None, - } - } else { - None - }, - pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), - })? - }; - - let map = if let Some(mut source_map) = source_map { - if let Some(input_source_map) = &config.input_source_map { - if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) { - let _ = source_map.extends(&mut sm); - } - } - - source_map.to_json(None).ok() - } else { - None - }; - - Ok(TransformResult { - code: res.code.into_bytes(), - map: map.map(|m| m.into_bytes()), - exports: res.exports, - references: res.references, - dependencies: res.dependencies, - warnings: warnings.map_or(Vec::new(), |w| { - Arc::try_unwrap(w) - .unwrap() - .into_inner() - .unwrap() - .into_iter() - .map(|w| w.into()) - .collect() - }), - }) -} - -fn compile_bundle< - 'i, - 'o, - P: SourceProvider, - F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, ->( - fs: &'i P, - config: &'o BundleConfig, - visit: Option, -) -> Result, CompileError<'i, P::Error>> { - let project_root = config.project_root.as_ref().map(|p| p.as_ref()); - let mut source_map = if config.source_map.unwrap_or_default() { - Some(SourceMap::new(project_root.unwrap_or("/"))) - } else { - None - }; - let warnings = Some(Arc::new(RwLock::new(Vec::new()))); - - let res = { - let drafts = config.drafts.as_ref(); - let non_standard = config.non_standard.as_ref(); - let mut flags = ParserFlags::empty(); - flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media)); - flags.set( - ParserFlags::DEEP_SELECTOR_COMBINATOR, - matches!(non_standard, Some(v) if v.deep_selector_combinator), - ); - - let parser_options = ParserOptions { - flags, - css_modules: if let Some(css_modules) = &config.css_modules { - match css_modules { - CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), - CssModulesOption::Bool(false) => None, - CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { - pattern: if let Some(pattern) = c.pattern.as_ref() { - match lightningcss::css_modules::Pattern::parse(pattern) { - Ok(p) => p, - Err(e) => return Err(CompileError::PatternError(e)), - } - } else { - Default::default() - }, - dashed_idents: c.dashed_idents.unwrap_or_default(), - }), - } - } else { - None - }, - error_recovery: config.error_recovery.unwrap_or_default(), - warnings: warnings.clone(), - filename: String::new(), - source_index: 0, - }; - - let mut at_rule_parser = CustomAtRuleParser { - configs: config.custom_at_rules.clone().unwrap_or_default(), - }; - - let mut bundler = - Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser); - let mut stylesheet = bundler.bundle(Path::new(&config.filename))?; - - if let Some(visit) = visit { - visit(&mut stylesheet).map_err(CompileError::JsError)?; - } - - let targets = Targets { - browsers: config.targets, - include: Features::from_bits_truncate(config.include), - exclude: Features::from_bits_truncate(config.exclude), - }; - - stylesheet.minify(MinifyOptions { - targets, - unused_symbols: config.unused_symbols.clone().unwrap_or_default(), - })?; - - stylesheet.to_css(PrinterOptions { - minify: config.minify.unwrap_or_default(), - source_map: source_map.as_mut(), - project_root, - targets, - analyze_dependencies: if let Some(d) = &config.analyze_dependencies { - match d { - AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }), - AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions { - remove_imports: !c.preserve_imports, - }), - _ => None, - } - } else { - None - }, - pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), - })? - }; - - let map = if let Some(source_map) = &mut source_map { - source_map.to_json(None).ok() - } else { - None - }; - - Ok(TransformResult { - code: res.code.into_bytes(), - map: map.map(|m| m.into_bytes()), - exports: res.exports, - references: res.references, - dependencies: res.dependencies, - warnings: warnings.map_or(Vec::new(), |w| { - Arc::try_unwrap(w) - .unwrap() - .into_inner() - .unwrap() - .into_iter() - .map(|w| w.into()) - .collect() - }), - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct AttrConfig { - pub filename: Option, - #[serde(with = "serde_bytes")] - pub code: Vec, - pub targets: Option, - #[serde(default)] - pub include: u32, - #[serde(default)] - pub exclude: u32, - #[serde(default)] - pub minify: bool, - #[serde(default)] - pub analyze_dependencies: bool, - #[serde(default)] - pub error_recovery: bool, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct AttrResult<'i> { - #[serde(with = "serde_bytes")] - code: Vec, - dependencies: Option>, - warnings: Vec>, -} - -impl<'i> AttrResult<'i> { - fn into_js(self, ctx: CallContext) -> napi::Result { - // Manually construct buffers so we avoid a copy and work around - // https://github.com/napi-rs/napi-rs/issues/1124. - let mut obj = ctx.env.create_object()?; - let buf = ctx.env.create_buffer_with_data(self.code)?; - obj.set_named_property("code", buf.into_raw())?; - obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?; - obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?; - Ok(obj.into_unknown()) - } -} - -fn compile_attr<'i>( - code: &'i str, - config: &AttrConfig, - visitor: &mut Option, -) -> Result, CompileError<'i, napi::Error>> { - let warnings = if config.error_recovery { - Some(Arc::new(RwLock::new(Vec::new()))) - } else { - None - }; - let res = { - let filename = config.filename.clone().unwrap_or_default(); - let mut attr = StyleAttribute::parse( - &code, - ParserOptions { - filename, - error_recovery: config.error_recovery, - warnings: warnings.clone(), - ..ParserOptions::default() - }, - )?; - - if let Some(visitor) = visitor.as_mut() { - attr.visit(visitor).unwrap(); - } - - let targets = Targets { - browsers: config.targets, - include: Features::from_bits_truncate(config.include), - exclude: Features::from_bits_truncate(config.exclude), - }; - - attr.minify(MinifyOptions { - targets, - ..MinifyOptions::default() - }); - attr.to_css(PrinterOptions { - minify: config.minify, - source_map: None, - project_root: None, - targets, - analyze_dependencies: if config.analyze_dependencies { - Some(DependencyOptions::default()) - } else { - None - }, - pseudo_classes: None, - })? - }; - Ok(AttrResult { - code: res.code.into_bytes(), - dependencies: res.dependencies, - warnings: warnings.map_or(Vec::new(), |w| { - Arc::try_unwrap(w) - .unwrap() - .into_inner() - .unwrap() - .into_iter() - .map(|w| w.into()) - .collect() - }), - }) -} - -enum CompileError<'i, E: std::error::Error> { - ParseError(Error>), - MinifyError(Error), - PrinterError(Error), - SourceMapError(parcel_sourcemap::SourceMapError), - BundleError(Error>), - PatternError(PatternParseError), - JsError(napi::Error), -} - -impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - CompileError::ParseError(err) => err.kind.fmt(f), - CompileError::MinifyError(err) => err.kind.fmt(f), - CompileError::PrinterError(err) => err.kind.fmt(f), - CompileError::BundleError(err) => err.kind.fmt(f), - CompileError::PatternError(err) => err.fmt(f), - CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this - CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f), - } - } -} - -impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> { - fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result { - let reason = self.to_string(); - let data = match &self { - CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?, - CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?, - CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?, - CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?, - _ => env.get_null()?.into_unknown(), - }; - - let (js_error, loc) = match self { - CompileError::BundleError(Error { - loc, - kind: BundleErrorKind::ResolverError(e), - }) => { - // Add location info to existing JS error if available. - (e.into_js_error(env)?, loc) - } - CompileError::ParseError(Error { loc, .. }) - | CompileError::PrinterError(Error { loc, .. }) - | CompileError::MinifyError(Error { loc, .. }) - | CompileError::BundleError(Error { loc, .. }) => { - // Generate an error with location information. - let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; - let reason = env.create_string_from_std(reason)?; - let obj = syntax_error.new_instance(&[reason])?; - (obj.into_unknown(), loc) - } - _ => return Ok(self.into()), - }; - - if js_error.get_type()? == napi::ValueType::Object { - let mut obj: JsObject = unsafe { js_error.cast() }; - if let Some(loc) = loc { - let line = env.create_int32((loc.line + 1) as i32)?; - let col = env.create_int32(loc.column as i32)?; - let filename = env.create_string_from_std(loc.filename)?; - obj.set_named_property("fileName", filename)?; - if let Some(code) = code { - let source = env.create_string(code)?; - obj.set_named_property("source", source)?; - } - let mut loc = env.create_object()?; - loc.set_named_property("line", line)?; - loc.set_named_property("column", col)?; - obj.set_named_property("loc", loc)?; - } - obj.set_named_property("data", data)?; - Ok(obj.into_unknown().into()) - } else { - Ok(js_error.into()) - } - } -} - -trait IntoJsError { - fn into_js_error(self, env: Env) -> napi::Result; -} - -impl IntoJsError for std::io::Error { - fn into_js_error(self, env: Env) -> napi::Result { - let reason = self.to_string(); - let syntax_error = env.get_global()?.get_named_property::("SyntaxError")?; - let reason = env.create_string_from_std(reason)?; - let obj = syntax_error.new_instance(&[reason])?; - Ok(obj.into_unknown()) - } -} - -impl IntoJsError for napi::Error { - fn into_js_error(self, env: Env) -> napi::Result { - unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) } - } -} - -impl<'i, E: std::error::Error> From>> for CompileError<'i, E> { - fn from(e: Error>) -> CompileError<'i, E> { - CompileError::ParseError(e) - } -} - -impl<'i, E: std::error::Error> From> for CompileError<'i, E> { - fn from(err: Error) -> CompileError<'i, E> { - CompileError::MinifyError(err) - } -} - -impl<'i, E: std::error::Error> From> for CompileError<'i, E> { - fn from(err: Error) -> CompileError<'i, E> { - CompileError::PrinterError(err) - } -} - -impl<'i, E: std::error::Error> From for CompileError<'i, E> { - fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> { - CompileError::SourceMapError(e) - } -} - -impl<'i, E: std::error::Error> From>> for CompileError<'i, E> { - fn from(e: Error>) -> CompileError<'i, E> { - CompileError::BundleError(e) - } -} - -impl<'i, E: std::error::Error> From> for napi::Error { - fn from(e: CompileError<'i, E>) -> napi::Error { - match e { - CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()), - CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()), - CompileError::JsError(e) => e, - _ => napi::Error::new(napi::Status::GenericFailure, e.to_string()), - } - } -} - -#[derive(Serialize)] -struct Warning<'i> { - message: String, - #[serde(flatten)] - data: ParserError<'i>, - loc: Option, -} - -impl<'i> From>> for Warning<'i> { - fn from(mut e: Error>) -> Self { - // Convert to 1-based line numbers. - if let Some(loc) = &mut e.loc { - loc.line += 1; - } - Warning { - message: e.kind.to_string(), - data: e.kind, - loc: e.loc, - } - } -}