1use std::path::Path;
7use std::sync::LazyLock;
8
9use oxc_span::Span;
10
11use crate::{ExportInfo, ExportName, ImportInfo, ImportedName, ModuleInfo, VisibilityTag};
12use fallow_types::discover::FileId;
13
14static CSS_IMPORT_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
17 regex::Regex::new(r#"@import\s+(?:url\(\s*(?:["']([^"']+)["']|([^)]+))\s*\)|["']([^"']+)["'])"#)
18 .expect("valid regex")
19});
20
21static SCSS_USE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
24 regex::Regex::new(r#"@(?:use|forward)\s+["']([^"']+)["']"#).expect("valid regex")
25});
26
27static CSS_PLUGIN_RE: LazyLock<regex::Regex> =
30 LazyLock::new(|| regex::Regex::new(r#"@plugin\s+["']([^"']+)["']"#).expect("valid regex"));
31
32static CSS_APPLY_RE: LazyLock<regex::Regex> =
35 LazyLock::new(|| regex::Regex::new(r"@apply\s+[^;}\n]+").expect("valid regex"));
36
37static CSS_TAILWIND_RE: LazyLock<regex::Regex> =
40 LazyLock::new(|| regex::Regex::new(r"@tailwind\s+\w+").expect("valid regex"));
41
42static CSS_COMMENT_RE: LazyLock<regex::Regex> =
44 LazyLock::new(|| regex::Regex::new(r"(?s)/\*.*?\*/").expect("valid regex"));
45
46static SCSS_LINE_COMMENT_RE: LazyLock<regex::Regex> =
48 LazyLock::new(|| regex::Regex::new(r"//[^\n]*").expect("valid regex"));
49
50static CSS_CLASS_RE: LazyLock<regex::Regex> =
53 LazyLock::new(|| regex::Regex::new(r"\.([a-zA-Z_][a-zA-Z0-9_-]*)").expect("valid regex"));
54
55static CSS_NON_SELECTOR_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
58 regex::Regex::new(r#"(?s)"[^"]*"|'[^']*'|url\([^)]*\)"#).expect("valid regex")
59});
60
61pub(crate) fn is_css_file(path: &Path) -> bool {
62 path.extension()
63 .and_then(|e| e.to_str())
64 .is_some_and(|ext| ext == "css" || ext == "scss")
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct CssImportSource {
70 pub raw: String,
72 pub normalized: String,
74 pub is_plugin: bool,
76}
77
78fn is_css_module_file(path: &Path) -> bool {
79 is_css_file(path)
80 && path
81 .file_stem()
82 .and_then(|s| s.to_str())
83 .is_some_and(|stem| stem.ends_with(".module"))
84}
85
86fn is_css_url_import(source: &str) -> bool {
88 source.starts_with("http://") || source.starts_with("https://") || source.starts_with("data:")
89}
90
91fn normalize_css_import_path(path: String, is_scss: bool) -> String {
103 if path.starts_with('.') || path.starts_with('/') || path.contains("://") {
104 return path;
105 }
106 if path.starts_with('@') && path.contains('/') {
109 return path;
110 }
111 let ext = std::path::Path::new(&path)
113 .extension()
114 .and_then(|e| e.to_str());
115 match ext {
116 Some(e)
117 if e.eq_ignore_ascii_case("css")
118 || e.eq_ignore_ascii_case("scss")
119 || e.eq_ignore_ascii_case("sass")
120 || e.eq_ignore_ascii_case("less") =>
121 {
122 format!("./{path}")
123 }
124 _ => {
125 if is_scss && !path.contains(':') {
129 format!("./{path}")
130 } else {
131 path
132 }
133 }
134 }
135}
136
137fn strip_css_comments(source: &str, is_scss: bool) -> String {
139 let stripped = CSS_COMMENT_RE.replace_all(source, "");
140 if is_scss {
141 SCSS_LINE_COMMENT_RE.replace_all(&stripped, "").into_owned()
142 } else {
143 stripped.into_owned()
144 }
145}
146
147fn normalize_css_plugin_path(path: String) -> String {
153 path
154}
155
156#[must_use]
162pub fn extract_css_import_sources(source: &str, is_scss: bool) -> Vec<CssImportSource> {
163 let stripped = strip_css_comments(source, is_scss);
164 let mut out = Vec::new();
165
166 for cap in CSS_IMPORT_RE.captures_iter(&stripped) {
167 let raw = cap
168 .get(1)
169 .or_else(|| cap.get(2))
170 .or_else(|| cap.get(3))
171 .map(|m| m.as_str().trim().to_string());
172 if let Some(src) = raw
173 && !src.is_empty()
174 && !is_css_url_import(&src)
175 {
176 out.push(CssImportSource {
177 normalized: normalize_css_import_path(src.clone(), is_scss),
178 raw: src,
179 is_plugin: false,
180 });
181 }
182 }
183
184 if is_scss {
185 for cap in SCSS_USE_RE.captures_iter(&stripped) {
186 if let Some(m) = cap.get(1) {
187 let raw = m.as_str().to_string();
188 out.push(CssImportSource {
189 normalized: normalize_css_import_path(raw.clone(), true),
190 raw,
191 is_plugin: false,
192 });
193 }
194 }
195 }
196
197 for cap in CSS_PLUGIN_RE.captures_iter(&stripped) {
198 if let Some(m) = cap.get(1) {
199 let raw = m.as_str().trim().to_string();
200 if !raw.is_empty() && !is_css_url_import(&raw) {
201 out.push(CssImportSource {
202 normalized: normalize_css_plugin_path(raw.clone()),
203 raw,
204 is_plugin: true,
205 });
206 }
207 }
208 }
209
210 out
211}
212
213#[must_use]
220pub fn extract_css_imports(source: &str, is_scss: bool) -> Vec<String> {
221 extract_css_import_sources(source, is_scss)
222 .into_iter()
223 .map(|source| source.normalized)
224 .collect()
225}
226
227pub fn extract_css_module_exports(source: &str) -> Vec<ExportInfo> {
229 let cleaned = CSS_NON_SELECTOR_RE.replace_all(source, "");
230 let mut seen = rustc_hash::FxHashSet::default();
231 let mut exports = Vec::new();
232 for cap in CSS_CLASS_RE.captures_iter(&cleaned) {
233 if let Some(m) = cap.get(1) {
234 let class_name = m.as_str().to_string();
235 if seen.insert(class_name.clone()) {
236 exports.push(ExportInfo {
237 name: ExportName::Named(class_name),
238 local_name: None,
239 is_type_only: false,
240 visibility: VisibilityTag::None,
241 span: Span::default(),
242 members: Vec::new(),
243 super_class: None,
244 });
245 }
246 }
247 }
248 exports
249}
250
251pub(crate) fn parse_css_to_module(
253 file_id: FileId,
254 path: &Path,
255 source: &str,
256 content_hash: u64,
257) -> ModuleInfo {
258 let suppressions = crate::suppress::parse_suppressions_from_source(source);
259 let is_scss = path
260 .extension()
261 .and_then(|e| e.to_str())
262 .is_some_and(|ext| ext == "scss");
263
264 let stripped = strip_css_comments(source, is_scss);
266
267 let mut imports = Vec::new();
268
269 for cap in CSS_IMPORT_RE.captures_iter(&stripped) {
271 let source_path = cap
272 .get(1)
273 .or_else(|| cap.get(2))
274 .or_else(|| cap.get(3))
275 .map(|m| m.as_str().trim().to_string());
276 if let Some(src) = source_path
277 && !src.is_empty()
278 && !is_css_url_import(&src)
279 {
280 let src = normalize_css_import_path(src, is_scss);
283 imports.push(ImportInfo {
284 source: src,
285 imported_name: ImportedName::SideEffect,
286 local_name: String::new(),
287 is_type_only: false,
288 from_style: false,
289 span: Span::default(),
290 source_span: Span::default(),
291 });
292 }
293 }
294
295 if is_scss {
297 for cap in SCSS_USE_RE.captures_iter(&stripped) {
298 if let Some(m) = cap.get(1) {
299 imports.push(ImportInfo {
300 source: normalize_css_import_path(m.as_str().to_string(), true),
301 imported_name: ImportedName::SideEffect,
302 local_name: String::new(),
303 is_type_only: false,
304 from_style: false,
305 span: Span::default(),
306 source_span: Span::default(),
307 });
308 }
309 }
310 }
311
312 for cap in CSS_PLUGIN_RE.captures_iter(&stripped) {
315 if let Some(m) = cap.get(1) {
316 let source = m.as_str().trim().to_string();
317 if !source.is_empty() && !is_css_url_import(&source) {
318 imports.push(ImportInfo {
319 source: normalize_css_plugin_path(source),
320 imported_name: ImportedName::Default,
321 local_name: String::new(),
322 is_type_only: false,
323 from_style: false,
324 span: Span::default(),
325 source_span: Span::default(),
326 });
327 }
328 }
329 }
330
331 let has_apply = CSS_APPLY_RE.is_match(&stripped);
334 let has_tailwind = CSS_TAILWIND_RE.is_match(&stripped);
335 if has_apply || has_tailwind {
336 imports.push(ImportInfo {
337 source: "tailwindcss".to_string(),
338 imported_name: ImportedName::SideEffect,
339 local_name: String::new(),
340 is_type_only: false,
341 from_style: false,
342 span: Span::default(),
343 source_span: Span::default(),
344 });
345 }
346
347 let exports = if is_css_module_file(path) {
349 extract_css_module_exports(&stripped)
350 } else {
351 Vec::new()
352 };
353
354 ModuleInfo {
355 file_id,
356 exports,
357 imports,
358 re_exports: Vec::new(),
359 dynamic_imports: Vec::new(),
360 dynamic_import_patterns: Vec::new(),
361 require_calls: Vec::new(),
362 member_accesses: Vec::new(),
363 whole_object_uses: Vec::new(),
364 has_cjs_exports: false,
365 content_hash,
366 suppressions,
367 unused_import_bindings: Vec::new(),
368 type_referenced_import_bindings: Vec::new(),
369 value_referenced_import_bindings: Vec::new(),
370 line_offsets: fallow_types::extract::compute_line_offsets(source),
371 complexity: Vec::new(),
372 flag_uses: Vec::new(),
373 class_heritage: vec![],
374 local_type_declarations: Vec::new(),
375 public_signature_type_references: Vec::new(),
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 fn export_names(source: &str) -> Vec<String> {
385 extract_css_module_exports(source)
386 .into_iter()
387 .filter_map(|e| match e.name {
388 ExportName::Named(n) => Some(n),
389 ExportName::Default => None,
390 })
391 .collect()
392 }
393
394 #[test]
397 fn is_css_file_css() {
398 assert!(is_css_file(Path::new("styles.css")));
399 }
400
401 #[test]
402 fn is_css_file_scss() {
403 assert!(is_css_file(Path::new("styles.scss")));
404 }
405
406 #[test]
407 fn is_css_file_rejects_js() {
408 assert!(!is_css_file(Path::new("app.js")));
409 }
410
411 #[test]
412 fn is_css_file_rejects_ts() {
413 assert!(!is_css_file(Path::new("app.ts")));
414 }
415
416 #[test]
417 fn is_css_file_rejects_less() {
418 assert!(!is_css_file(Path::new("styles.less")));
419 }
420
421 #[test]
422 fn is_css_file_rejects_no_extension() {
423 assert!(!is_css_file(Path::new("Makefile")));
424 }
425
426 #[test]
429 fn is_css_module_file_module_css() {
430 assert!(is_css_module_file(Path::new("Component.module.css")));
431 }
432
433 #[test]
434 fn is_css_module_file_module_scss() {
435 assert!(is_css_module_file(Path::new("Component.module.scss")));
436 }
437
438 #[test]
439 fn is_css_module_file_rejects_plain_css() {
440 assert!(!is_css_module_file(Path::new("styles.css")));
441 }
442
443 #[test]
444 fn is_css_module_file_rejects_plain_scss() {
445 assert!(!is_css_module_file(Path::new("styles.scss")));
446 }
447
448 #[test]
449 fn is_css_module_file_rejects_module_js() {
450 assert!(!is_css_module_file(Path::new("utils.module.js")));
451 }
452
453 #[test]
456 fn extracts_single_class() {
457 let names = export_names(".foo { color: red; }");
458 assert_eq!(names, vec!["foo"]);
459 }
460
461 #[test]
462 fn extracts_multiple_classes() {
463 let names = export_names(".foo { } .bar { }");
464 assert_eq!(names, vec!["foo", "bar"]);
465 }
466
467 #[test]
468 fn extracts_nested_classes() {
469 let names = export_names(".foo .bar { color: red; }");
470 assert!(names.contains(&"foo".to_string()));
471 assert!(names.contains(&"bar".to_string()));
472 }
473
474 #[test]
475 fn extracts_hyphenated_class() {
476 let names = export_names(".my-class { }");
477 assert_eq!(names, vec!["my-class"]);
478 }
479
480 #[test]
481 fn extracts_camel_case_class() {
482 let names = export_names(".myClass { }");
483 assert_eq!(names, vec!["myClass"]);
484 }
485
486 #[test]
487 fn extracts_underscore_class() {
488 let names = export_names("._hidden { } .__wrapper { }");
489 assert!(names.contains(&"_hidden".to_string()));
490 assert!(names.contains(&"__wrapper".to_string()));
491 }
492
493 #[test]
496 fn pseudo_selector_hover() {
497 let names = export_names(".foo:hover { color: blue; }");
498 assert_eq!(names, vec!["foo"]);
499 }
500
501 #[test]
502 fn pseudo_selector_focus() {
503 let names = export_names(".input:focus { outline: none; }");
504 assert_eq!(names, vec!["input"]);
505 }
506
507 #[test]
508 fn pseudo_element_before() {
509 let names = export_names(".icon::before { content: ''; }");
510 assert_eq!(names, vec!["icon"]);
511 }
512
513 #[test]
514 fn combined_pseudo_selectors() {
515 let names = export_names(".btn:hover, .btn:active, .btn:focus { }");
516 assert_eq!(names, vec!["btn"]);
518 }
519
520 #[test]
523 fn classes_inside_media_query() {
524 let names = export_names(
525 "@media (max-width: 768px) { .mobile-nav { display: block; } .desktop-nav { display: none; } }",
526 );
527 assert!(names.contains(&"mobile-nav".to_string()));
528 assert!(names.contains(&"desktop-nav".to_string()));
529 }
530
531 #[test]
534 fn deduplicates_repeated_class() {
535 let names = export_names(".btn { color: red; } .btn { font-size: 14px; }");
536 assert_eq!(names.iter().filter(|n| *n == "btn").count(), 1);
537 }
538
539 #[test]
542 fn empty_source() {
543 let names = export_names("");
544 assert!(names.is_empty());
545 }
546
547 #[test]
548 fn no_classes() {
549 let names = export_names("body { margin: 0; } * { box-sizing: border-box; }");
550 assert!(names.is_empty());
551 }
552
553 #[test]
554 fn ignores_classes_in_block_comments() {
555 let stripped = strip_css_comments("/* .fake { } */ .real { }", false);
560 let names = export_names(&stripped);
561 assert!(!names.contains(&"fake".to_string()));
562 assert!(names.contains(&"real".to_string()));
563 }
564
565 #[test]
566 fn ignores_classes_in_strings() {
567 let names = export_names(r#".real { content: ".fake"; }"#);
568 assert!(names.contains(&"real".to_string()));
569 assert!(!names.contains(&"fake".to_string()));
570 }
571
572 #[test]
573 fn ignores_classes_in_url() {
574 let names = export_names(".real { background: url(./images/hero.png); }");
575 assert!(names.contains(&"real".to_string()));
576 assert!(!names.contains(&"png".to_string()));
578 }
579
580 #[test]
583 fn strip_css_block_comment() {
584 let result = strip_css_comments("/* removed */ .kept { }", false);
585 assert!(!result.contains("removed"));
586 assert!(result.contains(".kept"));
587 }
588
589 #[test]
590 fn strip_scss_line_comment() {
591 let result = strip_css_comments("// removed\n.kept { }", true);
592 assert!(!result.contains("removed"));
593 assert!(result.contains(".kept"));
594 }
595
596 #[test]
597 fn strip_scss_preserves_css_outside_comments() {
598 let source = "// line comment\n/* block comment */\n.visible { color: red; }";
599 let result = strip_css_comments(source, true);
600 assert!(result.contains(".visible"));
601 }
602
603 #[test]
606 fn url_import_http() {
607 assert!(is_css_url_import("http://example.com/style.css"));
608 }
609
610 #[test]
611 fn url_import_https() {
612 assert!(is_css_url_import("https://fonts.googleapis.com/css"));
613 }
614
615 #[test]
616 fn url_import_data() {
617 assert!(is_css_url_import("data:text/css;base64,abc"));
618 }
619
620 #[test]
621 fn url_import_local_not_skipped() {
622 assert!(!is_css_url_import("./local.css"));
623 }
624
625 #[test]
626 fn url_import_bare_specifier_not_skipped() {
627 assert!(!is_css_url_import("tailwindcss"));
628 }
629
630 #[test]
633 fn normalize_relative_dot_path_unchanged() {
634 assert_eq!(
635 normalize_css_import_path("./reset.css".to_string(), false),
636 "./reset.css"
637 );
638 }
639
640 #[test]
641 fn normalize_parent_relative_path_unchanged() {
642 assert_eq!(
643 normalize_css_import_path("../shared.scss".to_string(), false),
644 "../shared.scss"
645 );
646 }
647
648 #[test]
649 fn normalize_absolute_path_unchanged() {
650 assert_eq!(
651 normalize_css_import_path("/styles/main.css".to_string(), false),
652 "/styles/main.css"
653 );
654 }
655
656 #[test]
657 fn normalize_url_unchanged() {
658 assert_eq!(
659 normalize_css_import_path("https://example.com/style.css".to_string(), false),
660 "https://example.com/style.css"
661 );
662 }
663
664 #[test]
665 fn normalize_bare_css_gets_dot_slash() {
666 assert_eq!(
667 normalize_css_import_path("app.css".to_string(), false),
668 "./app.css"
669 );
670 }
671
672 #[test]
673 fn normalize_bare_scss_gets_dot_slash() {
674 assert_eq!(
675 normalize_css_import_path("vars.scss".to_string(), false),
676 "./vars.scss"
677 );
678 }
679
680 #[test]
681 fn normalize_bare_sass_gets_dot_slash() {
682 assert_eq!(
683 normalize_css_import_path("main.sass".to_string(), false),
684 "./main.sass"
685 );
686 }
687
688 #[test]
689 fn normalize_bare_less_gets_dot_slash() {
690 assert_eq!(
691 normalize_css_import_path("theme.less".to_string(), false),
692 "./theme.less"
693 );
694 }
695
696 #[test]
697 fn normalize_bare_js_extension_stays_bare() {
698 assert_eq!(
699 normalize_css_import_path("module.js".to_string(), false),
700 "module.js"
701 );
702 }
703
704 #[test]
707 fn normalize_scss_bare_partial_gets_dot_slash() {
708 assert_eq!(
709 normalize_css_import_path("variables".to_string(), true),
710 "./variables"
711 );
712 }
713
714 #[test]
715 fn normalize_scss_bare_partial_with_subdir_gets_dot_slash() {
716 assert_eq!(
717 normalize_css_import_path("base/reset".to_string(), true),
718 "./base/reset"
719 );
720 }
721
722 #[test]
723 fn normalize_scss_builtin_stays_bare() {
724 assert_eq!(
725 normalize_css_import_path("sass:math".to_string(), true),
726 "sass:math"
727 );
728 }
729
730 #[test]
731 fn normalize_scss_relative_path_unchanged() {
732 assert_eq!(
733 normalize_css_import_path("../styles/variables".to_string(), true),
734 "../styles/variables"
735 );
736 }
737
738 #[test]
739 fn normalize_css_bare_extensionless_stays_bare() {
740 assert_eq!(
742 normalize_css_import_path("tailwindcss".to_string(), false),
743 "tailwindcss"
744 );
745 }
746
747 #[test]
750 fn normalize_scoped_package_with_css_extension_stays_bare() {
751 assert_eq!(
752 normalize_css_import_path("@fontsource/monaspace-neon/400.css".to_string(), false),
753 "@fontsource/monaspace-neon/400.css"
754 );
755 }
756
757 #[test]
758 fn normalize_scoped_package_with_scss_extension_stays_bare() {
759 assert_eq!(
760 normalize_css_import_path("@company/design-system/tokens.scss".to_string(), true),
761 "@company/design-system/tokens.scss"
762 );
763 }
764
765 #[test]
766 fn normalize_scoped_package_without_extension_stays_bare() {
767 assert_eq!(
768 normalize_css_import_path("@fallow/design-system/styles".to_string(), false),
769 "@fallow/design-system/styles"
770 );
771 }
772
773 #[test]
774 fn normalize_scoped_package_extensionless_scss_stays_bare() {
775 assert_eq!(
776 normalize_css_import_path("@company/tokens".to_string(), true),
777 "@company/tokens"
778 );
779 }
780
781 #[test]
782 fn normalize_path_alias_with_css_extension_stays_bare() {
783 assert_eq!(
788 normalize_css_import_path("@/components/Button.css".to_string(), false),
789 "@/components/Button.css"
790 );
791 }
792
793 #[test]
794 fn normalize_path_alias_extensionless_stays_bare() {
795 assert_eq!(
796 normalize_css_import_path("@/styles/variables".to_string(), false),
797 "@/styles/variables"
798 );
799 }
800
801 #[test]
804 fn strip_css_no_comments() {
805 let source = ".foo { color: red; }";
806 assert_eq!(strip_css_comments(source, false), source);
807 }
808
809 #[test]
810 fn strip_css_multiple_block_comments() {
811 let source = "/* comment-one */ .foo { } /* comment-two */ .bar { }";
812 let result = strip_css_comments(source, false);
813 assert!(!result.contains("comment-one"));
814 assert!(!result.contains("comment-two"));
815 assert!(result.contains(".foo"));
816 assert!(result.contains(".bar"));
817 }
818
819 #[test]
820 fn strip_scss_does_not_affect_non_scss() {
821 let source = "// this stays\n.foo { }";
823 let result = strip_css_comments(source, false);
824 assert!(result.contains("// this stays"));
825 }
826
827 #[test]
830 fn css_module_parses_suppressions() {
831 let info = parse_css_to_module(
832 fallow_types::discover::FileId(0),
833 Path::new("Component.module.css"),
834 "/* fallow-ignore-file */\n.btn { color: red; }",
835 0,
836 );
837 assert!(!info.suppressions.is_empty());
838 assert_eq!(info.suppressions[0].line, 0);
839 }
840
841 #[test]
844 fn extracts_class_starting_with_underscore() {
845 let names = export_names("._private { } .__dunder { }");
846 assert!(names.contains(&"_private".to_string()));
847 assert!(names.contains(&"__dunder".to_string()));
848 }
849
850 #[test]
851 fn ignores_id_selectors() {
852 let names = export_names("#myId { color: red; }");
853 assert!(!names.contains(&"myId".to_string()));
854 }
855
856 #[test]
857 fn ignores_element_selectors() {
858 let names = export_names("div { color: red; } span { }");
859 assert!(names.is_empty());
860 }
861
862 #[test]
865 fn extract_css_imports_at_import_quoted() {
866 let imports = extract_css_imports(r#"@import "./reset.css";"#, false);
867 assert_eq!(imports, vec!["./reset.css"]);
868 }
869
870 #[test]
871 fn extract_css_imports_at_import_url() {
872 let imports = extract_css_imports(r#"@import url("./reset.css");"#, false);
873 assert_eq!(imports, vec!["./reset.css"]);
874 }
875
876 #[test]
877 fn extract_css_imports_skips_remote_urls() {
878 let imports =
879 extract_css_imports(r#"@import "https://fonts.example.com/font.css";"#, false);
880 assert!(imports.is_empty());
881 }
882
883 #[test]
884 fn extract_css_imports_scss_use_normalizes_partial() {
885 let imports = extract_css_imports(r#"@use "variables";"#, true);
886 assert_eq!(imports, vec!["./variables"]);
887 }
888
889 #[test]
890 fn extract_css_imports_scss_forward_normalizes_partial() {
891 let imports = extract_css_imports(r#"@forward "tokens";"#, true);
892 assert_eq!(imports, vec!["./tokens"]);
893 }
894
895 #[test]
896 fn extract_css_imports_skips_comments() {
897 let imports = extract_css_imports(
898 r#"/* @import "./hidden.scss"; */
899@use "real";"#,
900 true,
901 );
902 assert_eq!(imports, vec!["./real"]);
903 }
904
905 #[test]
906 fn extract_css_imports_at_plugin_keeps_package_bare() {
907 let imports = extract_css_imports(r#"@plugin "daisyui";"#, true);
908 assert_eq!(imports, vec!["daisyui"]);
909 }
910
911 #[test]
912 fn extract_css_imports_at_plugin_tracks_relative_file() {
913 let imports = extract_css_imports(r#"@plugin "./tailwind-plugin.js";"#, false);
914 assert_eq!(imports, vec!["./tailwind-plugin.js"]);
915 }
916
917 #[test]
918 fn extract_css_imports_scss_at_import_kept_relative() {
919 let imports = extract_css_imports(r"@import 'Foo';", true);
920 assert_eq!(imports, vec!["./Foo"]);
922 }
923
924 #[test]
925 fn extract_css_imports_additional_data_string_body() {
926 let body = r#"@use "./src/styles/global.scss";"#;
928 let imports = extract_css_imports(body, true);
929 assert_eq!(imports, vec!["./src/styles/global.scss"]);
930 }
931}