lightningcss/
bundler.rs

1//! CSS bundling.
2//!
3//! A [Bundler](Bundler) can be used to combine a CSS file and all of its dependencies
4//! into a single merged style sheet. It works together with a [SourceProvider](SourceProvider)
5//! (e.g. [FileProvider](FileProvider)) to read files from the file system or another source,
6//! and returns a [StyleSheet](super::stylesheet::StyleSheet) containing the rules from all
7//! of the dependencies of the entry file, recursively.
8//!
9//! Rules are bundled following `@import` order, and wrapped in the necessary `@media`, `@supports`,
10//! and `@layer` rules as appropriate to preserve the authored behavior.
11//!
12//! # Example
13//!
14//! ```no_run
15//! use std::path::Path;
16//! use lightningcss::{
17//!   bundler::{Bundler, FileProvider},
18//!   stylesheet::ParserOptions
19//! };
20//!
21//! let fs = FileProvider::new();
22//! let mut bundler = Bundler::new(&fs, None, ParserOptions::default());
23//! let stylesheet = bundler.bundle(Path::new("style.css")).unwrap();
24//! ```
25
26use crate::{
27  error::ErrorLocation,
28  parser::DefaultAtRuleParser,
29  properties::{
30    css_modules::Specifier,
31    custom::{
32      CustomProperty, EnvironmentVariableName, TokenList, TokenOrValue, UnparsedProperty, UnresolvedColor,
33    },
34    Property,
35  },
36  rules::{
37    layer::{LayerBlockRule, LayerName},
38    Location,
39  },
40  traits::{AtRuleParser, ToCss},
41  values::ident::DashedIdentReference,
42};
43use crate::{
44  error::{Error, ParserError},
45  media_query::MediaList,
46  rules::{
47    import::ImportRule,
48    media::MediaRule,
49    supports::{SupportsCondition, SupportsRule},
50    CssRule, CssRuleList,
51  },
52  stylesheet::{ParserOptions, StyleSheet},
53};
54use dashmap::DashMap;
55use parcel_sourcemap::SourceMap;
56use rayon::prelude::*;
57use std::{
58  collections::HashSet,
59  fs,
60  path::{Path, PathBuf},
61  sync::Mutex,
62};
63
64/// A Bundler combines a CSS file and all imported dependencies together into
65/// a single merged style sheet.
66pub struct Bundler<'a, 'o, 's, P, T: AtRuleParser<'a>> {
67  source_map: Option<Mutex<&'s mut SourceMap>>,
68  fs: &'a P,
69  source_indexes: DashMap<PathBuf, u32>,
70  stylesheets: Mutex<Vec<BundleStyleSheet<'a, 'o, T::AtRule>>>,
71  options: ParserOptions<'o, 'a>,
72  at_rule_parser: Mutex<AtRuleParserValue<'s, T>>,
73}
74
75enum AtRuleParserValue<'a, T> {
76  Owned(T),
77  Borrowed(&'a mut T),
78}
79
80struct BundleStyleSheet<'i, 'o, T> {
81  stylesheet: Option<StyleSheet<'i, 'o, T>>,
82  dependencies: Vec<u32>,
83  css_modules_deps: Vec<u32>,
84  parent_source_index: u32,
85  parent_dep_index: u32,
86  layer: Option<Option<LayerName<'i>>>,
87  supports: Option<SupportsCondition<'i>>,
88  media: MediaList<'i>,
89  loc: Location,
90}
91
92/// A trait to provide the contents of files to a Bundler.
93///
94/// See [FileProvider](FileProvider) for an implementation that uses the
95/// file system.
96pub trait SourceProvider: Send + Sync {
97  /// A custom error.
98  type Error: std::error::Error + Send + Sync;
99
100  /// Reads the contents of the given file path to a string.
101  fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error>;
102
103  /// Resolves the given import specifier to a file path given the file
104  /// which the import originated from.
105  fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error>;
106}
107
108/// Provides an implementation of [SourceProvider](SourceProvider)
109/// that reads files from the file system.
110pub struct FileProvider {
111  inputs: Mutex<Vec<*mut String>>,
112}
113
114impl FileProvider {
115  /// Creates a new FileProvider.
116  pub fn new() -> FileProvider {
117    FileProvider {
118      inputs: Mutex::new(Vec::new()),
119    }
120  }
121}
122
123unsafe impl Sync for FileProvider {}
124unsafe impl Send for FileProvider {}
125
126impl SourceProvider for FileProvider {
127  type Error = std::io::Error;
128
129  fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
130    let source = fs::read_to_string(file)?;
131    let ptr = Box::into_raw(Box::new(source));
132    self.inputs.lock().unwrap().push(ptr);
133    // SAFETY: this is safe because the pointer is not dropped
134    // until the FileProvider is, and we never remove from the
135    // list of pointers stored in the vector.
136    Ok(unsafe { &*ptr })
137  }
138
139  fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
140    // Assume the specifier is a relative file path and join it with current path.
141    Ok(originating_file.with_file_name(specifier))
142  }
143}
144
145impl Drop for FileProvider {
146  fn drop(&mut self) {
147    for ptr in self.inputs.lock().unwrap().iter() {
148      std::mem::drop(unsafe { Box::from_raw(*ptr) })
149    }
150  }
151}
152
153/// An error that could occur during bundling.
154#[derive(Debug)]
155#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
156pub enum BundleErrorKind<'i, T: std::error::Error> {
157  /// A parser error occurred.
158  ParserError(ParserError<'i>),
159  /// An unsupported `@import` condition was encountered.
160  UnsupportedImportCondition,
161  /// An unsupported cascade layer combination was encountered.
162  UnsupportedLayerCombination,
163  /// Unsupported media query boolean logic was encountered.
164  UnsupportedMediaBooleanLogic,
165  /// A custom resolver error.
166  ResolverError(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] T),
167}
168
169impl<'i, T: std::error::Error> From<Error<ParserError<'i>>> for Error<BundleErrorKind<'i, T>> {
170  fn from(err: Error<ParserError<'i>>) -> Self {
171    Error {
172      kind: BundleErrorKind::ParserError(err.kind),
173      loc: err.loc,
174    }
175  }
176}
177
178impl<'i, T: std::error::Error> std::fmt::Display for BundleErrorKind<'i, T> {
179  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180    use BundleErrorKind::*;
181    match self {
182      ParserError(err) => err.fmt(f),
183      UnsupportedImportCondition => write!(f, "Unsupported import condition"),
184      UnsupportedLayerCombination => write!(f, "Unsupported layer combination in @import"),
185      UnsupportedMediaBooleanLogic => write!(f, "Unsupported boolean logic in @import media query"),
186      ResolverError(err) => std::fmt::Display::fmt(&err, f),
187    }
188  }
189}
190
191impl<'i, T: std::error::Error> BundleErrorKind<'i, T> {
192  #[deprecated(note = "use `BundleErrorKind::to_string()` or `std::fmt::Display` instead")]
193  #[allow(missing_docs)]
194  pub fn reason(&self) -> String {
195    self.to_string()
196  }
197}
198
199impl<'a, 'o, 's, P: SourceProvider> Bundler<'a, 'o, 's, P, DefaultAtRuleParser> {
200  /// Creates a new Bundler using the given source provider.
201  /// If a source map is given, the content of each source file included in the bundle will
202  /// be added accordingly.
203  pub fn new(
204    fs: &'a P,
205    source_map: Option<&'s mut SourceMap>,
206    options: ParserOptions<'o, 'a>,
207  ) -> Bundler<'a, 'o, 's, P, DefaultAtRuleParser> {
208    Bundler {
209      source_map: source_map.map(Mutex::new),
210      fs,
211      source_indexes: DashMap::new(),
212      stylesheets: Mutex::new(Vec::new()),
213      options,
214      at_rule_parser: Mutex::new(AtRuleParserValue::Owned(DefaultAtRuleParser)),
215    }
216  }
217}
218
219impl<'a, 'o, 's, P: SourceProvider, T: AtRuleParser<'a> + Clone + Sync + Send> Bundler<'a, 'o, 's, P, T>
220where
221  T::AtRule: Sync + Send + ToCss + Clone,
222{
223  /// Creates a new Bundler using the given source provider.
224  /// If a source map is given, the content of each source file included in the bundle will
225  /// be added accordingly.
226  pub fn new_with_at_rule_parser(
227    fs: &'a P,
228    source_map: Option<&'s mut SourceMap>,
229    options: ParserOptions<'o, 'a>,
230    at_rule_parser: &'s mut T,
231  ) -> Self {
232    Bundler {
233      source_map: source_map.map(Mutex::new),
234      fs,
235      source_indexes: DashMap::new(),
236      stylesheets: Mutex::new(Vec::new()),
237      options,
238      at_rule_parser: Mutex::new(AtRuleParserValue::Borrowed(at_rule_parser)),
239    }
240  }
241
242  /// Bundles the given entry file and all dependencies into a single style sheet.
243  pub fn bundle<'e>(
244    &mut self,
245    entry: &'e Path,
246  ) -> Result<StyleSheet<'a, 'o, T::AtRule>, Error<BundleErrorKind<'a, P::Error>>> {
247    // Phase 1: load and parse all files. This is done in parallel.
248    self.load_file(
249      &entry,
250      ImportRule {
251        url: "".into(),
252        layer: None,
253        supports: None,
254        media: MediaList::new(),
255        loc: Location {
256          source_index: 0,
257          line: 0,
258          column: 0,
259        },
260      },
261    )?;
262
263    // Phase 2: determine the order that the files should be concatenated.
264    self.order();
265
266    // Phase 3: concatenate.
267    let mut rules: Vec<CssRule<'a, T::AtRule>> = Vec::new();
268    self.inline(&mut rules);
269
270    let sources = self
271      .stylesheets
272      .get_mut()
273      .unwrap()
274      .iter()
275      .flat_map(|s| s.stylesheet.as_ref().unwrap().sources.iter().cloned())
276      .collect();
277
278    let mut stylesheet = StyleSheet::new(sources, CssRuleList(rules), self.options.clone());
279
280    stylesheet.source_map_urls = self
281      .stylesheets
282      .get_mut()
283      .unwrap()
284      .iter()
285      .flat_map(|s| s.stylesheet.as_ref().unwrap().source_map_urls.iter().cloned())
286      .collect();
287
288    stylesheet.license_comments = self
289      .stylesheets
290      .get_mut()
291      .unwrap()
292      .iter()
293      .flat_map(|s| s.stylesheet.as_ref().unwrap().license_comments.iter().cloned())
294      .collect();
295
296    if let Some(config) = &self.options.css_modules {
297      if config.pattern.has_content_hash() {
298        stylesheet.content_hashes = Some(
299          self
300            .stylesheets
301            .get_mut()
302            .unwrap()
303            .iter()
304            .flat_map(|s| {
305              let s = s.stylesheet.as_ref().unwrap();
306              s.content_hashes.as_ref().unwrap().iter().cloned()
307            })
308            .collect(),
309        );
310      }
311    }
312
313    Ok(stylesheet)
314  }
315
316  fn find_filename(&self, source_index: u32) -> String {
317    // This function is only used for error handling, so it's ok if this is a bit slow.
318    let entry = self.source_indexes.iter().find(|x| *x.value() == source_index).unwrap();
319    entry.key().to_str().unwrap().into()
320  }
321
322  fn load_file(&self, file: &Path, rule: ImportRule<'a>) -> Result<u32, Error<BundleErrorKind<'a, P::Error>>> {
323    // Check if we already loaded this file.
324    let mut stylesheets = self.stylesheets.lock().unwrap();
325    let source_index = match self.source_indexes.get(file) {
326      Some(source_index) => {
327        // If we already loaded this file, combine the media queries and supports conditions
328        // from this import rule with the existing ones using a logical or operator.
329        let entry = &mut stylesheets[*source_index as usize];
330
331        // We cannot combine a media query and a supports query from different @import rules.
332        // e.g. @import "a.css" print; @import "a.css" supports(color: red);
333        // This would require duplicating the actual rules in the file.
334        if (!rule.media.media_queries.is_empty() && !entry.supports.is_none())
335          || (!entry.media.media_queries.is_empty() && !rule.supports.is_none())
336        {
337          return Err(Error {
338            kind: BundleErrorKind::UnsupportedImportCondition,
339            loc: Some(ErrorLocation::new(rule.loc, self.find_filename(rule.loc.source_index))),
340          });
341        }
342
343        if rule.media.media_queries.is_empty() {
344          entry.media.media_queries.clear();
345        } else if !entry.media.media_queries.is_empty() {
346          entry.media.or(&rule.media);
347        }
348
349        if let Some(supports) = rule.supports {
350          if let Some(existing_supports) = &mut entry.supports {
351            existing_supports.or(&supports)
352          }
353        } else {
354          entry.supports = None;
355        }
356
357        if let Some(layer) = &rule.layer {
358          if let Some(existing_layer) = &entry.layer {
359            // We can't OR layer names without duplicating all of the nested rules, so error for now.
360            if layer != existing_layer || (layer.is_none() && existing_layer.is_none()) {
361              return Err(Error {
362                kind: BundleErrorKind::UnsupportedLayerCombination,
363                loc: Some(ErrorLocation::new(rule.loc, self.find_filename(rule.loc.source_index))),
364              });
365            }
366          } else {
367            entry.layer = rule.layer;
368          }
369        }
370
371        return Ok(*source_index);
372      }
373      None => {
374        let source_index = stylesheets.len() as u32;
375        self.source_indexes.insert(file.to_owned(), source_index);
376
377        stylesheets.push(BundleStyleSheet {
378          stylesheet: None,
379          layer: rule.layer.clone(),
380          media: rule.media.clone(),
381          supports: rule.supports.clone(),
382          loc: rule.loc.clone(),
383          dependencies: Vec::new(),
384          css_modules_deps: Vec::new(),
385          parent_source_index: 0,
386          parent_dep_index: 0,
387        });
388
389        source_index
390      }
391    };
392
393    drop(stylesheets); // ensure we aren't holding the lock anymore
394
395    let code = self.fs.read(file).map_err(|e| Error {
396      kind: BundleErrorKind::ResolverError(e),
397      loc: if rule.loc.column == 0 {
398        None
399      } else {
400        Some(ErrorLocation::new(rule.loc, self.find_filename(rule.loc.source_index)))
401      },
402    })?;
403
404    let mut opts = self.options.clone();
405    let filename = file.to_str().unwrap();
406    opts.filename = filename.to_owned();
407    opts.source_index = source_index;
408
409    let mut stylesheet = {
410      let mut at_rule_parser = self.at_rule_parser.lock().unwrap();
411      let at_rule_parser = match &mut *at_rule_parser {
412        AtRuleParserValue::Owned(owned) => owned,
413        AtRuleParserValue::Borrowed(borrowed) => *borrowed,
414      };
415
416      StyleSheet::<T::AtRule>::parse_with(code, opts, at_rule_parser)?
417    };
418
419    if let Some(source_map) = &self.source_map {
420      // Only add source if we don't have an input source map.
421      // If we do, this will be handled by the printer when remapping locations.
422      let sm = stylesheet.source_map_url(0);
423      if sm.is_none() || !sm.unwrap().starts_with("data") {
424        let mut source_map = source_map.lock().unwrap();
425        let source_index = source_map.add_source(filename);
426        let _ = source_map.set_source_content(source_index as usize, code);
427      }
428    }
429
430    // Collect and load dependencies for this stylesheet in parallel.
431    let dependencies: Result<Vec<u32>, _> = stylesheet
432      .rules
433      .0
434      .par_iter_mut()
435      .filter_map(|r| {
436        // Prepend parent layer name to @layer statements.
437        if let CssRule::LayerStatement(layer) = r {
438          if let Some(Some(parent_layer)) = &rule.layer {
439            for name in &mut layer.names {
440              name.0.insert_many(0, parent_layer.0.iter().cloned())
441            }
442          }
443        }
444
445        if let CssRule::Import(import) = r {
446          let specifier = &import.url;
447
448          // Combine media queries and supports conditions from parent
449          // stylesheet with @import rule using a logical and operator.
450          let mut media = rule.media.clone();
451          let result = media.and(&import.media).map_err(|_| Error {
452            kind: BundleErrorKind::UnsupportedMediaBooleanLogic,
453            loc: Some(ErrorLocation::new(
454              import.loc,
455              self.find_filename(import.loc.source_index),
456            )),
457          });
458
459          if let Err(e) = result {
460            return Some(Err(e));
461          }
462
463          let layer = if (rule.layer == Some(None) && import.layer.is_some())
464            || (import.layer == Some(None) && rule.layer.is_some())
465          {
466            // Cannot combine anonymous layers
467            return Some(Err(Error {
468              kind: BundleErrorKind::UnsupportedLayerCombination,
469              loc: Some(ErrorLocation::new(
470                import.loc,
471                self.find_filename(import.loc.source_index),
472              )),
473            }));
474          } else if let Some(Some(a)) = &rule.layer {
475            if let Some(Some(b)) = &import.layer {
476              let mut name = a.clone();
477              name.0.extend(b.0.iter().cloned());
478              Some(Some(name))
479            } else {
480              Some(Some(a.clone()))
481            }
482          } else {
483            import.layer.clone()
484          };
485
486          let result = match self.fs.resolve(&specifier, file) {
487            Ok(path) => self.load_file(
488              &path,
489              ImportRule {
490                layer,
491                media,
492                supports: combine_supports(rule.supports.clone(), &import.supports),
493                url: "".into(),
494                loc: import.loc,
495              },
496            ),
497            Err(err) => Err(Error {
498              kind: BundleErrorKind::ResolverError(err),
499              loc: Some(ErrorLocation::new(
500                import.loc,
501                self.find_filename(import.loc.source_index),
502              )),
503            }),
504          };
505
506          Some(result)
507        } else {
508          None
509        }
510      })
511      .collect();
512
513    // Collect CSS modules dependencies from the `composes` property.
514    let css_modules_deps: Result<Vec<u32>, _> = if self.options.css_modules.is_some() {
515      stylesheet
516        .rules
517        .0
518        .par_iter_mut()
519        .filter_map(|r| {
520          if let CssRule::Style(style) = r {
521            Some(
522              style
523                .declarations
524                .declarations
525                .par_iter_mut()
526                .chain(style.declarations.important_declarations.par_iter_mut())
527                .filter_map(|d| match d {
528                  Property::Composes(composes) => self
529                    .add_css_module_dep(file, &rule, style.loc, composes.loc, &mut composes.from)
530                    .map(|result| rayon::iter::Either::Left(rayon::iter::once(result))),
531
532                  // Handle variable references if the dashed_idents option is present.
533                  Property::Custom(CustomProperty { value, .. })
534                  | Property::Unparsed(UnparsedProperty { value, .. })
535                    if matches!(&self.options.css_modules, Some(css_modules) if css_modules.dashed_idents) =>
536                  {
537                    Some(rayon::iter::Either::Right(visit_vars(value).filter_map(|name| {
538                      self.add_css_module_dep(
539                        file,
540                        &rule,
541                        style.loc,
542                        // TODO: store loc in variable reference?
543                        crate::dependencies::Location {
544                          line: style.loc.line,
545                          column: style.loc.column,
546                        },
547                        &mut name.from,
548                      )
549                    })))
550                  }
551                  _ => None,
552                })
553                .flatten(),
554            )
555          } else {
556            None
557          }
558        })
559        .flatten()
560        .collect()
561    } else {
562      Ok(vec![])
563    };
564
565    let entry = &mut self.stylesheets.lock().unwrap()[source_index as usize];
566    entry.stylesheet = Some(stylesheet);
567    entry.dependencies = dependencies?;
568    entry.css_modules_deps = css_modules_deps?;
569
570    Ok(source_index)
571  }
572
573  fn add_css_module_dep(
574    &self,
575    file: &Path,
576    rule: &ImportRule<'a>,
577    style_loc: Location,
578    loc: crate::dependencies::Location,
579    specifier: &mut Option<Specifier>,
580  ) -> Option<Result<u32, Error<BundleErrorKind<'a, P::Error>>>> {
581    if let Some(Specifier::File(f)) = specifier {
582      let result = match self.fs.resolve(&f, file) {
583        Ok(path) => {
584          let res = self.load_file(
585            &path,
586            ImportRule {
587              layer: rule.layer.clone(),
588              media: rule.media.clone(),
589              supports: rule.supports.clone(),
590              url: "".into(),
591              loc: Location {
592                source_index: style_loc.source_index,
593                line: loc.line,
594                column: loc.column,
595              },
596            },
597          );
598
599          if let Ok(source_index) = res {
600            *specifier = Some(Specifier::SourceIndex(source_index));
601          }
602
603          res
604        }
605        Err(err) => Err(Error {
606          kind: BundleErrorKind::ResolverError(err),
607          loc: Some(ErrorLocation::new(
608            style_loc,
609            self.find_filename(style_loc.source_index),
610          )),
611        }),
612      };
613      Some(result)
614    } else {
615      None
616    }
617  }
618
619  fn order(&mut self) {
620    process(self.stylesheets.get_mut().unwrap(), 0, &mut HashSet::new());
621
622    fn process<'i, T>(
623      stylesheets: &mut Vec<BundleStyleSheet<'i, '_, T>>,
624      source_index: u32,
625      visited: &mut HashSet<u32>,
626    ) {
627      if visited.contains(&source_index) {
628        return;
629      }
630
631      visited.insert(source_index);
632
633      let mut dep_index = 0;
634      for i in 0..stylesheets[source_index as usize].css_modules_deps.len() {
635        let dep_source_index = stylesheets[source_index as usize].css_modules_deps[i];
636        let resolved = &mut stylesheets[dep_source_index as usize];
637
638        // CSS modules preserve the first instance of composed stylesheets.
639        if !visited.contains(&dep_source_index) {
640          resolved.parent_dep_index = dep_index;
641          resolved.parent_source_index = source_index;
642          process(stylesheets, dep_source_index, visited);
643        }
644
645        dep_index += 1;
646      }
647
648      for i in 0..stylesheets[source_index as usize].dependencies.len() {
649        let dep_source_index = stylesheets[source_index as usize].dependencies[i];
650        let resolved = &mut stylesheets[dep_source_index as usize];
651
652        // In browsers, every instance of an @import is evaluated, so we preserve the last.
653        resolved.parent_dep_index = dep_index;
654        resolved.parent_source_index = source_index;
655
656        process(stylesheets, dep_source_index, visited);
657        dep_index += 1;
658      }
659    }
660  }
661
662  fn inline(&mut self, dest: &mut Vec<CssRule<'a, T::AtRule>>) {
663    process(self.stylesheets.get_mut().unwrap(), 0, dest);
664
665    fn process<'a, T>(
666      stylesheets: &mut Vec<BundleStyleSheet<'a, '_, T>>,
667      source_index: u32,
668      dest: &mut Vec<CssRule<'a, T>>,
669    ) {
670      let stylesheet = &mut stylesheets[source_index as usize];
671      let mut rules = std::mem::take(&mut stylesheet.stylesheet.as_mut().unwrap().rules.0);
672
673      // Hoist css modules deps
674      let mut dep_index = 0;
675      for i in 0..stylesheet.css_modules_deps.len() {
676        let dep_source_index = stylesheets[source_index as usize].css_modules_deps[i];
677        let resolved = &stylesheets[dep_source_index as usize];
678
679        // Include the dependency if this is the first instance as computed earlier.
680        if resolved.parent_source_index == source_index && resolved.parent_dep_index == dep_index as u32 {
681          process(stylesheets, dep_source_index, dest);
682        }
683
684        dep_index += 1;
685      }
686
687      let mut import_index = 0;
688      for rule in &mut rules {
689        match rule {
690          CssRule::Import(_) => {
691            let dep_source_index = stylesheets[source_index as usize].dependencies[import_index];
692            let resolved = &stylesheets[dep_source_index as usize];
693
694            // Include the dependency if this is the last instance as computed earlier.
695            if resolved.parent_source_index == source_index && resolved.parent_dep_index == dep_index {
696              process(stylesheets, dep_source_index, dest);
697            }
698
699            *rule = CssRule::Ignored;
700            dep_index += 1;
701            import_index += 1;
702          }
703          CssRule::LayerStatement(_) => {
704            // @layer rules are the only rules that may appear before an @import.
705            // We must preserve this order to ensure correctness.
706            let layer = std::mem::replace(rule, CssRule::Ignored);
707            dest.push(layer);
708          }
709          CssRule::Ignored => {}
710          _ => break,
711        }
712      }
713
714      // Wrap rules in the appropriate @layer, @media, and @supports rules.
715      let stylesheet = &mut stylesheets[source_index as usize];
716
717      if stylesheet.layer.is_some() {
718        rules = vec![CssRule::LayerBlock(LayerBlockRule {
719          name: stylesheet.layer.take().unwrap(),
720          rules: CssRuleList(rules),
721          loc: stylesheet.loc,
722        })]
723      }
724
725      if !stylesheet.media.media_queries.is_empty() {
726        rules = vec![CssRule::Media(MediaRule {
727          query: std::mem::replace(&mut stylesheet.media, MediaList::new()),
728          rules: CssRuleList(rules),
729          loc: stylesheet.loc,
730        })]
731      }
732
733      if stylesheet.supports.is_some() {
734        rules = vec![CssRule::Supports(SupportsRule {
735          condition: stylesheet.supports.take().unwrap(),
736          rules: CssRuleList(rules),
737          loc: stylesheet.loc,
738        })]
739      }
740
741      dest.extend(rules);
742    }
743  }
744}
745
746fn combine_supports<'a>(
747  a: Option<SupportsCondition<'a>>,
748  b: &Option<SupportsCondition<'a>>,
749) -> Option<SupportsCondition<'a>> {
750  if let Some(mut a) = a {
751    if let Some(b) = b {
752      a.and(b)
753    }
754    Some(a)
755  } else {
756    b.clone()
757  }
758}
759
760fn visit_vars<'a, 'b>(
761  token_list: &'b mut TokenList<'a>,
762) -> impl ParallelIterator<Item = &'b mut DashedIdentReference<'a>> {
763  let mut stack = vec![token_list.0.iter_mut()];
764  std::iter::from_fn(move || {
765    while !stack.is_empty() {
766      let iter = stack.last_mut().unwrap();
767      match iter.next() {
768        Some(TokenOrValue::Var(var)) => {
769          if let Some(fallback) = &mut var.fallback {
770            stack.push(fallback.0.iter_mut());
771          }
772          return Some(&mut var.name);
773        }
774        Some(TokenOrValue::Env(env)) => {
775          if let Some(fallback) = &mut env.fallback {
776            stack.push(fallback.0.iter_mut());
777          }
778          if let EnvironmentVariableName::Custom(name) = &mut env.name {
779            return Some(name);
780          }
781        }
782        Some(TokenOrValue::UnresolvedColor(color)) => match color {
783          UnresolvedColor::RGB { alpha, .. } | UnresolvedColor::HSL { alpha, .. } => {
784            stack.push(alpha.0.iter_mut());
785          }
786          UnresolvedColor::LightDark { light, dark } => {
787            stack.push(light.0.iter_mut());
788            stack.push(dark.0.iter_mut());
789          }
790        },
791        None => {
792          stack.pop();
793        }
794        _ => {}
795      }
796    }
797    None
798  })
799  .par_bridge()
800}
801
802#[cfg(test)]
803mod tests {
804  use super::*;
805  use crate::{
806    css_modules::{self, CssModuleExports, CssModuleReference},
807    parser::ParserFlags,
808    stylesheet::{MinifyOptions, PrinterOptions},
809    targets::{Browsers, Targets},
810  };
811  use indoc::indoc;
812  use std::collections::HashMap;
813
814  #[derive(Clone)]
815  struct TestProvider {
816    map: HashMap<PathBuf, String>,
817  }
818
819  impl SourceProvider for TestProvider {
820    type Error = std::io::Error;
821
822    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
823      Ok(self.map.get(file).unwrap())
824    }
825
826    fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
827      Ok(originating_file.with_file_name(specifier))
828    }
829  }
830
831  /// Stand-in for a user-authored `SourceProvider` with application-specific logic.
832  struct CustomProvider {
833    map: HashMap<PathBuf, String>,
834  }
835
836  impl SourceProvider for CustomProvider {
837    type Error = std::io::Error;
838
839    /// Read files from in-memory map.
840    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
841      Ok(self.map.get(file).unwrap())
842    }
843
844    /// Resolve by stripping a `foo:` prefix off any import. Specifiers without
845    /// this prefix fail with an error.
846    fn resolve(&self, specifier: &str, _originating_file: &Path) -> Result<PathBuf, Self::Error> {
847      if specifier.starts_with("foo:") {
848        Ok(Path::new(&specifier["foo:".len()..]).to_path_buf())
849      } else {
850        let err = std::io::Error::new(
851          std::io::ErrorKind::NotFound,
852          format!(
853            "Failed to resolve `{}`, specifier does not start with `foo:`.",
854            &specifier
855          ),
856        );
857
858        Err(err)
859      }
860    }
861  }
862
863  macro_rules! fs(
864    { $($key:literal: $value:expr),* } => {
865      {
866        #[allow(unused_mut)]
867        let mut m = HashMap::new();
868        $(
869          m.insert(PathBuf::from($key), $value.to_owned());
870        )*
871        m
872      }
873    };
874  );
875
876  fn bundle<P: SourceProvider>(fs: P, entry: &str) -> String {
877    let mut bundler = Bundler::new(&fs, None, ParserOptions::default());
878    let stylesheet = bundler.bundle(Path::new(entry)).unwrap();
879    stylesheet.to_css(PrinterOptions::default()).unwrap().code
880  }
881
882  fn bundle_css_module<P: SourceProvider>(
883    fs: P,
884    entry: &str,
885    project_root: Option<&str>,
886  ) -> (String, CssModuleExports) {
887    bundle_css_module_with_pattern(fs, entry, project_root, "[hash]_[local]")
888  }
889
890  fn bundle_css_module_with_pattern<P: SourceProvider>(
891    fs: P,
892    entry: &str,
893    project_root: Option<&str>,
894    pattern: &'static str,
895  ) -> (String, CssModuleExports) {
896    let mut bundler = Bundler::new(
897      &fs,
898      None,
899      ParserOptions {
900        css_modules: Some(css_modules::Config {
901          dashed_idents: true,
902          pattern: css_modules::Pattern::parse(pattern).unwrap(),
903          ..Default::default()
904        }),
905        ..ParserOptions::default()
906      },
907    );
908    let mut stylesheet = bundler.bundle(Path::new(entry)).unwrap();
909    stylesheet.minify(MinifyOptions::default()).unwrap();
910    let res = stylesheet
911      .to_css(PrinterOptions {
912        project_root,
913        ..PrinterOptions::default()
914      })
915      .unwrap();
916    (res.code, res.exports.unwrap())
917  }
918
919  fn bundle_custom_media<P: SourceProvider>(fs: P, entry: &str) -> String {
920    let mut bundler = Bundler::new(
921      &fs,
922      None,
923      ParserOptions {
924        flags: ParserFlags::CUSTOM_MEDIA,
925        ..ParserOptions::default()
926      },
927    );
928    let mut stylesheet = bundler.bundle(Path::new(entry)).unwrap();
929    let targets = Targets {
930      browsers: Some(Browsers {
931        safari: Some(13 << 16),
932        ..Browsers::default()
933      }),
934      ..Default::default()
935    };
936    stylesheet
937      .minify(MinifyOptions {
938        targets,
939        ..MinifyOptions::default()
940      })
941      .unwrap();
942    stylesheet
943      .to_css(PrinterOptions {
944        targets,
945        ..PrinterOptions::default()
946      })
947      .unwrap()
948      .code
949  }
950
951  fn error_test<P: SourceProvider>(
952    fs: P,
953    entry: &str,
954    maybe_cb: Option<Box<dyn FnOnce(BundleErrorKind<P::Error>) -> ()>>,
955  ) {
956    let mut bundler = Bundler::new(&fs, None, ParserOptions::default());
957    let res = bundler.bundle(Path::new(entry));
958    match res {
959      Ok(_) => unreachable!(),
960      Err(e) => {
961        if let Some(cb) = maybe_cb {
962          cb(e.kind);
963        }
964      }
965    }
966  }
967
968  fn flatten_exports(exports: CssModuleExports) -> HashMap<String, String> {
969    let mut res = HashMap::new();
970    for (name, export) in &exports {
971      let mut classes = export.name.clone();
972      for composes in &export.composes {
973        classes.push(' ');
974        classes.push_str(match composes {
975          CssModuleReference::Local { name } => name,
976          CssModuleReference::Global { name } => name,
977          _ => unreachable!(),
978        })
979      }
980      res.insert(name.clone(), classes);
981    }
982    res
983  }
984
985  #[test]
986  fn test_bundle() {
987    let res = bundle(
988      TestProvider {
989        map: fs! {
990          "/a.css": r#"
991          @import "b.css";
992          .a { color: red }
993        "#,
994          "/b.css": r#"
995          .b { color: green }
996        "#
997        },
998      },
999      "/a.css",
1000    );
1001    assert_eq!(
1002      res,
1003      indoc! { r#"
1004      .b {
1005        color: green;
1006      }
1007
1008      .a {
1009        color: red;
1010      }
1011    "#}
1012    );
1013
1014    let res = bundle(
1015      TestProvider {
1016        map: fs! {
1017          "/a.css": r#"
1018          @import "b.css" print;
1019          .a { color: red }
1020        "#,
1021          "/b.css": r#"
1022          .b { color: green }
1023        "#
1024        },
1025      },
1026      "/a.css",
1027    );
1028    assert_eq!(
1029      res,
1030      indoc! { r#"
1031      @media print {
1032        .b {
1033          color: green;
1034        }
1035      }
1036
1037      .a {
1038        color: red;
1039      }
1040    "#}
1041    );
1042
1043    let res = bundle(
1044      TestProvider {
1045        map: fs! {
1046          "/a.css": r#"
1047          @import "b.css" supports(color: green);
1048          .a { color: red }
1049        "#,
1050          "/b.css": r#"
1051          .b { color: green }
1052        "#
1053        },
1054      },
1055      "/a.css",
1056    );
1057    assert_eq!(
1058      res,
1059      indoc! { r#"
1060      @supports (color: green) {
1061        .b {
1062          color: green;
1063        }
1064      }
1065
1066      .a {
1067        color: red;
1068      }
1069    "#}
1070    );
1071
1072    let res = bundle(
1073      TestProvider {
1074        map: fs! {
1075          "/a.css": r#"
1076          @import "b.css" supports(color: green) print;
1077          .a { color: red }
1078        "#,
1079          "/b.css": r#"
1080          .b { color: green }
1081        "#
1082        },
1083      },
1084      "/a.css",
1085    );
1086    assert_eq!(
1087      res,
1088      indoc! { r#"
1089      @supports (color: green) {
1090        @media print {
1091          .b {
1092            color: green;
1093          }
1094        }
1095      }
1096
1097      .a {
1098        color: red;
1099      }
1100    "#}
1101    );
1102
1103    let res = bundle(
1104      TestProvider {
1105        map: fs! {
1106          "/a.css": r#"
1107          @import "b.css" print;
1108          @import "b.css" screen;
1109          .a { color: red }
1110        "#,
1111          "/b.css": r#"
1112          .b { color: green }
1113        "#
1114        },
1115      },
1116      "/a.css",
1117    );
1118    assert_eq!(
1119      res,
1120      indoc! { r#"
1121      @media print, screen {
1122        .b {
1123          color: green;
1124        }
1125      }
1126
1127      .a {
1128        color: red;
1129      }
1130    "#}
1131    );
1132
1133    let res = bundle(
1134      TestProvider {
1135        map: fs! {
1136          "/a.css": r#"
1137          @import "b.css" supports(color: red);
1138          @import "b.css" supports(foo: bar);
1139          .a { color: red }
1140        "#,
1141          "/b.css": r#"
1142          .b { color: green }
1143        "#
1144        },
1145      },
1146      "/a.css",
1147    );
1148    assert_eq!(
1149      res,
1150      indoc! { r#"
1151      @supports (color: red) or (foo: bar) {
1152        .b {
1153          color: green;
1154        }
1155      }
1156
1157      .a {
1158        color: red;
1159      }
1160    "#}
1161    );
1162
1163    let res = bundle(
1164      TestProvider {
1165        map: fs! {
1166          "/a.css": r#"
1167          @import "b.css" print;
1168          .a { color: red }
1169        "#,
1170          "/b.css": r#"
1171          @import "c.css" (color);
1172          .b { color: yellow }
1173        "#,
1174          "/c.css": r#"
1175          .c { color: green }
1176        "#
1177        },
1178      },
1179      "/a.css",
1180    );
1181    assert_eq!(
1182      res,
1183      indoc! { r#"
1184      @media print and (color) {
1185        .c {
1186          color: green;
1187        }
1188      }
1189
1190      @media print {
1191        .b {
1192          color: #ff0;
1193        }
1194      }
1195
1196      .a {
1197        color: red;
1198      }
1199    "#}
1200    );
1201
1202    let res = bundle(
1203      TestProvider {
1204        map: fs! {
1205          "/a.css": r#"
1206          @import "b.css";
1207          .a { color: red }
1208        "#,
1209          "/b.css": r#"
1210          @import "c.css";
1211        "#,
1212          "/c.css": r#"
1213          @import "a.css";
1214          .c { color: green }
1215        "#
1216        },
1217      },
1218      "/a.css",
1219    );
1220    assert_eq!(
1221      res,
1222      indoc! { r#"
1223      .c {
1224        color: green;
1225      }
1226
1227      .a {
1228        color: red;
1229      }
1230    "#}
1231    );
1232
1233    let res = bundle(
1234      TestProvider {
1235        map: fs! {
1236          "/a.css": r#"
1237          @import "b/c.css";
1238          .a { color: red }
1239        "#,
1240          "/b/c.css": r#"
1241          .b { color: green }
1242        "#
1243        },
1244      },
1245      "/a.css",
1246    );
1247    assert_eq!(
1248      res,
1249      indoc! { r#"
1250      .b {
1251        color: green;
1252      }
1253
1254      .a {
1255        color: red;
1256      }
1257    "#}
1258    );
1259
1260    let res = bundle(
1261      TestProvider {
1262        map: fs! {
1263          "/a.css": r#"
1264          @import "./b/c.css";
1265          .a { color: red }
1266        "#,
1267          "/b/c.css": r#"
1268          .b { color: green }
1269        "#
1270        },
1271      },
1272      "/a.css",
1273    );
1274    assert_eq!(
1275      res,
1276      indoc! { r#"
1277      .b {
1278        color: green;
1279      }
1280
1281      .a {
1282        color: red;
1283      }
1284    "#}
1285    );
1286
1287    let res = bundle_custom_media(
1288      TestProvider {
1289        map: fs! {
1290          "/a.css": r#"
1291          @import "media.css";
1292          @import "b.css";
1293          .a { color: red }
1294        "#,
1295          "/media.css": r#"
1296          @custom-media --foo print;
1297        "#,
1298          "/b.css": r#"
1299          @media (--foo) {
1300            .a { color: green }
1301          }
1302        "#
1303        },
1304      },
1305      "/a.css",
1306    );
1307    assert_eq!(
1308      res,
1309      indoc! { r#"
1310      @media print {
1311        .a {
1312          color: green;
1313        }
1314      }
1315
1316      .a {
1317        color: red;
1318      }
1319    "#}
1320    );
1321
1322    let res = bundle(
1323      TestProvider {
1324        map: fs! {
1325          "/a.css": r#"
1326          @import "b.css" layer(foo);
1327          .a { color: red }
1328        "#,
1329          "/b.css": r#"
1330          .b { color: green }
1331        "#
1332        },
1333      },
1334      "/a.css",
1335    );
1336    assert_eq!(
1337      res,
1338      indoc! { r#"
1339      @layer foo {
1340        .b {
1341          color: green;
1342        }
1343      }
1344
1345      .a {
1346        color: red;
1347      }
1348    "#}
1349    );
1350
1351    let res = bundle(
1352      TestProvider {
1353        map: fs! {
1354          "/a.css": r#"
1355          @import "b.css" layer;
1356          .a { color: red }
1357        "#,
1358          "/b.css": r#"
1359          .b { color: green }
1360        "#
1361        },
1362      },
1363      "/a.css",
1364    );
1365    assert_eq!(
1366      res,
1367      indoc! { r#"
1368      @layer {
1369        .b {
1370          color: green;
1371        }
1372      }
1373
1374      .a {
1375        color: red;
1376      }
1377    "#}
1378    );
1379
1380    let res = bundle(
1381      TestProvider {
1382        map: fs! {
1383          "/a.css": r#"
1384          @import "b.css" layer(foo);
1385          .a { color: red }
1386        "#,
1387          "/b.css": r#"
1388          @import "c.css" layer(bar);
1389          .b { color: green }
1390        "#,
1391          "/c.css": r#"
1392          .c { color: green }
1393        "#
1394        },
1395      },
1396      "/a.css",
1397    );
1398    assert_eq!(
1399      res,
1400      indoc! { r#"
1401      @layer foo.bar {
1402        .c {
1403          color: green;
1404        }
1405      }
1406
1407      @layer foo {
1408        .b {
1409          color: green;
1410        }
1411      }
1412
1413      .a {
1414        color: red;
1415      }
1416    "#}
1417    );
1418
1419    let res = bundle(
1420      TestProvider {
1421        map: fs! {
1422          "/a.css": r#"
1423          @import "b.css" layer(foo);
1424          @import "b.css" layer(foo);
1425        "#,
1426          "/b.css": r#"
1427          .b { color: green }
1428        "#
1429        },
1430      },
1431      "/a.css",
1432    );
1433    assert_eq!(
1434      res,
1435      indoc! { r#"
1436      @layer foo {
1437        .b {
1438          color: green;
1439        }
1440      }
1441    "#}
1442    );
1443
1444    let res = bundle(
1445      TestProvider {
1446        map: fs! {
1447          "/a.css": r#"
1448          @layer bar, foo;
1449          @import "b.css" layer(foo);
1450
1451          @layer bar {
1452            div {
1453              background: red;
1454            }
1455          }
1456        "#,
1457          "/b.css": r#"
1458          @layer qux, baz;
1459          @import "c.css" layer(baz);
1460
1461          @layer qux {
1462            div {
1463              background: green;
1464            }
1465          }
1466        "#,
1467          "/c.css": r#"
1468          div {
1469            background: yellow;
1470          }
1471        "#
1472        },
1473      },
1474      "/a.css",
1475    );
1476    assert_eq!(
1477      res,
1478      indoc! { r#"
1479      @layer bar, foo;
1480      @layer foo.qux, foo.baz;
1481
1482      @layer foo.baz {
1483        div {
1484          background: #ff0;
1485        }
1486      }
1487
1488      @layer foo {
1489        @layer qux {
1490          div {
1491            background: green;
1492          }
1493        }
1494      }
1495
1496      @layer bar {
1497        div {
1498          background: red;
1499        }
1500      }
1501    "#}
1502    );
1503
1504    // Layer order depends on @import conditions.
1505    let res = bundle(
1506      TestProvider {
1507        map: fs! {
1508          "/a.css": r#"
1509          @import "b.css" layer(bar) (min-width: 1000px);
1510
1511          @layer baz {
1512            #box { background: purple }
1513          }
1514
1515          @layer bar {
1516            #box { background: yellow }
1517          }
1518        "#,
1519          "/b.css": r#"
1520          #box { background: green }
1521        "#
1522        },
1523      },
1524      "/a.css",
1525    );
1526    assert_eq!(
1527      res,
1528      indoc! { r#"
1529      @media (width >= 1000px) {
1530        @layer bar {
1531          #box {
1532            background: green;
1533          }
1534        }
1535      }
1536
1537      @layer baz {
1538        #box {
1539          background: purple;
1540        }
1541      }
1542
1543      @layer bar {
1544        #box {
1545          background: #ff0;
1546        }
1547      }
1548    "#}
1549    );
1550
1551    error_test(
1552      TestProvider {
1553        map: fs! {
1554          "/a.css": r#"
1555          @import "b.css" layer(foo);
1556          @import "b.css" layer(bar);
1557        "#,
1558          "/b.css": r#"
1559          .b { color: red }
1560        "#
1561        },
1562      },
1563      "/a.css",
1564      Some(Box::new(|err| {
1565        assert!(matches!(err, BundleErrorKind::UnsupportedLayerCombination));
1566      })),
1567    );
1568
1569    error_test(
1570      TestProvider {
1571        map: fs! {
1572          "/a.css": r#"
1573          @import "b.css" layer;
1574          @import "b.css" layer;
1575        "#,
1576          "/b.css": r#"
1577          .b { color: red }
1578        "#
1579        },
1580      },
1581      "/a.css",
1582      Some(Box::new(|err| {
1583        assert!(matches!(err, BundleErrorKind::UnsupportedLayerCombination));
1584      })),
1585    );
1586
1587    error_test(
1588      TestProvider {
1589        map: fs! {
1590          "/a.css": r#"
1591          @import "b.css" layer;
1592          .a { color: red }
1593        "#,
1594          "/b.css": r#"
1595          @import "c.css" layer;
1596          .b { color: green }
1597        "#,
1598          "/c.css": r#"
1599          .c { color: green }
1600        "#
1601        },
1602      },
1603      "/a.css",
1604      Some(Box::new(|err| {
1605        assert!(matches!(err, BundleErrorKind::UnsupportedLayerCombination));
1606      })),
1607    );
1608
1609    error_test(
1610      TestProvider {
1611        map: fs! {
1612          "/a.css": r#"
1613          @import "b.css" layer;
1614          .a { color: red }
1615        "#,
1616          "/b.css": r#"
1617          @import "c.css" layer(foo);
1618          .b { color: green }
1619        "#,
1620          "/c.css": r#"
1621          .c { color: green }
1622        "#
1623        },
1624      },
1625      "/a.css",
1626      Some(Box::new(|err| {
1627        assert!(matches!(err, BundleErrorKind::UnsupportedLayerCombination));
1628      })),
1629    );
1630
1631    let res = bundle(
1632      TestProvider {
1633        map: fs! {
1634          "/index.css": r#"
1635          @import "a.css";
1636          @import "b.css";
1637        "#,
1638          "/a.css": r#"
1639          @import "./c.css";
1640          body { background: red; }
1641        "#,
1642          "/b.css": r#"
1643          @import "./c.css";
1644          body { color: red; }
1645        "#,
1646          "/c.css": r#"
1647          body {
1648            background: white;
1649            color: black;
1650          }
1651        "#
1652        },
1653      },
1654      "/index.css",
1655    );
1656    assert_eq!(
1657      res,
1658      indoc! { r#"
1659      body {
1660        background: red;
1661      }
1662
1663      body {
1664        background: #fff;
1665        color: #000;
1666      }
1667
1668      body {
1669        color: red;
1670      }
1671    "#}
1672    );
1673
1674    let res = bundle(
1675      TestProvider {
1676        map: fs! {
1677          "/index.css": r#"
1678          @import "a.css";
1679          @import "b.css";
1680          @import "a.css";
1681        "#,
1682          "/a.css": r#"
1683          body { background: green; }
1684        "#,
1685          "/b.css": r#"
1686          body { background: red; }
1687        "#
1688        },
1689      },
1690      "/index.css",
1691    );
1692    assert_eq!(
1693      res,
1694      indoc! { r#"
1695      body {
1696        background: red;
1697      }
1698
1699      body {
1700        background: green;
1701      }
1702    "#}
1703    );
1704
1705    let res = bundle(
1706      CustomProvider {
1707        map: fs! {
1708          "/a.css": r#"
1709            @import "foo:/b.css";
1710            .a { color: red; }
1711          "#,
1712          "/b.css": ".b { color: green; }"
1713        },
1714      },
1715      "/a.css",
1716    );
1717    assert_eq!(
1718      res,
1719      indoc! { r#"
1720        .b {
1721          color: green;
1722        }
1723
1724        .a {
1725          color: red;
1726        }
1727      "# }
1728    );
1729
1730    error_test(
1731      CustomProvider {
1732        map: fs! {
1733          "/a.css": r#"
1734            /* Forgot to prefix with `foo:`. */
1735            @import "/b.css";
1736            .a { color: red; }
1737          "#,
1738          "/b.css": ".b { color: green; }"
1739        },
1740      },
1741      "/a.css",
1742      Some(Box::new(|err| {
1743        let kind = match err {
1744          BundleErrorKind::ResolverError(ref error) => error.kind(),
1745          _ => unreachable!(),
1746        };
1747        assert!(matches!(kind, std::io::ErrorKind::NotFound));
1748        assert!(err
1749          .to_string()
1750          .contains("Failed to resolve `/b.css`, specifier does not start with `foo:`."));
1751      })),
1752    );
1753
1754    // let res = bundle(fs! {
1755    //   "/a.css": r#"
1756    //     @import "b.css" supports(color: red) (color);
1757    //     @import "b.css" supports(foo: bar) (orientation: horizontal);
1758    //     .a { color: red }
1759    //   "#,
1760    //   "/b.css": r#"
1761    //     .b { color: green }
1762    //   "#
1763    // }, "/a.css");
1764
1765    // let res = bundle(fs! {
1766    //   "/a.css": r#"
1767    //     @import "b.css" not print;
1768    //     .a { color: red }
1769    //   "#,
1770    //   "/b.css": r#"
1771    //     @import "c.css" not screen;
1772    //     .b { color: green }
1773    //   "#,
1774    //   "/c.css": r#"
1775    //     .c { color: yellow }
1776    //   "#
1777    // }, "/a.css");
1778  }
1779
1780  #[test]
1781  fn test_css_module() {
1782    macro_rules! map {
1783      { $($key:expr => $val:expr),* } => {
1784        HashMap::from([
1785          $(($key.to_owned(), $val.to_owned()),)*
1786        ])
1787      };
1788    }
1789
1790    let (code, exports) = bundle_css_module(
1791      TestProvider {
1792        map: fs! {
1793          "/a.css": r#"
1794          @import "b.css";
1795          .a { color: red }
1796        "#,
1797          "/b.css": r#"
1798          .a { color: green }
1799        "#
1800        },
1801      },
1802      "/a.css",
1803      None,
1804    );
1805    assert_eq!(
1806      code,
1807      indoc! { r#"
1808      ._9z6RGq_a {
1809        color: green;
1810      }
1811
1812      ._6lixEq_a {
1813        color: red;
1814      }
1815    "#}
1816    );
1817    assert_eq!(
1818      flatten_exports(exports),
1819      map! {
1820        "a" => "_6lixEq_a"
1821      }
1822    );
1823
1824    let (code, exports) = bundle_css_module(
1825      TestProvider {
1826        map: fs! {
1827          "/a.css": r#"
1828          .a { composes: x from './b.css'; color: red; }
1829          .b { color: yellow }
1830        "#,
1831          "/b.css": r#"
1832          .x { composes: y; background: green }
1833          .y { font: Helvetica }
1834        "#
1835        },
1836      },
1837      "/a.css",
1838      None,
1839    );
1840    assert_eq!(
1841      code,
1842      indoc! { r#"
1843      ._8Cs9ZG_x {
1844        background: green;
1845      }
1846
1847      ._8Cs9ZG_y {
1848        font: Helvetica;
1849      }
1850
1851      ._6lixEq_a {
1852        color: red;
1853      }
1854
1855      ._6lixEq_b {
1856        color: #ff0;
1857      }
1858    "#}
1859    );
1860    assert_eq!(
1861      flatten_exports(exports),
1862      map! {
1863        "a" => "_6lixEq_a _8Cs9ZG_x _8Cs9ZG_y",
1864        "b" => "_6lixEq_b"
1865      }
1866    );
1867
1868    let (code, exports) = bundle_css_module(
1869      TestProvider {
1870        map: fs! {
1871          "/a.css": r#"
1872          .a { composes: x from './b.css'; background: red; }
1873        "#,
1874          "/b.css": r#"
1875          .a { background: red }
1876        "#
1877        },
1878      },
1879      "/a.css",
1880      None,
1881    );
1882    assert_eq!(
1883      code,
1884      indoc! { r#"
1885      ._8Cs9ZG_a {
1886        background: red;
1887      }
1888
1889      ._6lixEq_a {
1890        background: red;
1891      }
1892    "#}
1893    );
1894    assert_eq!(
1895      flatten_exports(exports),
1896      map! {
1897        "a" => "_6lixEq_a"
1898      }
1899    );
1900
1901    let (code, exports) = bundle_css_module(
1902      TestProvider {
1903        map: fs! {
1904          "/a.css": r#"
1905          .a {
1906            background: var(--bg from "./b.css", var(--fallback from "./b.css"));
1907            color: rgb(255 255 255 / var(--opacity from "./b.css"));
1908            width: env(--env, var(--env-fallback from "./env.css"));
1909          }
1910        "#,
1911          "/b.css": r#"
1912          .b {
1913            --bg: red;
1914            --fallback: yellow;
1915            --opacity: 0.5;
1916          }
1917        "#,
1918          "/env.css": r#"
1919          .env {
1920            --env-fallback: 20px;
1921          }
1922        "#
1923        },
1924      },
1925      "/a.css",
1926      None,
1927    );
1928    assert_eq!(
1929      code,
1930      indoc! { r#"
1931      ._8Cs9ZG_b {
1932        --_8Cs9ZG_bg: red;
1933        --_8Cs9ZG_fallback: yellow;
1934        --_8Cs9ZG_opacity: .5;
1935      }
1936
1937      .GbJUva_env {
1938        --GbJUva_env-fallback: 20px;
1939      }
1940
1941      ._6lixEq_a {
1942        background: var(--_8Cs9ZG_bg, var(--_8Cs9ZG_fallback));
1943        color: rgb(255 255 255 / var(--_8Cs9ZG_opacity));
1944        width: env(--_6lixEq_env, var(--GbJUva_env-fallback));
1945      }
1946    "#}
1947    );
1948    assert_eq!(
1949      flatten_exports(exports),
1950      map! {
1951        "a" => "_6lixEq_a",
1952        "--env" => "--_6lixEq_env"
1953      }
1954    );
1955
1956    // Hashes are stable between project roots.
1957    let expected = indoc! { r#"
1958    .dyGcAa_b {
1959      background: #ff0;
1960    }
1961
1962    .CK9avG_a {
1963      background: #fff;
1964    }
1965  "#};
1966
1967    let (code, _) = bundle_css_module(
1968      TestProvider {
1969        map: fs! {
1970          "/foo/bar/a.css": r#"
1971        @import "b.css";
1972        .a {
1973          background: white;
1974        }
1975      "#,
1976          "/foo/bar/b.css": r#"
1977        .b {
1978          background: yellow;
1979        }
1980      "#
1981        },
1982      },
1983      "/foo/bar/a.css",
1984      Some("/foo/bar"),
1985    );
1986    assert_eq!(code, expected);
1987
1988    let (code, _) = bundle_css_module(
1989      TestProvider {
1990        map: fs! {
1991          "/x/y/z/a.css": r#"
1992      @import "b.css";
1993      .a {
1994        background: white;
1995      }
1996    "#,
1997          "/x/y/z/b.css": r#"
1998      .b {
1999        background: yellow;
2000      }
2001    "#
2002        },
2003      },
2004      "/x/y/z/a.css",
2005      Some("/x/y/z"),
2006    );
2007    assert_eq!(code, expected);
2008
2009    let (code, _) = bundle_css_module_with_pattern(
2010      TestProvider {
2011        map: fs! {
2012          "/a.css": r#"
2013          @import "b.css";
2014          .a { color: red }
2015        "#,
2016          "/b.css": r#"
2017          .a { color: green }
2018        "#
2019        },
2020      },
2021      "/a.css",
2022      None,
2023      "[content-hash]-[local]",
2024    );
2025    assert_eq!(
2026      code,
2027      indoc! { r#"
2028      .do5n2W-a {
2029        color: green;
2030      }
2031
2032      .pP97eq-a {
2033        color: red;
2034      }
2035    "#}
2036    );
2037  }
2038
2039  #[test]
2040  fn test_source_map() {
2041    let source = r#".imported {
2042      content: "yay, file support!";
2043    }
2044
2045    .selector {
2046      margin: 1em;
2047      background-color: #f60;
2048    }
2049
2050    .selector .nested {
2051      margin: 0.5em;
2052    }
2053
2054    /*# sourceMappingURL=data:application/json;base64,ewoJInZlcnNpb24iOiAzLAoJInNvdXJjZVJvb3QiOiAicm9vdCIsCgkiZmlsZSI6ICJzdGRvdXQiLAoJInNvdXJjZXMiOiBbCgkJInN0ZGluIiwKCQkic2Fzcy9fdmFyaWFibGVzLnNjc3MiLAoJCSJzYXNzL19kZW1vLnNjc3MiCgldLAoJInNvdXJjZXNDb250ZW50IjogWwoJCSJAaW1wb3J0IFwiX3ZhcmlhYmxlc1wiO1xuQGltcG9ydCBcIl9kZW1vXCI7XG5cbi5zZWxlY3RvciB7XG4gIG1hcmdpbjogJHNpemU7XG4gIGJhY2tncm91bmQtY29sb3I6ICRicmFuZENvbG9yO1xuXG4gIC5uZXN0ZWQge1xuICAgIG1hcmdpbjogJHNpemUgLyAyO1xuICB9XG59IiwKCQkiJGJyYW5kQ29sb3I6ICNmNjA7XG4kc2l6ZTogMWVtOyIsCgkJIi5pbXBvcnRlZCB7XG4gIGNvbnRlbnQ6IFwieWF5LCBmaWxlIHN1cHBvcnQhXCI7XG59IgoJXSwKCSJtYXBwaW5ncyI6ICJBRUFBLFNBQVMsQ0FBQztFQUNSLE9BQU8sRUFBRSxvQkFBcUI7Q0FDL0I7O0FGQ0QsU0FBUyxDQUFDO0VBQ1IsTUFBTSxFQ0hELEdBQUc7RURJUixnQkFBZ0IsRUNMTCxJQUFJO0NEVWhCOztBQVBELFNBQVMsQ0FJUCxPQUFPLENBQUM7RUFDTixNQUFNLEVDUEgsS0FBRztDRFFQIiwKCSJuYW1lcyI6IFtdCn0= */"#;
2055
2056    let fs = TestProvider {
2057      map: fs! {
2058        "/a.css": r#"
2059        @import "/b.css";
2060        .a { color: red; }
2061      "#,
2062        "/b.css": source
2063      },
2064    };
2065
2066    let mut sm = parcel_sourcemap::SourceMap::new("/");
2067    let mut bundler = Bundler::new(&fs, Some(&mut sm), ParserOptions::default());
2068    let mut stylesheet = bundler.bundle(Path::new("/a.css")).unwrap();
2069    stylesheet.minify(MinifyOptions::default()).unwrap();
2070    stylesheet
2071      .to_css(PrinterOptions {
2072        source_map: Some(&mut sm),
2073        minify: true,
2074        ..PrinterOptions::default()
2075      })
2076      .unwrap();
2077    let map = sm.to_json(None).unwrap();
2078    assert_eq!(
2079      map,
2080      r#"{"version":3,"sourceRoot":null,"mappings":"ACAA,uCCGA,2CAAA,8BFDQ","sources":["a.css","sass/_demo.scss","stdin"],"sourcesContent":["\n        @import \"/b.css\";\n        .a { color: red; }\n      ",".imported {\n  content: \"yay, file support!\";\n}","@import \"_variables\";\n@import \"_demo\";\n\n.selector {\n  margin: $size;\n  background-color: $brandColor;\n\n  .nested {\n    margin: $size / 2;\n  }\n}"],"names":[]}"#
2081    );
2082  }
2083
2084  #[test]
2085  fn test_license_comments() {
2086    let res = bundle(
2087      TestProvider {
2088        map: fs! {
2089          "/a.css": r#"
2090          /*! Copyright 2023 Someone awesome */
2091          @import "b.css";
2092          .a { color: red }
2093        "#,
2094          "/b.css": r#"
2095          /*! Copyright 2023 Someone else */
2096          .b { color: green }
2097        "#
2098        },
2099      },
2100      "/a.css",
2101    );
2102    assert_eq!(
2103      res,
2104      indoc! { r#"
2105      /*! Copyright 2023 Someone awesome */
2106      /*! Copyright 2023 Someone else */
2107      .b {
2108        color: green;
2109      }
2110
2111      .a {
2112        color: red;
2113      }
2114    "#}
2115    );
2116  }
2117}