1use 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
68pub 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#[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 External(String),
120 #[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
131pub trait SourceProvider: Send + Sync {
136 type Error: std::error::Error + Send + Sync;
138
139 fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error>;
141
142 fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<ResolveResult, Self::Error>;
145}
146
147pub struct FileProvider {
150 inputs: Mutex<Vec<*mut String>>,
151}
152
153impl FileProvider {
154 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 Ok(unsafe { &*ptr })
176 }
177
178 fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<ResolveResult, Self::Error> {
179 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#[derive(Debug)]
194#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
195pub enum BundleErrorKind<'i, T: std::error::Error> {
196 ParserError(ParserError<'i>),
198 UnsupportedImportCondition,
200 UnsupportedLayerCombination,
202 UnsupportedMediaBooleanLogic,
204 ReferencedExternalModuleWithCssModuleFrom,
206 ExternalImportAfterBundledImport,
209 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 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 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 pub fn bundle<'e>(
295 &mut self,
296 entry: &'e Path,
297 ) -> Result<StyleSheet<'a, T::AtRule>, Error<BundleErrorKind<'a, P::Error>>> {
298 let mut errors = Mutex::new(Vec::new());
301 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 self.order(errors.get_mut().unwrap())?;
320
321 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 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 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); 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 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 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 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 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 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 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 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 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 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 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 let layer = std::mem::replace(rule, CssRule::Ignored);
754 dest.push(layer);
755 }
756 CssRule::Ignored => {}
757 _ => break,
758 }
759 }
760
761 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 struct CustomProvider {
886 map: HashMap<PathBuf, String>,
887 }
888
889 impl SourceProvider for CustomProvider {
890 type Error = std::io::Error;
891
892 fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
894 Ok(self.map.get(file).unwrap())
895 }
896
897 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 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 }
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 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}