Skip to main content

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