#[cfg(target_os = "macos")] #[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_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))?), } } #[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))?), } } #[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) } } #[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_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)?; #[cfg(not(target_arch = "wasm32"))] { exports.create_named_method("bundleAsync", bundle::bundle_async)?; } Ok(()) } #[cfg(target_arch = "wasm32")] #[no_mangle] pub unsafe fn napi_register_wasm_v1(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) { use napi::{Env, JsObject, NapiValue}; let env = Env::from_raw(raw_env); let exports = JsObject::from_raw_unchecked(raw_env, raw_exports); init(exports); } #[cfg(target_arch = "wasm32")] #[no_mangle] pub extern "C" fn napi_wasm_malloc(size: usize) -> *mut u8 { use std::alloc::{alloc, Layout}; use std::mem; let align = mem::align_of::(); if let Ok(layout) = Layout::from_size_align(size, align) { unsafe { if layout.size() > 0 { let ptr = alloc(layout); if !ptr.is_null() { return ptr; } } else { return align as *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, } } }