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