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