Skip to main content

lightningcss_napi/
lib.rs

1#[cfg(feature = "bundler")]
2use at_rule_parser::AtRule;
3use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser};
4use lightningcss::bundler::BundleErrorKind;
5#[cfg(feature = "bundler")]
6use lightningcss::bundler::{Bundler, SourceProvider};
7use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError};
8use lightningcss::dependencies::{Dependency, DependencyOptions};
9use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind};
10use lightningcss::stylesheet::{
11  MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet,
12};
13use lightningcss::targets::{Browsers, Features, Targets};
14use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
15use napi::{CallContext, Env, JsObject, JsUnknown};
16use parcel_sourcemap::SourceMap;
17use serde::{Deserialize, Serialize};
18use std::collections::{HashMap, HashSet};
19use std::sync::{Arc, RwLock};
20
21mod at_rule_parser;
22#[cfg(feature = "bundler")]
23#[cfg(not(target_arch = "wasm32"))]
24mod threadsafe_function;
25#[cfg(feature = "visitor")]
26mod transformer;
27mod utils;
28
29#[cfg(feature = "visitor")]
30use transformer::JsVisitor;
31
32#[cfg(not(feature = "visitor"))]
33struct JsVisitor;
34
35#[cfg(feature = "visitor")]
36use lightningcss::visitor::Visit;
37
38use utils::get_named_property;
39
40#[derive(Serialize)]
41#[serde(rename_all = "camelCase")]
42struct TransformResult<'i> {
43  #[serde(with = "serde_bytes")]
44  code: Vec<u8>,
45  #[serde(with = "serde_bytes")]
46  map: Option<Vec<u8>>,
47  exports: Option<CssModuleExports>,
48  references: Option<CssModuleReferences>,
49  dependencies: Option<Vec<Dependency>>,
50  warnings: Vec<Warning<'i>>,
51}
52
53impl<'i> TransformResult<'i> {
54  fn into_js(self, env: Env) -> napi::Result<JsUnknown> {
55    // Manually construct buffers so we avoid a copy and work around
56    // https://github.com/napi-rs/napi-rs/issues/1124.
57    let mut obj = env.create_object()?;
58    let buf = env.create_buffer_with_data(self.code)?;
59    obj.set_named_property("code", buf.into_raw())?;
60    obj.set_named_property(
61      "map",
62      if let Some(map) = self.map {
63        let buf = env.create_buffer_with_data(map)?;
64        buf.into_raw().into_unknown()
65      } else {
66        env.get_null()?.into_unknown()
67      },
68    )?;
69    obj.set_named_property("exports", env.to_js_value(&self.exports)?)?;
70    obj.set_named_property("references", env.to_js_value(&self.references)?)?;
71    obj.set_named_property("dependencies", env.to_js_value(&self.dependencies)?)?;
72    obj.set_named_property("warnings", env.to_js_value(&self.warnings)?)?;
73    Ok(obj.into_unknown())
74  }
75}
76
77#[cfg(feature = "visitor")]
78fn get_visitor(env: Env, opts: &JsObject) -> Option<JsVisitor> {
79  if let Ok(visitor) = get_named_property::<JsObject>(opts, "visitor") {
80    Some(JsVisitor::new(env, visitor))
81  } else {
82    None
83  }
84}
85
86#[cfg(not(feature = "visitor"))]
87fn get_visitor(_env: Env, _opts: &JsObject) -> Option<JsVisitor> {
88  None
89}
90
91pub fn transform(ctx: CallContext) -> napi::Result<JsUnknown> {
92  let opts = ctx.get::<JsObject>(0)?;
93  let mut visitor = get_visitor(*ctx.env, &opts);
94
95  let config: Config = ctx.env.from_js_value(opts)?;
96  let code = unsafe { std::str::from_utf8_unchecked(&config.code) };
97  let res = compile(code, &config, &mut visitor);
98
99  match res {
100    Ok(res) => res.into_js(*ctx.env),
101    Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?),
102  }
103}
104
105pub fn transform_style_attribute(ctx: CallContext) -> napi::Result<JsUnknown> {
106  let opts = ctx.get::<JsObject>(0)?;
107  let mut visitor = get_visitor(*ctx.env, &opts);
108
109  let config: AttrConfig = ctx.env.from_js_value(opts)?;
110  let code = unsafe { std::str::from_utf8_unchecked(&config.code) };
111  let res = compile_attr(code, &config, &mut visitor);
112
113  match res {
114    Ok(res) => res.into_js(ctx),
115    Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?),
116  }
117}
118
119#[cfg(feature = "bundler")]
120#[cfg(not(target_arch = "wasm32"))]
121mod bundle {
122  use super::*;
123  use crossbeam_channel::{self, Receiver, Sender};
124  use lightningcss::bundler::{FileProvider, ResolveResult};
125  use napi::{Env, JsBoolean, JsFunction, JsString, NapiRaw};
126  use std::path::{Path, PathBuf};
127  use std::str::FromStr;
128  use std::sync::Mutex;
129  use threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode};
130
131  pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
132    let opts = ctx.get::<JsObject>(0)?;
133    let mut visitor = get_visitor(*ctx.env, &opts);
134
135    let config: BundleConfig = ctx.env.from_js_value(opts)?;
136    let fs = FileProvider::new();
137
138    // This is pretty silly, but works around a rust limitation that you cannot
139    // explicitly annotate lifetime bounds on closures.
140    fn annotate<'i, F>(f: F) -> F
141    where
142      F: FnOnce(&mut StyleSheet<'i, AtRule<'i>>) -> napi::Result<()>,
143    {
144      f
145    }
146
147    let res = compile_bundle(
148      &fs,
149      &config,
150      visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))),
151    );
152
153    match res {
154      Ok(res) => res.into_js(*ctx.env),
155      Err(err) => Err(err.into_js_error(*ctx.env, None)?),
156    }
157  }
158
159  // A SourceProvider which calls JavaScript functions to resolve and read files.
160  struct JsSourceProvider {
161    resolve: Option<ThreadsafeFunction<ResolveMessage>>,
162    read: Option<ThreadsafeFunction<ReadMessage>>,
163    inputs: Mutex<Vec<*mut String>>,
164  }
165
166  unsafe impl Sync for JsSourceProvider {}
167  unsafe impl Send for JsSourceProvider {}
168
169  // Allocate a single channel per thread to communicate with the JS thread.
170  thread_local! {
171    static CHANNEL: (Sender<napi::Result<String>>, Receiver<napi::Result<String>>) = crossbeam_channel::unbounded();
172    static RESOLVER_CHANNEL: (Sender<napi::Result<ResolveResult>>, Receiver<napi::Result<ResolveResult>>) = crossbeam_channel::unbounded();
173  }
174
175  impl SourceProvider for JsSourceProvider {
176    type Error = napi::Error;
177
178    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
179      let source = if let Some(read) = &self.read {
180        CHANNEL.with(|channel| {
181          let message = ReadMessage {
182            file: file.to_str().unwrap().to_owned(),
183            tx: channel.0.clone(),
184          };
185
186          read.call(message, ThreadsafeFunctionCallMode::Blocking);
187          channel.1.recv().unwrap()
188        })
189      } else {
190        Ok(std::fs::read_to_string(file)?)
191      };
192
193      match source {
194        Ok(source) => {
195          // cache the result
196          let ptr = Box::into_raw(Box::new(source));
197          self.inputs.lock().unwrap().push(ptr);
198          // SAFETY: this is safe because the pointer is not dropped
199          // until the JsSourceProvider is, and we never remove from the
200          // list of pointers stored in the vector.
201          Ok(unsafe { &*ptr })
202        }
203        Err(e) => Err(e),
204      }
205    }
206
207    fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<ResolveResult, Self::Error> {
208      if let Some(resolve) = &self.resolve {
209        return RESOLVER_CHANNEL.with(|channel| {
210          let message = ResolveMessage {
211            specifier: specifier.to_owned(),
212            originating_file: originating_file.to_str().unwrap().to_owned(),
213            tx: channel.0.clone(),
214          };
215
216          resolve.call(message, ThreadsafeFunctionCallMode::Blocking);
217          channel.1.recv().unwrap()
218        });
219      }
220
221      Ok(originating_file.with_file_name(specifier).into())
222    }
223  }
224
225  struct ResolveMessage {
226    specifier: String,
227    originating_file: String,
228    tx: Sender<napi::Result<ResolveResult>>,
229  }
230
231  struct ReadMessage {
232    file: String,
233    tx: Sender<napi::Result<String>>,
234  }
235
236  struct VisitMessage {
237    stylesheet: &'static mut StyleSheet<'static, AtRule<'static>>,
238    tx: Sender<napi::Result<String>>,
239  }
240
241  fn await_promise<T, Cb>(env: Env, result: JsUnknown, tx: Sender<napi::Result<T>>, parse: Cb) -> napi::Result<()>
242  where
243    T: 'static,
244    Cb: 'static + Fn(JsUnknown) -> Result<T, napi::Error>,
245  {
246    // If the result is a promise, wait for it to resolve, and send the result to the channel.
247    // Otherwise, send the result immediately.
248    if result.is_promise()? {
249      let result: JsObject = result.try_into()?;
250      let then: JsFunction = get_named_property(&result, "then")?;
251      let tx2 = tx.clone();
252      let cb = env.create_function_from_closure("callback", move |ctx| {
253        let res = parse(ctx.get::<JsUnknown>(0)?)?;
254        tx.send(Ok(res)).unwrap();
255        ctx.env.get_undefined()
256      })?;
257      let eb = env.create_function_from_closure("error_callback", move |ctx| {
258        let res = ctx.get::<JsUnknown>(0)?;
259        tx2.send(Err(napi::Error::from(res))).unwrap();
260        ctx.env.get_undefined()
261      })?;
262      then.call(Some(&result), &[cb, eb])?;
263    } else {
264      let result = parse(result)?;
265      tx.send(Ok(result)).unwrap();
266    }
267
268    Ok(())
269  }
270
271  fn resolve_on_js_thread(ctx: ThreadSafeCallContext<ResolveMessage>) -> napi::Result<()> {
272    let specifier = ctx.env.create_string(&ctx.value.specifier)?;
273    let originating_file = ctx.env.create_string(&ctx.value.originating_file)?;
274    let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?;
275    await_promise(ctx.env, result, ctx.value.tx, move |unknown| {
276      ctx.env.from_js_value(unknown)
277    })
278  }
279
280  fn handle_error<T>(tx: Sender<napi::Result<T>>, res: napi::Result<()>) -> napi::Result<()> {
281    match res {
282      Ok(_) => Ok(()),
283      Err(e) => {
284        tx.send(Err(e)).expect("send error");
285        Ok(())
286      }
287    }
288  }
289
290  fn resolve_on_js_thread_wrapper(ctx: ThreadSafeCallContext<ResolveMessage>) -> napi::Result<()> {
291    let tx = ctx.value.tx.clone();
292    handle_error(tx, resolve_on_js_thread(ctx))
293  }
294
295  fn read_on_js_thread(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
296    let file = ctx.env.create_string(&ctx.value.file)?;
297    let result = ctx.callback.unwrap().call(None, &[file])?;
298    await_promise(ctx.env, result, ctx.value.tx, |unknown| {
299      JsString::try_from(unknown)?.into_utf8()?.into_owned()
300    })
301  }
302
303  fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
304    let tx = ctx.value.tx.clone();
305    handle_error(tx, read_on_js_thread(ctx))
306  }
307
308  pub fn bundle_async(ctx: CallContext) -> napi::Result<JsObject> {
309    let opts = ctx.get::<JsObject>(0)?;
310    let visitor = get_visitor(*ctx.env, &opts);
311
312    let config: BundleConfig = ctx.env.from_js_value(&opts)?;
313
314    if let Ok(resolver) = get_named_property::<JsObject>(&opts, "resolver") {
315      let read = if resolver.has_named_property("read")? {
316        let read = get_named_property::<JsFunction>(&resolver, "read")?;
317        Some(ThreadsafeFunction::create(
318          ctx.env.raw(),
319          unsafe { read.raw() },
320          0,
321          read_on_js_thread_wrapper,
322        )?)
323      } else {
324        None
325      };
326
327      let resolve = if resolver.has_named_property("resolve")? {
328        let resolve = get_named_property::<JsFunction>(&resolver, "resolve")?;
329        Some(ThreadsafeFunction::create(
330          ctx.env.raw(),
331          unsafe { resolve.raw() },
332          0,
333          resolve_on_js_thread_wrapper,
334        )?)
335      } else {
336        None
337      };
338
339      let provider = JsSourceProvider {
340        resolve,
341        read,
342        inputs: Mutex::new(Vec::new()),
343      };
344
345      run_bundle_task(provider, config, visitor, *ctx.env)
346    } else {
347      let provider = FileProvider::new();
348      run_bundle_task(provider, config, visitor, *ctx.env)
349    }
350  }
351
352  // Runs bundling on a background thread managed by rayon. This is similar to AsyncTask from napi-rs, however,
353  // because we call back into the JS thread, which might call other tasks in the node threadpool (e.g. fs.readFile),
354  // we may end up deadlocking if the number of rayon threads exceeds node's threadpool size. Therefore, we must
355  // run bundling from a thread not managed by Node.
356  fn run_bundle_task<P: 'static + SourceProvider>(
357    provider: P,
358    config: BundleConfig,
359    visitor: Option<JsVisitor>,
360    env: Env,
361  ) -> napi::Result<JsObject>
362  where
363    P::Error: IntoJsError,
364  {
365    let (deferred, promise) = env.create_deferred()?;
366
367    let tsfn = if let Some(mut visitor) = visitor {
368      Some(ThreadsafeFunction::create(
369        env.raw(),
370        std::ptr::null_mut(),
371        0,
372        move |ctx: ThreadSafeCallContext<VisitMessage>| {
373          if let Err(err) = ctx.value.stylesheet.visit(&mut visitor) {
374            ctx.value.tx.send(Err(err)).expect("send error");
375            return Ok(());
376          }
377          ctx.value.tx.send(Ok(Default::default())).expect("send error");
378          Ok(())
379        },
380      )?)
381    } else {
382      None
383    };
384
385    // Run bundling task in rayon threadpool.
386    rayon::spawn(move || {
387      let res = compile_bundle(
388        unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) },
389        &config,
390        tsfn.map(move |tsfn| {
391          move |stylesheet: &mut StyleSheet<AtRule>| {
392            CHANNEL.with(|channel| {
393              let message = VisitMessage {
394                // SAFETY: we immediately lock the thread until we get a response,
395                // so stylesheet cannot be dropped in that time.
396                stylesheet: unsafe {
397                  std::mem::transmute::<&'_ mut StyleSheet<'_, AtRule>, &'static mut StyleSheet<'static, AtRule>>(
398                    stylesheet,
399                  )
400                },
401                tx: channel.0.clone(),
402              };
403
404              tsfn.call(message, ThreadsafeFunctionCallMode::Blocking);
405              channel.1.recv().expect("recv error").map(|_| ())
406            })
407          }
408        }),
409      );
410
411      deferred.resolve(move |env| match res {
412        Ok(v) => v.into_js(env),
413        Err(err) => Err(err.into_js_error(env, None)?),
414      });
415    });
416
417    Ok(promise)
418  }
419}
420
421#[cfg(feature = "bundler")]
422#[cfg(target_arch = "wasm32")]
423mod bundle {
424  use super::*;
425  use lightningcss::bundler::ResolveResult;
426  use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref};
427  use std::cell::UnsafeCell;
428  use std::path::Path;
429
430  pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
431    let opts = ctx.get::<JsObject>(0)?;
432    let mut visitor = get_visitor(*ctx.env, &opts);
433
434    let resolver = get_named_property::<JsObject>(&opts, "resolver")?;
435    let read = get_named_property::<JsFunction>(&resolver, "read")?;
436    let resolve = if resolver.has_named_property("resolve")? {
437      let resolve = get_named_property::<JsFunction>(&resolver, "resolve")?;
438      Some(ctx.env.create_reference(resolve)?)
439    } else {
440      None
441    };
442    let config: BundleConfig = ctx.env.from_js_value(opts)?;
443
444    let provider = JsSourceProvider {
445      env: ctx.env.clone(),
446      resolve,
447      read: ctx.env.create_reference(read)?,
448      inputs: UnsafeCell::new(Vec::new()),
449    };
450
451    // This is pretty silly, but works around a rust limitation that you cannot
452    // explicitly annotate lifetime bounds on closures.
453    fn annotate<'i, F>(f: F) -> F
454    where
455      F: FnOnce(&mut StyleSheet<'i, AtRule<'i>>) -> napi::Result<()>,
456    {
457      f
458    }
459
460    let res = compile_bundle(
461      &provider,
462      &config,
463      visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))),
464    );
465
466    match res {
467      Ok(res) => res.into_js(*ctx.env),
468      Err(err) => Err(err.into_js_error(*ctx.env, None)?),
469    }
470  }
471
472  struct JsSourceProvider {
473    env: Env,
474    resolve: Option<Ref<()>>,
475    read: Ref<()>,
476    inputs: UnsafeCell<Vec<*mut String>>,
477  }
478
479  impl Drop for JsSourceProvider {
480    fn drop(&mut self) {
481      if let Some(resolve) = &mut self.resolve {
482        drop(resolve.unref(self.env));
483      }
484      drop(self.read.unref(self.env));
485    }
486  }
487
488  unsafe impl Sync for JsSourceProvider {}
489  unsafe impl Send for JsSourceProvider {}
490
491  // This relies on Binaryen's Asyncify transform to allow Rust to call async JS functions from sync code.
492  // See the comments in async.mjs for more details about how this works.
493  extern "C" {
494    fn await_promise_sync(
495      promise: napi::sys::napi_value,
496      result: *mut napi::sys::napi_value,
497      error: *mut napi::sys::napi_value,
498    );
499  }
500
501  fn get_result(env: Env, mut value: JsUnknown) -> napi::Result<JsUnknown> {
502    if value.is_promise()? {
503      let mut result = std::ptr::null_mut();
504      let mut error = std::ptr::null_mut();
505      unsafe { await_promise_sync(value.raw(), &mut result, &mut error) };
506      if !error.is_null() {
507        let error = unsafe { JsUnknown::from_raw(env.raw(), error)? };
508        return Err(napi::Error::from(error));
509      }
510      if result.is_null() {
511        return Err(napi::Error::new(napi::Status::GenericFailure, "No result".to_string()));
512      }
513
514      value = unsafe { JsUnknown::from_raw(env.raw(), result)? };
515    }
516
517    Ok(value)
518  }
519
520  impl SourceProvider for JsSourceProvider {
521    type Error = napi::Error;
522
523    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
524      let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?;
525      let file = self.env.create_string(file.to_str().unwrap())?;
526      let source: JsUnknown = read.call(None, &[file])?;
527      let source = get_result(self.env, source)?;
528      let source: JsString = source.try_into()?;
529      let source = source.into_utf8()?.into_owned()?;
530
531      // cache the result
532      let ptr = Box::into_raw(Box::new(source));
533      let inputs = unsafe { &mut *self.inputs.get() };
534      inputs.push(ptr);
535      // SAFETY: this is safe because the pointer is not dropped
536      // until the JsSourceProvider is, and we never remove from the
537      // list of pointers stored in the vector.
538      Ok(unsafe { &*ptr })
539    }
540
541    fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<ResolveResult, Self::Error> {
542      if let Some(resolve) = &self.resolve {
543        let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?;
544        let specifier = self.env.create_string(specifier)?;
545        let originating_file = self.env.create_string(originating_file.to_str().unwrap())?;
546        let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?;
547        let result = get_result(self.env, result)?;
548        let result = self.env.from_js_value(result)?;
549        Ok(result)
550      } else {
551        Ok(ResolveResult::File(originating_file.with_file_name(specifier)))
552      }
553    }
554  }
555}
556
557#[cfg(feature = "bundler")]
558pub use bundle::*;
559
560// ---------------------------------------------
561
562#[derive(Debug, Deserialize)]
563#[serde(rename_all = "camelCase")]
564struct Config {
565  pub filename: Option<String>,
566  pub project_root: Option<String>,
567  #[serde(with = "serde_bytes")]
568  pub code: Vec<u8>,
569  pub targets: Option<Browsers>,
570  #[serde(default)]
571  pub include: u32,
572  #[serde(default)]
573  pub exclude: u32,
574  pub minify: Option<bool>,
575  pub source_map: Option<bool>,
576  pub input_source_map: Option<String>,
577  pub drafts: Option<Drafts>,
578  pub non_standard: Option<NonStandard>,
579  pub css_modules: Option<CssModulesOption>,
580  pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
581  pub pseudo_classes: Option<OwnedPseudoClasses>,
582  pub unused_symbols: Option<HashSet<String>>,
583  pub error_recovery: Option<bool>,
584  pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
585}
586
587#[derive(Debug, Deserialize)]
588#[serde(untagged)]
589enum AnalyzeDependenciesOption {
590  Bool(bool),
591  Config(AnalyzeDependenciesConfig),
592}
593
594#[derive(Debug, Deserialize)]
595#[serde(rename_all = "camelCase")]
596struct AnalyzeDependenciesConfig {
597  preserve_imports: bool,
598}
599
600#[derive(Debug, Deserialize)]
601#[serde(untagged)]
602enum CssModulesOption {
603  Bool(bool),
604  Config(CssModulesConfig),
605}
606
607#[derive(Debug, Deserialize)]
608#[serde(rename_all = "camelCase")]
609struct CssModulesConfig {
610  pattern: Option<String>,
611  dashed_idents: Option<bool>,
612  animation: Option<bool>,
613  container: Option<bool>,
614  grid: Option<bool>,
615  custom_idents: Option<bool>,
616  pure: Option<bool>,
617}
618
619#[cfg(feature = "bundler")]
620#[derive(Debug, Deserialize)]
621#[serde(rename_all = "camelCase")]
622struct BundleConfig {
623  pub filename: String,
624  pub project_root: Option<String>,
625  pub targets: Option<Browsers>,
626  #[serde(default)]
627  pub include: u32,
628  #[serde(default)]
629  pub exclude: u32,
630  pub minify: Option<bool>,
631  pub source_map: Option<bool>,
632  pub drafts: Option<Drafts>,
633  pub non_standard: Option<NonStandard>,
634  pub css_modules: Option<CssModulesOption>,
635  pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
636  pub pseudo_classes: Option<OwnedPseudoClasses>,
637  pub unused_symbols: Option<HashSet<String>>,
638  pub error_recovery: Option<bool>,
639  pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
640}
641
642#[derive(Debug, Deserialize)]
643#[serde(rename_all = "camelCase")]
644struct OwnedPseudoClasses {
645  pub hover: Option<String>,
646  pub active: Option<String>,
647  pub focus: Option<String>,
648  pub focus_visible: Option<String>,
649  pub focus_within: Option<String>,
650}
651
652impl<'a> Into<PseudoClasses<'a>> for &'a OwnedPseudoClasses {
653  fn into(self) -> PseudoClasses<'a> {
654    PseudoClasses {
655      hover: self.hover.as_deref(),
656      active: self.active.as_deref(),
657      focus: self.focus.as_deref(),
658      focus_visible: self.focus_visible.as_deref(),
659      focus_within: self.focus_within.as_deref(),
660    }
661  }
662}
663
664#[derive(Serialize, Debug, Deserialize, Default)]
665#[serde(rename_all = "camelCase")]
666struct Drafts {
667  #[serde(default)]
668  custom_media: bool,
669  #[serde(default)]
670  scroll_navigation_controls: bool,
671}
672
673#[derive(Serialize, Debug, Deserialize, Default)]
674#[serde(rename_all = "camelCase")]
675struct NonStandard {
676  #[serde(default)]
677  deep_selector_combinator: bool,
678}
679
680fn compile<'i>(
681  code: &'i str,
682  config: &Config,
683  #[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
684) -> Result<TransformResult<'i>, CompileError<'i, napi::Error>> {
685  let drafts = config.drafts.as_ref();
686  let non_standard = config.non_standard.as_ref();
687  let warnings = Some(Arc::new(RwLock::new(Vec::new())));
688
689  let filename = config.filename.clone().unwrap_or_default();
690  let project_root = config.project_root.as_ref().map(|p| p.as_ref());
691  let mut source_map = if config.source_map.unwrap_or_default() {
692    let mut sm = SourceMap::new(project_root.unwrap_or("/"));
693    sm.add_source(&filename);
694    sm.set_source_content(0, code)?;
695    Some(sm)
696  } else {
697    None
698  };
699
700  let res = {
701    let mut flags = ParserFlags::empty();
702    flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
703    flags.set(
704      ParserFlags::SCROLL_NAVIGATION_CONTROLS,
705      matches!(drafts, Some(d) if d.scroll_navigation_controls),
706    );
707    flags.set(
708      ParserFlags::DEEP_SELECTOR_COMBINATOR,
709      matches!(non_standard, Some(v) if v.deep_selector_combinator),
710    );
711
712    let mut stylesheet = StyleSheet::parse_with(
713      &code,
714      ParserOptions {
715        filename: filename.clone(),
716        flags,
717        css_modules: if let Some(css_modules) = &config.css_modules {
718          match css_modules {
719            CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
720            CssModulesOption::Bool(false) => None,
721            CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
722              pattern: if let Some(pattern) = c.pattern.as_ref() {
723                match lightningcss::css_modules::Pattern::parse(pattern) {
724                  Ok(p) => p,
725                  Err(e) => return Err(CompileError::PatternError(e)),
726                }
727              } else {
728                Default::default()
729              },
730              dashed_idents: c.dashed_idents.unwrap_or_default(),
731              animation: c.animation.unwrap_or(true),
732              container: c.container.unwrap_or(true),
733              grid: c.grid.unwrap_or(true),
734              custom_idents: c.custom_idents.unwrap_or(true),
735              pure: c.pure.unwrap_or_default(),
736            }),
737          }
738        } else {
739          None
740        },
741        source_index: 0,
742        error_recovery: config.error_recovery.unwrap_or_default(),
743        warnings: warnings.clone(),
744      },
745      &mut CustomAtRuleParser {
746        configs: config.custom_at_rules.clone().unwrap_or_default(),
747      },
748    )?;
749
750    #[cfg(feature = "visitor")]
751    if let Some(visitor) = visitor.as_mut() {
752      stylesheet.visit(visitor).map_err(CompileError::JsError)?;
753    }
754
755    let targets = Targets {
756      browsers: config.targets,
757      include: Features::from_bits_truncate(config.include),
758      exclude: Features::from_bits_truncate(config.exclude),
759    };
760
761    stylesheet.minify(MinifyOptions {
762      targets,
763      unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
764    })?;
765
766    stylesheet.to_css(PrinterOptions {
767      minify: config.minify.unwrap_or_default(),
768      source_map: source_map.as_mut(),
769      project_root,
770      targets,
771      analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
772        match d {
773          AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
774          AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
775            remove_imports: !c.preserve_imports,
776          }),
777          _ => None,
778        }
779      } else {
780        None
781      },
782      pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
783    })?
784  };
785
786  let map = if let Some(mut source_map) = source_map {
787    if let Some(input_source_map) = &config.input_source_map {
788      if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) {
789        let _ = source_map.extends(&mut sm);
790      }
791    }
792
793    source_map.to_json(None).ok()
794  } else {
795    None
796  };
797
798  Ok(TransformResult {
799    code: res.code.into_bytes(),
800    map: map.map(|m| m.into_bytes()),
801    exports: res.exports,
802    references: res.references,
803    dependencies: res.dependencies,
804    warnings: warnings.map_or(Vec::new(), |w| {
805      Arc::try_unwrap(w)
806        .unwrap()
807        .into_inner()
808        .unwrap()
809        .into_iter()
810        .map(|w| w.into())
811        .collect()
812    }),
813  })
814}
815
816#[cfg(feature = "bundler")]
817fn compile_bundle<'i, 'o, P: SourceProvider, F: FnOnce(&mut StyleSheet<'i, AtRule<'i>>) -> napi::Result<()>>(
818  fs: &'i P,
819  config: &'o BundleConfig,
820  visit: Option<F>,
821) -> Result<TransformResult<'i>, CompileError<'i, P::Error>> {
822  use std::path::Path;
823
824  let project_root = config.project_root.as_ref().map(|p| p.as_ref());
825  let mut source_map = if config.source_map.unwrap_or_default() {
826    Some(SourceMap::new(project_root.unwrap_or("/")))
827  } else {
828    None
829  };
830  let warnings = Some(Arc::new(RwLock::new(Vec::new())));
831
832  let res = {
833    let drafts = config.drafts.as_ref();
834    let non_standard = config.non_standard.as_ref();
835    let mut flags = ParserFlags::empty();
836    flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
837    flags.set(
838      ParserFlags::SCROLL_NAVIGATION_CONTROLS,
839      matches!(drafts, Some(d) if d.scroll_navigation_controls),
840    );
841    flags.set(
842      ParserFlags::DEEP_SELECTOR_COMBINATOR,
843      matches!(non_standard, Some(v) if v.deep_selector_combinator),
844    );
845
846    let parser_options = ParserOptions {
847      flags,
848      css_modules: if let Some(css_modules) = &config.css_modules {
849        match css_modules {
850          CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
851          CssModulesOption::Bool(false) => None,
852          CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
853            pattern: if let Some(pattern) = c.pattern.as_ref() {
854              match lightningcss::css_modules::Pattern::parse(pattern) {
855                Ok(p) => p,
856                Err(e) => return Err(CompileError::PatternError(e)),
857              }
858            } else {
859              Default::default()
860            },
861            dashed_idents: c.dashed_idents.unwrap_or_default(),
862            animation: c.animation.unwrap_or(true),
863            container: c.container.unwrap_or(true),
864            grid: c.grid.unwrap_or(true),
865            custom_idents: c.custom_idents.unwrap_or(true),
866            pure: c.pure.unwrap_or_default(),
867          }),
868        }
869      } else {
870        None
871      },
872      error_recovery: config.error_recovery.unwrap_or_default(),
873      warnings: warnings.clone(),
874      filename: String::new(),
875      source_index: 0,
876    };
877
878    let mut at_rule_parser = CustomAtRuleParser {
879      configs: config.custom_at_rules.clone().unwrap_or_default(),
880    };
881
882    let mut bundler =
883      Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser);
884    let mut stylesheet = bundler.bundle(Path::new(&config.filename))?;
885
886    if let Some(visit) = visit {
887      visit(&mut stylesheet).map_err(CompileError::JsError)?;
888    }
889
890    let targets = Targets {
891      browsers: config.targets,
892      include: Features::from_bits_truncate(config.include),
893      exclude: Features::from_bits_truncate(config.exclude),
894    };
895
896    stylesheet.minify(MinifyOptions {
897      targets,
898      unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
899    })?;
900
901    stylesheet.to_css(PrinterOptions {
902      minify: config.minify.unwrap_or_default(),
903      source_map: source_map.as_mut(),
904      project_root,
905      targets,
906      analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
907        match d {
908          AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
909          AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
910            remove_imports: !c.preserve_imports,
911          }),
912          _ => None,
913        }
914      } else {
915        None
916      },
917      pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
918    })?
919  };
920
921  let map = if let Some(source_map) = &mut source_map {
922    source_map.to_json(None).ok()
923  } else {
924    None
925  };
926
927  Ok(TransformResult {
928    code: res.code.into_bytes(),
929    map: map.map(|m| m.into_bytes()),
930    exports: res.exports,
931    references: res.references,
932    dependencies: res.dependencies,
933    warnings: warnings.map_or(Vec::new(), |w| {
934      Arc::try_unwrap(w)
935        .unwrap()
936        .into_inner()
937        .unwrap()
938        .into_iter()
939        .map(|w| w.into())
940        .collect()
941    }),
942  })
943}
944
945#[derive(Debug, Deserialize)]
946#[serde(rename_all = "camelCase")]
947struct AttrConfig {
948  pub filename: Option<String>,
949  #[serde(with = "serde_bytes")]
950  pub code: Vec<u8>,
951  pub targets: Option<Browsers>,
952  #[serde(default)]
953  pub include: u32,
954  #[serde(default)]
955  pub exclude: u32,
956  #[serde(default)]
957  pub minify: bool,
958  #[serde(default)]
959  pub analyze_dependencies: bool,
960  #[serde(default)]
961  pub error_recovery: bool,
962}
963
964#[derive(Serialize)]
965#[serde(rename_all = "camelCase")]
966struct AttrResult<'i> {
967  #[serde(with = "serde_bytes")]
968  code: Vec<u8>,
969  dependencies: Option<Vec<Dependency>>,
970  warnings: Vec<Warning<'i>>,
971}
972
973impl<'i> AttrResult<'i> {
974  fn into_js(self, ctx: CallContext) -> napi::Result<JsUnknown> {
975    // Manually construct buffers so we avoid a copy and work around
976    // https://github.com/napi-rs/napi-rs/issues/1124.
977    let mut obj = ctx.env.create_object()?;
978    let buf = ctx.env.create_buffer_with_data(self.code)?;
979    obj.set_named_property("code", buf.into_raw())?;
980    obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?;
981    obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?;
982    Ok(obj.into_unknown())
983  }
984}
985
986fn compile_attr<'i>(
987  code: &'i str,
988  config: &AttrConfig,
989  #[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
990) -> Result<AttrResult<'i>, CompileError<'i, napi::Error>> {
991  let warnings = if config.error_recovery {
992    Some(Arc::new(RwLock::new(Vec::new())))
993  } else {
994    None
995  };
996  let res = {
997    let filename = config.filename.clone().unwrap_or_default();
998    let mut attr = StyleAttribute::parse(
999      &code,
1000      ParserOptions {
1001        filename,
1002        error_recovery: config.error_recovery,
1003        warnings: warnings.clone(),
1004        ..ParserOptions::default()
1005      },
1006    )?;
1007
1008    #[cfg(feature = "visitor")]
1009    if let Some(visitor) = visitor.as_mut() {
1010      attr.visit(visitor).unwrap();
1011    }
1012
1013    let targets = Targets {
1014      browsers: config.targets,
1015      include: Features::from_bits_truncate(config.include),
1016      exclude: Features::from_bits_truncate(config.exclude),
1017    };
1018
1019    attr.minify(MinifyOptions {
1020      targets,
1021      ..MinifyOptions::default()
1022    });
1023    attr.to_css(PrinterOptions {
1024      minify: config.minify,
1025      source_map: None,
1026      project_root: None,
1027      targets,
1028      analyze_dependencies: if config.analyze_dependencies {
1029        Some(DependencyOptions::default())
1030      } else {
1031        None
1032      },
1033      pseudo_classes: None,
1034    })?
1035  };
1036  Ok(AttrResult {
1037    code: res.code.into_bytes(),
1038    dependencies: res.dependencies,
1039    warnings: warnings.map_or(Vec::new(), |w| {
1040      Arc::try_unwrap(w)
1041        .unwrap()
1042        .into_inner()
1043        .unwrap()
1044        .into_iter()
1045        .map(|w| w.into())
1046        .collect()
1047    }),
1048  })
1049}
1050
1051enum CompileError<'i, E: std::error::Error> {
1052  ParseError(Error<ParserError<'i>>),
1053  MinifyError(Error<MinifyErrorKind>),
1054  PrinterError(Error<PrinterErrorKind>),
1055  SourceMapError(parcel_sourcemap::SourceMapError),
1056  BundleError(Error<BundleErrorKind<'i, E>>),
1057  PatternError(PatternParseError),
1058  #[cfg(feature = "visitor")]
1059  JsError(napi::Error),
1060}
1061
1062impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> {
1063  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1064    match self {
1065      CompileError::ParseError(err) => err.kind.fmt(f),
1066      CompileError::MinifyError(err) => err.kind.fmt(f),
1067      CompileError::PrinterError(err) => err.kind.fmt(f),
1068      CompileError::BundleError(err) => err.kind.fmt(f),
1069      CompileError::PatternError(err) => err.fmt(f),
1070      CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this
1071      #[cfg(feature = "visitor")]
1072      CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f),
1073    }
1074  }
1075}
1076
1077impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> {
1078  fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result<napi::Error> {
1079    let reason = self.to_string();
1080    let data = match &self {
1081      CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?,
1082      CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?,
1083      CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?,
1084      CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?,
1085      _ => env.get_null()?.into_unknown(),
1086    };
1087
1088    let (js_error, loc) = match self {
1089      CompileError::BundleError(Error {
1090        loc,
1091        kind: BundleErrorKind::ResolverError(e),
1092      }) => {
1093        // Add location info to existing JS error if available.
1094        (e.into_js_error(env)?, loc)
1095      }
1096      CompileError::ParseError(Error { loc, .. })
1097      | CompileError::PrinterError(Error { loc, .. })
1098      | CompileError::MinifyError(Error { loc, .. })
1099      | CompileError::BundleError(Error { loc, .. }) => {
1100        // Generate an error with location information.
1101        let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
1102        let reason = env.create_string_from_std(reason)?;
1103        let obj = syntax_error.new_instance(&[reason])?;
1104        (obj.into_unknown(), loc)
1105      }
1106      _ => return Ok(self.into()),
1107    };
1108
1109    if js_error.get_type()? == napi::ValueType::Object {
1110      let mut obj: JsObject = unsafe { js_error.cast() };
1111      if let Some(loc) = loc {
1112        let line = env.create_int32((loc.line + 1) as i32)?;
1113        let col = env.create_int32(loc.column as i32)?;
1114        let filename = env.create_string_from_std(loc.filename)?;
1115        obj.set_named_property("fileName", filename)?;
1116        if let Some(code) = code {
1117          let source = env.create_string(code)?;
1118          obj.set_named_property("source", source)?;
1119        }
1120        let mut loc = env.create_object()?;
1121        loc.set_named_property("line", line)?;
1122        loc.set_named_property("column", col)?;
1123        obj.set_named_property("loc", loc)?;
1124      }
1125      obj.set_named_property("data", data)?;
1126      Ok(obj.into_unknown().into())
1127    } else {
1128      Ok(js_error.into())
1129    }
1130  }
1131}
1132
1133trait IntoJsError {
1134  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown>;
1135}
1136
1137impl IntoJsError for std::io::Error {
1138  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
1139    let reason = self.to_string();
1140    let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
1141    let reason = env.create_string_from_std(reason)?;
1142    let obj = syntax_error.new_instance(&[reason])?;
1143    Ok(obj.into_unknown())
1144  }
1145}
1146
1147impl IntoJsError for napi::Error {
1148  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
1149    unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) }
1150  }
1151}
1152
1153impl<'i, E: std::error::Error> From<Error<ParserError<'i>>> for CompileError<'i, E> {
1154  fn from(e: Error<ParserError<'i>>) -> CompileError<'i, E> {
1155    CompileError::ParseError(e)
1156  }
1157}
1158
1159impl<'i, E: std::error::Error> From<Error<MinifyErrorKind>> for CompileError<'i, E> {
1160  fn from(err: Error<MinifyErrorKind>) -> CompileError<'i, E> {
1161    CompileError::MinifyError(err)
1162  }
1163}
1164
1165impl<'i, E: std::error::Error> From<Error<PrinterErrorKind>> for CompileError<'i, E> {
1166  fn from(err: Error<PrinterErrorKind>) -> CompileError<'i, E> {
1167    CompileError::PrinterError(err)
1168  }
1169}
1170
1171impl<'i, E: std::error::Error> From<parcel_sourcemap::SourceMapError> for CompileError<'i, E> {
1172  fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> {
1173    CompileError::SourceMapError(e)
1174  }
1175}
1176
1177impl<'i, E: std::error::Error> From<Error<BundleErrorKind<'i, E>>> for CompileError<'i, E> {
1178  fn from(e: Error<BundleErrorKind<'i, E>>) -> CompileError<'i, E> {
1179    CompileError::BundleError(e)
1180  }
1181}
1182
1183impl<'i, E: std::error::Error> From<CompileError<'i, E>> for napi::Error {
1184  fn from(e: CompileError<'i, E>) -> napi::Error {
1185    match e {
1186      CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()),
1187      CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()),
1188      #[cfg(feature = "visitor")]
1189      CompileError::JsError(e) => e,
1190      _ => napi::Error::new(napi::Status::GenericFailure, e.to_string()),
1191    }
1192  }
1193}
1194
1195#[derive(Serialize)]
1196struct Warning<'i> {
1197  message: String,
1198  #[serde(flatten)]
1199  data: ParserError<'i>,
1200  loc: Option<ErrorLocation>,
1201}
1202
1203impl<'i> From<Error<ParserError<'i>>> for Warning<'i> {
1204  fn from(mut e: Error<ParserError<'i>>) -> Self {
1205    // Convert to 1-based line numbers.
1206    if let Some(loc) = &mut e.loc {
1207      loc.line += 1;
1208    }
1209    Warning {
1210      message: e.kind.to_string(),
1211      data: e.kind,
1212      loc: e.loc,
1213    }
1214  }
1215}