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