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, 'o, F>(f: F) -> F
141    where
142      F: FnOnce(&mut StyleSheet<'i, 'o, 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, '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::<
398                    &'_ mut StyleSheet<'_, '_, AtRule>,
399                    &'static mut StyleSheet<'static, 'static, AtRule>,
400                  >(stylesheet)
401                },
402                tx: channel.0.clone(),
403              };
404
405              tsfn.call(message, ThreadsafeFunctionCallMode::Blocking);
406              channel.1.recv().expect("recv error").map(|_| ())
407            })
408          }
409        }),
410      );
411
412      deferred.resolve(move |env| match res {
413        Ok(v) => v.into_js(env),
414        Err(err) => Err(err.into_js_error(env, None)?),
415      });
416    });
417
418    Ok(promise)
419  }
420}
421
422#[cfg(feature = "bundler")]
423#[cfg(target_arch = "wasm32")]
424mod bundle {
425  use super::*;
426  use lightningcss::bundler::ResolveResult;
427  use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref};
428  use std::cell::UnsafeCell;
429  use std::path::Path;
430
431  pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
432    let opts = ctx.get::<JsObject>(0)?;
433    let mut visitor = get_visitor(*ctx.env, &opts);
434
435    let resolver = get_named_property::<JsObject>(&opts, "resolver")?;
436    let read = get_named_property::<JsFunction>(&resolver, "read")?;
437    let resolve = if resolver.has_named_property("resolve")? {
438      let resolve = get_named_property::<JsFunction>(&resolver, "resolve")?;
439      Some(ctx.env.create_reference(resolve)?)
440    } else {
441      None
442    };
443    let config: BundleConfig = ctx.env.from_js_value(opts)?;
444
445    let provider = JsSourceProvider {
446      env: ctx.env.clone(),
447      resolve,
448      read: ctx.env.create_reference(read)?,
449      inputs: UnsafeCell::new(Vec::new()),
450    };
451
452    // This is pretty silly, but works around a rust limitation that you cannot
453    // explicitly annotate lifetime bounds on closures.
454    fn annotate<'i, 'o, F>(f: F) -> F
455    where
456      F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
457    {
458      f
459    }
460
461    let res = compile_bundle(
462      &provider,
463      &config,
464      visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))),
465    );
466
467    match res {
468      Ok(res) => res.into_js(*ctx.env),
469      Err(err) => Err(err.into_js_error(*ctx.env, None)?),
470    }
471  }
472
473  struct JsSourceProvider {
474    env: Env,
475    resolve: Option<Ref<()>>,
476    read: Ref<()>,
477    inputs: UnsafeCell<Vec<*mut String>>,
478  }
479
480  impl Drop for JsSourceProvider {
481    fn drop(&mut self) {
482      if let Some(resolve) = &mut self.resolve {
483        drop(resolve.unref(self.env));
484      }
485      drop(self.read.unref(self.env));
486    }
487  }
488
489  unsafe impl Sync for JsSourceProvider {}
490  unsafe impl Send for JsSourceProvider {}
491
492  // This relies on Binaryen's Asyncify transform to allow Rust to call async JS functions from sync code.
493  // See the comments in async.mjs for more details about how this works.
494  extern "C" {
495    fn await_promise_sync(
496      promise: napi::sys::napi_value,
497      result: *mut napi::sys::napi_value,
498      error: *mut napi::sys::napi_value,
499    );
500  }
501
502  fn get_result(env: Env, mut value: JsUnknown) -> napi::Result<JsUnknown> {
503    if value.is_promise()? {
504      let mut result = std::ptr::null_mut();
505      let mut error = std::ptr::null_mut();
506      unsafe { await_promise_sync(value.raw(), &mut result, &mut error) };
507      if !error.is_null() {
508        let error = unsafe { JsUnknown::from_raw(env.raw(), error)? };
509        return Err(napi::Error::from(error));
510      }
511      if result.is_null() {
512        return Err(napi::Error::new(napi::Status::GenericFailure, "No result".to_string()));
513      }
514
515      value = unsafe { JsUnknown::from_raw(env.raw(), result)? };
516    }
517
518    Ok(value)
519  }
520
521  impl SourceProvider for JsSourceProvider {
522    type Error = napi::Error;
523
524    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
525      let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?;
526      let file = self.env.create_string(file.to_str().unwrap())?;
527      let source: JsUnknown = read.call(None, &[file])?;
528      let source = get_result(self.env, source)?;
529      let source: JsString = source.try_into()?;
530      let source = source.into_utf8()?.into_owned()?;
531
532      // cache the result
533      let ptr = Box::into_raw(Box::new(source));
534      let inputs = unsafe { &mut *self.inputs.get() };
535      inputs.push(ptr);
536      // SAFETY: this is safe because the pointer is not dropped
537      // until the JsSourceProvider is, and we never remove from the
538      // list of pointers stored in the vector.
539      Ok(unsafe { &*ptr })
540    }
541
542    fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<ResolveResult, Self::Error> {
543      if let Some(resolve) = &self.resolve {
544        let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?;
545        let specifier = self.env.create_string(specifier)?;
546        let originating_file = self.env.create_string(originating_file.to_str().unwrap())?;
547        let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?;
548        let result = get_result(self.env, result)?;
549        let result = self.env.from_js_value(result)?;
550        Ok(result)
551      } else {
552        Ok(ResolveResult::File(originating_file.with_file_name(specifier)))
553      }
554    }
555  }
556}
557
558#[cfg(feature = "bundler")]
559pub use bundle::*;
560
561// ---------------------------------------------
562
563#[derive(Debug, Deserialize)]
564#[serde(rename_all = "camelCase")]
565struct Config {
566  pub filename: Option<String>,
567  pub project_root: Option<String>,
568  #[serde(with = "serde_bytes")]
569  pub code: Vec<u8>,
570  pub targets: Option<Browsers>,
571  #[serde(default)]
572  pub include: u32,
573  #[serde(default)]
574  pub exclude: u32,
575  pub minify: Option<bool>,
576  pub source_map: Option<bool>,
577  pub input_source_map: Option<String>,
578  pub drafts: Option<Drafts>,
579  pub non_standard: Option<NonStandard>,
580  pub css_modules: Option<CssModulesOption>,
581  pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
582  pub pseudo_classes: Option<OwnedPseudoClasses>,
583  pub unused_symbols: Option<HashSet<String>>,
584  pub error_recovery: Option<bool>,
585  pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
586}
587
588#[derive(Debug, Deserialize)]
589#[serde(untagged)]
590enum AnalyzeDependenciesOption {
591  Bool(bool),
592  Config(AnalyzeDependenciesConfig),
593}
594
595#[derive(Debug, Deserialize)]
596#[serde(rename_all = "camelCase")]
597struct AnalyzeDependenciesConfig {
598  preserve_imports: bool,
599}
600
601#[derive(Debug, Deserialize)]
602#[serde(untagged)]
603enum CssModulesOption {
604  Bool(bool),
605  Config(CssModulesConfig),
606}
607
608#[derive(Debug, Deserialize)]
609#[serde(rename_all = "camelCase")]
610struct CssModulesConfig {
611  pattern: Option<String>,
612  dashed_idents: Option<bool>,
613  animation: Option<bool>,
614  container: Option<bool>,
615  grid: Option<bool>,
616  custom_idents: Option<bool>,
617  pure: Option<bool>,
618}
619
620#[cfg(feature = "bundler")]
621#[derive(Debug, Deserialize)]
622#[serde(rename_all = "camelCase")]
623struct BundleConfig {
624  pub filename: String,
625  pub project_root: Option<String>,
626  pub targets: Option<Browsers>,
627  #[serde(default)]
628  pub include: u32,
629  #[serde(default)]
630  pub exclude: u32,
631  pub minify: Option<bool>,
632  pub source_map: Option<bool>,
633  pub drafts: Option<Drafts>,
634  pub non_standard: Option<NonStandard>,
635  pub css_modules: Option<CssModulesOption>,
636  pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
637  pub pseudo_classes: Option<OwnedPseudoClasses>,
638  pub unused_symbols: Option<HashSet<String>>,
639  pub error_recovery: Option<bool>,
640  pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
641}
642
643#[derive(Debug, Deserialize)]
644#[serde(rename_all = "camelCase")]
645struct OwnedPseudoClasses {
646  pub hover: Option<String>,
647  pub active: Option<String>,
648  pub focus: Option<String>,
649  pub focus_visible: Option<String>,
650  pub focus_within: Option<String>,
651}
652
653impl<'a> Into<PseudoClasses<'a>> for &'a OwnedPseudoClasses {
654  fn into(self) -> PseudoClasses<'a> {
655    PseudoClasses {
656      hover: self.hover.as_deref(),
657      active: self.active.as_deref(),
658      focus: self.focus.as_deref(),
659      focus_visible: self.focus_visible.as_deref(),
660      focus_within: self.focus_within.as_deref(),
661    }
662  }
663}
664
665#[derive(Serialize, Debug, Deserialize, Default)]
666#[serde(rename_all = "camelCase")]
667struct Drafts {
668  #[serde(default)]
669  custom_media: bool,
670}
671
672#[derive(Serialize, Debug, Deserialize, Default)]
673#[serde(rename_all = "camelCase")]
674struct NonStandard {
675  #[serde(default)]
676  deep_selector_combinator: bool,
677}
678
679fn compile<'i>(
680  code: &'i str,
681  config: &Config,
682  #[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
683) -> Result<TransformResult<'i>, CompileError<'i, napi::Error>> {
684  let drafts = config.drafts.as_ref();
685  let non_standard = config.non_standard.as_ref();
686  let warnings = Some(Arc::new(RwLock::new(Vec::new())));
687
688  let filename = config.filename.clone().unwrap_or_default();
689  let project_root = config.project_root.as_ref().map(|p| p.as_ref());
690  let mut source_map = if config.source_map.unwrap_or_default() {
691    let mut sm = SourceMap::new(project_root.unwrap_or("/"));
692    sm.add_source(&filename);
693    sm.set_source_content(0, code)?;
694    Some(sm)
695  } else {
696    None
697  };
698
699  let res = {
700    let mut flags = ParserFlags::empty();
701    flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
702    flags.set(
703      ParserFlags::DEEP_SELECTOR_COMBINATOR,
704      matches!(non_standard, Some(v) if v.deep_selector_combinator),
705    );
706
707    let mut stylesheet = StyleSheet::parse_with(
708      &code,
709      ParserOptions {
710        filename: filename.clone(),
711        flags,
712        css_modules: if let Some(css_modules) = &config.css_modules {
713          match css_modules {
714            CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
715            CssModulesOption::Bool(false) => None,
716            CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
717              pattern: if let Some(pattern) = c.pattern.as_ref() {
718                match lightningcss::css_modules::Pattern::parse(pattern) {
719                  Ok(p) => p,
720                  Err(e) => return Err(CompileError::PatternError(e)),
721                }
722              } else {
723                Default::default()
724              },
725              dashed_idents: c.dashed_idents.unwrap_or_default(),
726              animation: c.animation.unwrap_or(true),
727              container: c.container.unwrap_or(true),
728              grid: c.grid.unwrap_or(true),
729              custom_idents: c.custom_idents.unwrap_or(true),
730              pure: c.pure.unwrap_or_default(),
731            }),
732          }
733        } else {
734          None
735        },
736        source_index: 0,
737        error_recovery: config.error_recovery.unwrap_or_default(),
738        warnings: warnings.clone(),
739      },
740      &mut CustomAtRuleParser {
741        configs: config.custom_at_rules.clone().unwrap_or_default(),
742      },
743    )?;
744
745    #[cfg(feature = "visitor")]
746    if let Some(visitor) = visitor.as_mut() {
747      stylesheet.visit(visitor).map_err(CompileError::JsError)?;
748    }
749
750    let targets = Targets {
751      browsers: config.targets,
752      include: Features::from_bits_truncate(config.include),
753      exclude: Features::from_bits_truncate(config.exclude),
754    };
755
756    stylesheet.minify(MinifyOptions {
757      targets,
758      unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
759    })?;
760
761    stylesheet.to_css(PrinterOptions {
762      minify: config.minify.unwrap_or_default(),
763      source_map: source_map.as_mut(),
764      project_root,
765      targets,
766      analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
767        match d {
768          AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
769          AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
770            remove_imports: !c.preserve_imports,
771          }),
772          _ => None,
773        }
774      } else {
775        None
776      },
777      pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
778    })?
779  };
780
781  let map = if let Some(mut source_map) = source_map {
782    if let Some(input_source_map) = &config.input_source_map {
783      if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) {
784        let _ = source_map.extends(&mut sm);
785      }
786    }
787
788    source_map.to_json(None).ok()
789  } else {
790    None
791  };
792
793  Ok(TransformResult {
794    code: res.code.into_bytes(),
795    map: map.map(|m| m.into_bytes()),
796    exports: res.exports,
797    references: res.references,
798    dependencies: res.dependencies,
799    warnings: warnings.map_or(Vec::new(), |w| {
800      Arc::try_unwrap(w)
801        .unwrap()
802        .into_inner()
803        .unwrap()
804        .into_iter()
805        .map(|w| w.into())
806        .collect()
807    }),
808  })
809}
810
811#[cfg(feature = "bundler")]
812fn compile_bundle<
813  'i,
814  'o,
815  P: SourceProvider,
816  F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
817>(
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::DEEP_SELECTOR_COMBINATOR,
839      matches!(non_standard, Some(v) if v.deep_selector_combinator),
840    );
841
842    let parser_options = ParserOptions {
843      flags,
844      css_modules: if let Some(css_modules) = &config.css_modules {
845        match css_modules {
846          CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
847          CssModulesOption::Bool(false) => None,
848          CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
849            pattern: if let Some(pattern) = c.pattern.as_ref() {
850              match lightningcss::css_modules::Pattern::parse(pattern) {
851                Ok(p) => p,
852                Err(e) => return Err(CompileError::PatternError(e)),
853              }
854            } else {
855              Default::default()
856            },
857            dashed_idents: c.dashed_idents.unwrap_or_default(),
858            animation: c.animation.unwrap_or(true),
859            container: c.container.unwrap_or(true),
860            grid: c.grid.unwrap_or(true),
861            custom_idents: c.custom_idents.unwrap_or(true),
862            pure: c.pure.unwrap_or_default(),
863          }),
864        }
865      } else {
866        None
867      },
868      error_recovery: config.error_recovery.unwrap_or_default(),
869      warnings: warnings.clone(),
870      filename: String::new(),
871      source_index: 0,
872    };
873
874    let mut at_rule_parser = CustomAtRuleParser {
875      configs: config.custom_at_rules.clone().unwrap_or_default(),
876    };
877
878    let mut bundler =
879      Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser);
880    let mut stylesheet = bundler.bundle(Path::new(&config.filename))?;
881
882    if let Some(visit) = visit {
883      visit(&mut stylesheet).map_err(CompileError::JsError)?;
884    }
885
886    let targets = Targets {
887      browsers: config.targets,
888      include: Features::from_bits_truncate(config.include),
889      exclude: Features::from_bits_truncate(config.exclude),
890    };
891
892    stylesheet.minify(MinifyOptions {
893      targets,
894      unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
895    })?;
896
897    stylesheet.to_css(PrinterOptions {
898      minify: config.minify.unwrap_or_default(),
899      source_map: source_map.as_mut(),
900      project_root,
901      targets,
902      analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
903        match d {
904          AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
905          AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
906            remove_imports: !c.preserve_imports,
907          }),
908          _ => None,
909        }
910      } else {
911        None
912      },
913      pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
914    })?
915  };
916
917  let map = if let Some(source_map) = &mut source_map {
918    source_map.to_json(None).ok()
919  } else {
920    None
921  };
922
923  Ok(TransformResult {
924    code: res.code.into_bytes(),
925    map: map.map(|m| m.into_bytes()),
926    exports: res.exports,
927    references: res.references,
928    dependencies: res.dependencies,
929    warnings: warnings.map_or(Vec::new(), |w| {
930      Arc::try_unwrap(w)
931        .unwrap()
932        .into_inner()
933        .unwrap()
934        .into_iter()
935        .map(|w| w.into())
936        .collect()
937    }),
938  })
939}
940
941#[derive(Debug, Deserialize)]
942#[serde(rename_all = "camelCase")]
943struct AttrConfig {
944  pub filename: Option<String>,
945  #[serde(with = "serde_bytes")]
946  pub code: Vec<u8>,
947  pub targets: Option<Browsers>,
948  #[serde(default)]
949  pub include: u32,
950  #[serde(default)]
951  pub exclude: u32,
952  #[serde(default)]
953  pub minify: bool,
954  #[serde(default)]
955  pub analyze_dependencies: bool,
956  #[serde(default)]
957  pub error_recovery: bool,
958}
959
960#[derive(Serialize)]
961#[serde(rename_all = "camelCase")]
962struct AttrResult<'i> {
963  #[serde(with = "serde_bytes")]
964  code: Vec<u8>,
965  dependencies: Option<Vec<Dependency>>,
966  warnings: Vec<Warning<'i>>,
967}
968
969impl<'i> AttrResult<'i> {
970  fn into_js(self, ctx: CallContext) -> napi::Result<JsUnknown> {
971    // Manually construct buffers so we avoid a copy and work around
972    // https://github.com/napi-rs/napi-rs/issues/1124.
973    let mut obj = ctx.env.create_object()?;
974    let buf = ctx.env.create_buffer_with_data(self.code)?;
975    obj.set_named_property("code", buf.into_raw())?;
976    obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?;
977    obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?;
978    Ok(obj.into_unknown())
979  }
980}
981
982fn compile_attr<'i>(
983  code: &'i str,
984  config: &AttrConfig,
985  #[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
986) -> Result<AttrResult<'i>, CompileError<'i, napi::Error>> {
987  let warnings = if config.error_recovery {
988    Some(Arc::new(RwLock::new(Vec::new())))
989  } else {
990    None
991  };
992  let res = {
993    let filename = config.filename.clone().unwrap_or_default();
994    let mut attr = StyleAttribute::parse(
995      &code,
996      ParserOptions {
997        filename,
998        error_recovery: config.error_recovery,
999        warnings: warnings.clone(),
1000        ..ParserOptions::default()
1001      },
1002    )?;
1003
1004    #[cfg(feature = "visitor")]
1005    if let Some(visitor) = visitor.as_mut() {
1006      attr.visit(visitor).unwrap();
1007    }
1008
1009    let targets = Targets {
1010      browsers: config.targets,
1011      include: Features::from_bits_truncate(config.include),
1012      exclude: Features::from_bits_truncate(config.exclude),
1013    };
1014
1015    attr.minify(MinifyOptions {
1016      targets,
1017      ..MinifyOptions::default()
1018    });
1019    attr.to_css(PrinterOptions {
1020      minify: config.minify,
1021      source_map: None,
1022      project_root: None,
1023      targets,
1024      analyze_dependencies: if config.analyze_dependencies {
1025        Some(DependencyOptions::default())
1026      } else {
1027        None
1028      },
1029      pseudo_classes: None,
1030    })?
1031  };
1032  Ok(AttrResult {
1033    code: res.code.into_bytes(),
1034    dependencies: res.dependencies,
1035    warnings: warnings.map_or(Vec::new(), |w| {
1036      Arc::try_unwrap(w)
1037        .unwrap()
1038        .into_inner()
1039        .unwrap()
1040        .into_iter()
1041        .map(|w| w.into())
1042        .collect()
1043    }),
1044  })
1045}
1046
1047enum CompileError<'i, E: std::error::Error> {
1048  ParseError(Error<ParserError<'i>>),
1049  MinifyError(Error<MinifyErrorKind>),
1050  PrinterError(Error<PrinterErrorKind>),
1051  SourceMapError(parcel_sourcemap::SourceMapError),
1052  BundleError(Error<BundleErrorKind<'i, E>>),
1053  PatternError(PatternParseError),
1054  #[cfg(feature = "visitor")]
1055  JsError(napi::Error),
1056}
1057
1058impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> {
1059  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1060    match self {
1061      CompileError::ParseError(err) => err.kind.fmt(f),
1062      CompileError::MinifyError(err) => err.kind.fmt(f),
1063      CompileError::PrinterError(err) => err.kind.fmt(f),
1064      CompileError::BundleError(err) => err.kind.fmt(f),
1065      CompileError::PatternError(err) => err.fmt(f),
1066      CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this
1067      #[cfg(feature = "visitor")]
1068      CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f),
1069    }
1070  }
1071}
1072
1073impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> {
1074  fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result<napi::Error> {
1075    let reason = self.to_string();
1076    let data = match &self {
1077      CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?,
1078      CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?,
1079      CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?,
1080      CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?,
1081      _ => env.get_null()?.into_unknown(),
1082    };
1083
1084    let (js_error, loc) = match self {
1085      CompileError::BundleError(Error {
1086        loc,
1087        kind: BundleErrorKind::ResolverError(e),
1088      }) => {
1089        // Add location info to existing JS error if available.
1090        (e.into_js_error(env)?, loc)
1091      }
1092      CompileError::ParseError(Error { loc, .. })
1093      | CompileError::PrinterError(Error { loc, .. })
1094      | CompileError::MinifyError(Error { loc, .. })
1095      | CompileError::BundleError(Error { loc, .. }) => {
1096        // Generate an error with location information.
1097        let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
1098        let reason = env.create_string_from_std(reason)?;
1099        let obj = syntax_error.new_instance(&[reason])?;
1100        (obj.into_unknown(), loc)
1101      }
1102      _ => return Ok(self.into()),
1103    };
1104
1105    if js_error.get_type()? == napi::ValueType::Object {
1106      let mut obj: JsObject = unsafe { js_error.cast() };
1107      if let Some(loc) = loc {
1108        let line = env.create_int32((loc.line + 1) as i32)?;
1109        let col = env.create_int32(loc.column as i32)?;
1110        let filename = env.create_string_from_std(loc.filename)?;
1111        obj.set_named_property("fileName", filename)?;
1112        if let Some(code) = code {
1113          let source = env.create_string(code)?;
1114          obj.set_named_property("source", source)?;
1115        }
1116        let mut loc = env.create_object()?;
1117        loc.set_named_property("line", line)?;
1118        loc.set_named_property("column", col)?;
1119        obj.set_named_property("loc", loc)?;
1120      }
1121      obj.set_named_property("data", data)?;
1122      Ok(obj.into_unknown().into())
1123    } else {
1124      Ok(js_error.into())
1125    }
1126  }
1127}
1128
1129trait IntoJsError {
1130  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown>;
1131}
1132
1133impl IntoJsError for std::io::Error {
1134  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
1135    let reason = self.to_string();
1136    let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
1137    let reason = env.create_string_from_std(reason)?;
1138    let obj = syntax_error.new_instance(&[reason])?;
1139    Ok(obj.into_unknown())
1140  }
1141}
1142
1143impl IntoJsError for napi::Error {
1144  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
1145    unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) }
1146  }
1147}
1148
1149impl<'i, E: std::error::Error> From<Error<ParserError<'i>>> for CompileError<'i, E> {
1150  fn from(e: Error<ParserError<'i>>) -> CompileError<'i, E> {
1151    CompileError::ParseError(e)
1152  }
1153}
1154
1155impl<'i, E: std::error::Error> From<Error<MinifyErrorKind>> for CompileError<'i, E> {
1156  fn from(err: Error<MinifyErrorKind>) -> CompileError<'i, E> {
1157    CompileError::MinifyError(err)
1158  }
1159}
1160
1161impl<'i, E: std::error::Error> From<Error<PrinterErrorKind>> for CompileError<'i, E> {
1162  fn from(err: Error<PrinterErrorKind>) -> CompileError<'i, E> {
1163    CompileError::PrinterError(err)
1164  }
1165}
1166
1167impl<'i, E: std::error::Error> From<parcel_sourcemap::SourceMapError> for CompileError<'i, E> {
1168  fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> {
1169    CompileError::SourceMapError(e)
1170  }
1171}
1172
1173impl<'i, E: std::error::Error> From<Error<BundleErrorKind<'i, E>>> for CompileError<'i, E> {
1174  fn from(e: Error<BundleErrorKind<'i, E>>) -> CompileError<'i, E> {
1175    CompileError::BundleError(e)
1176  }
1177}
1178
1179impl<'i, E: std::error::Error> From<CompileError<'i, E>> for napi::Error {
1180  fn from(e: CompileError<'i, E>) -> napi::Error {
1181    match e {
1182      CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()),
1183      CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()),
1184      #[cfg(feature = "visitor")]
1185      CompileError::JsError(e) => e,
1186      _ => napi::Error::new(napi::Status::GenericFailure, e.to_string()),
1187    }
1188  }
1189}
1190
1191#[derive(Serialize)]
1192struct Warning<'i> {
1193  message: String,
1194  #[serde(flatten)]
1195  data: ParserError<'i>,
1196  loc: Option<ErrorLocation>,
1197}
1198
1199impl<'i> From<Error<ParserError<'i>>> for Warning<'i> {
1200  fn from(mut e: Error<ParserError<'i>>) -> Self {
1201    // Convert to 1-based line numbers.
1202    if let Some(loc) = &mut e.loc {
1203      loc.line += 1;
1204    }
1205    Warning {
1206      message: e.kind.to_string(),
1207      data: e.kind,
1208      loc: e.loc,
1209    }
1210  }
1211}