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