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 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 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 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 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 let ptr = Box::into_raw(Box::new(source));
196 self.inputs.lock().unwrap().push(ptr);
197 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 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 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 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 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 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 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 let ptr = Box::into_raw(Box::new(source));
530 let inputs = unsafe { &mut *self.inputs.get() };
531 inputs.push(ptr);
532 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#[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 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()), #[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 (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 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 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}