1use std::path::Path;
48
49use oxc_allocator::Allocator;
50use oxc_ast::ast::{
51 Argument, BindingPattern, ComputedMemberExpression, Expression, ImportDeclarationSpecifier,
52 NumericLiteral, ObjectExpression, ObjectPropertyKind, Program, Statement,
53 StaticMemberExpression, UnaryOperator, VariableDeclarator,
54};
55use oxc_ast_visit::{Visit, walk};
56use oxc_parser::Parser;
57use oxc_span::{GetSpan, SourceType};
58use rustc_hash::{FxHashMap, FxHashSet};
59
60use super::object::{Lib, module_library};
61
62const PANDA_CONFIG_BINDING: &str = "pandaConfig";
63
64#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct CssInJsToken {
69 pub path: String,
71 pub def_line: u32,
73 pub value: Option<String>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct CssInJsTokenDef {
82 pub binding: String,
85 pub origin: CssInJsTokenOrigin,
87 pub tokens: Vec<CssInJsToken>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum CssInJsTokenOrigin {
94 StyleX,
96 VanillaExtract,
98 Panda,
100 Theme,
102}
103
104#[must_use]
108pub fn css_in_js_token_defs(source: &str, path: &Path) -> Vec<CssInJsTokenDef> {
109 let source_type = SourceType::from_path(path).unwrap_or_default();
110 let allocator = Allocator::default();
111 let ret = Parser::new(&allocator, source, source_type).parse();
112
113 let mut collector = TokenDefCollector::new(source);
114 collector.build_import_map(&ret.program);
115 if collector.imports.is_empty() {
116 return Vec::new();
117 }
118 collector.visit_program(&ret.program);
119 collector.defs
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct TokenConsumerHit {
127 pub token_path: String,
130 pub line: u32,
132}
133
134#[must_use]
142#[expect(
143 clippy::implicit_hasher,
144 reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
145)]
146pub fn css_in_js_token_consumers(
147 source: &str,
148 path: &Path,
149 alias: &str,
150 leaf_paths: &FxHashSet<String>,
151) -> Vec<TokenConsumerHit> {
152 css_in_js_consumer_scan(
153 source,
154 path,
155 &[ConsumerQuery::MemberBinding { alias, leaf_paths }],
156 )
157 .into_iter()
158 .map(|(_, hit)| hit)
159 .collect()
160}
161
162#[must_use]
166#[expect(
167 clippy::implicit_hasher,
168 reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
169)]
170pub fn panda_token_call_consumers(
171 source: &str,
172 path: &Path,
173 alias: &str,
174 leaf_paths: &FxHashSet<String>,
175) -> Vec<TokenConsumerHit> {
176 css_in_js_consumer_scan(
177 source,
178 path,
179 &[ConsumerQuery::PandaTokenCall { alias, leaf_paths }],
180 )
181 .into_iter()
182 .map(|(_, hit)| hit)
183 .collect()
184}
185
186#[must_use]
189#[expect(
190 clippy::implicit_hasher,
191 reason = "callers build FxHashSet values; std HashSet is a disallowed type here"
192)]
193pub fn panda_style_value_consumers(
194 source: &str,
195 path: &Path,
196 aliases: &FxHashSet<String>,
197 leaf_paths: &FxHashSet<String>,
198) -> Vec<TokenConsumerHit> {
199 css_in_js_consumer_scan(
200 source,
201 path,
202 &[ConsumerQuery::PandaStyleValues {
203 aliases,
204 leaf_paths,
205 }],
206 )
207 .into_iter()
208 .map(|(_, hit)| hit)
209 .collect()
210}
211
212#[must_use]
217pub fn css_in_js_theme_token_defs(source: &str, path: &Path) -> Vec<CssInJsTokenDef> {
218 let source_type = SourceType::from_path(path).unwrap_or_default();
219 let allocator = Allocator::default();
220 let ret = Parser::new(&allocator, source, source_type).parse();
221
222 let mut collector = ThemeDefCollector {
223 lines: LineCounter::new(source),
224 defs: Vec::new(),
225 };
226 collector.visit_program(&ret.program);
227 collector.defs
228}
229
230#[must_use]
233#[expect(
234 clippy::implicit_hasher,
235 reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
236)]
237pub fn css_in_js_theme_consumers(
238 source: &str,
239 path: &Path,
240 leaf_paths: &FxHashSet<String>,
241) -> Vec<TokenConsumerHit> {
242 css_in_js_consumer_scan(source, path, &[ConsumerQuery::ThemeReads { leaf_paths }])
243 .into_iter()
244 .map(|(_, hit)| hit)
245 .collect()
246}
247
248pub enum ConsumerQuery<'a> {
252 MemberBinding {
255 alias: &'a str,
257 leaf_paths: &'a FxHashSet<String>,
259 },
260 PandaTokenCall {
263 alias: &'a str,
265 leaf_paths: &'a FxHashSet<String>,
267 },
268 PandaStyleValues {
271 aliases: &'a FxHashSet<String>,
273 leaf_paths: &'a FxHashSet<String>,
275 },
276 ThemeReads {
279 leaf_paths: &'a FxHashSet<String>,
281 },
282}
283
284#[must_use]
291pub fn css_in_js_consumer_scan(
292 source: &str,
293 path: &Path,
294 queries: &[ConsumerQuery<'_>],
295) -> Vec<(usize, TokenConsumerHit)> {
296 if queries.is_empty() {
297 return Vec::new();
298 }
299 let source_type = SourceType::from_path(path).unwrap_or_default();
300 let allocator = Allocator::default();
301 let ret = Parser::new(&allocator, source, source_type).parse();
302 let mut out = Vec::new();
303 for (idx, query) in queries.iter().enumerate() {
304 run_consumer_query(query, source, &ret.program, idx, &mut out);
305 }
306 out
307}
308
309fn run_consumer_query<'a>(
313 query: &ConsumerQuery<'_>,
314 source: &'a str,
315 program: &Program<'a>,
316 idx: usize,
317 out: &mut Vec<(usize, TokenConsumerHit)>,
318) {
319 match query {
320 ConsumerQuery::MemberBinding { alias, leaf_paths } => {
321 if alias.is_empty() || leaf_paths.is_empty() {
322 return;
323 }
324 let mut collector = ConsumerCollector {
325 lines: LineCounter::new(source),
326 alias,
327 leaf_paths,
328 hits: Vec::new(),
329 };
330 collector.visit_program(program);
331 out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
332 }
333 ConsumerQuery::PandaTokenCall { alias, leaf_paths } => {
334 if alias.is_empty() || leaf_paths.is_empty() {
335 return;
336 }
337 let mut collector = PandaTokenCallCollector {
338 lines: LineCounter::new(source),
339 alias,
340 leaf_paths,
341 hits: Vec::new(),
342 };
343 collector.visit_program(program);
344 out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
345 }
346 ConsumerQuery::PandaStyleValues {
347 aliases,
348 leaf_paths,
349 } => {
350 if aliases.is_empty() || leaf_paths.is_empty() {
351 return;
352 }
353 let mut collector = PandaStyleValueCollector {
354 lines: LineCounter::new(source),
355 aliases,
356 leaf_paths,
357 hits: Vec::new(),
358 };
359 collector.visit_program(program);
360 out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
361 }
362 ConsumerQuery::ThemeReads { leaf_paths } => {
363 if leaf_paths.is_empty() {
364 return;
365 }
366 let mut collector = ThemeConsumerCollector {
367 lines: LineCounter::new(source),
368 leaf_paths,
369 hits: Vec::new(),
370 };
371 collector.visit_program(program);
372 out.extend(collector.hits.into_iter().map(|hit| (idx, hit)));
373 }
374 }
375}
376
377struct ConsumerCollector<'a, 'b> {
379 lines: LineCounter<'a>,
380 alias: &'b str,
381 leaf_paths: &'b FxHashSet<String>,
382 hits: Vec<TokenConsumerHit>,
383}
384
385impl<'a> ConsumerCollector<'a, '_> {
386 fn record(&mut self, chain: Option<(&'a str, Vec<&'a str>)>, span_start: u32) {
391 if let Some((base, segments)) = chain
392 && base == self.alias
393 && !segments.is_empty()
394 {
395 let token_path = segments.join(".");
396 if self.leaf_paths.contains(&token_path) {
397 let line = self.lines.line_at(span_start);
398 self.hits.push(TokenConsumerHit { token_path, line });
399 }
400 }
401 }
402}
403
404impl<'a> Visit<'a> for ConsumerCollector<'a, '_> {
405 fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
406 let mut chain = access_object_chain(&member.object);
407 if let Some((_, segments)) = chain.as_mut() {
408 segments.push(member.property.name.as_str());
409 }
410 self.record(chain, member.span().start);
411 walk::walk_static_member_expression(self, member);
412 }
413
414 fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
415 let mut chain = access_object_chain(&member.object);
421 if let (Some((_, segments)), Some(key)) =
422 (chain.as_mut(), string_literal_key(&member.expression))
423 {
424 segments.push(key);
425 } else {
426 chain = None;
427 }
428 self.record(chain, member.span().start);
429 walk::walk_computed_member_expression(self, member);
430 }
431}
432
433struct PandaTokenCallCollector<'a, 'b> {
434 lines: LineCounter<'a>,
435 alias: &'b str,
436 leaf_paths: &'b FxHashSet<String>,
437 hits: Vec<TokenConsumerHit>,
438}
439
440impl<'a> Visit<'a> for PandaTokenCallCollector<'a, '_> {
441 fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
442 let Expression::Identifier(callee) = &call.callee else {
443 walk::walk_call_expression(self, call);
444 return;
445 };
446 if callee.name.as_str() == self.alias
447 && let Some(Argument::StringLiteral(lit)) = call.arguments.first()
448 {
449 let token_path = lit.value.as_str();
450 if self.leaf_paths.contains(token_path) {
451 let line = self.lines.line_at(call.span().start);
452 self.hits.push(TokenConsumerHit {
453 token_path: token_path.to_owned(),
454 line,
455 });
456 }
457 }
458 walk::walk_call_expression(self, call);
459 }
460}
461
462struct PandaStyleValueCollector<'a, 'b> {
463 lines: LineCounter<'a>,
464 aliases: &'b FxHashSet<String>,
465 leaf_paths: &'b FxHashSet<String>,
466 hits: Vec<TokenConsumerHit>,
467}
468
469impl<'a> PandaStyleValueCollector<'a, '_> {
470 fn record_object(&mut self, obj: &ObjectExpression<'a>) {
471 for prop in &obj.properties {
472 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
473 continue;
474 };
475 self.record_expression(&prop.value);
476 }
477 }
478
479 fn record_expression(&mut self, expr: &Expression<'a>) {
480 match expr {
481 Expression::StringLiteral(lit) => {
482 let token_path = lit.value.as_str();
483 if self.leaf_paths.contains(token_path) {
484 let line = self.lines.line_at(lit.span().start);
485 self.hits.push(TokenConsumerHit {
486 token_path: token_path.to_owned(),
487 line,
488 });
489 }
490 }
491 Expression::ObjectExpression(obj) => self.record_object(obj),
492 _ => {}
493 }
494 }
495}
496
497impl<'a> Visit<'a> for PandaStyleValueCollector<'a, '_> {
498 fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
499 let Expression::Identifier(callee) = &call.callee else {
500 walk::walk_call_expression(self, call);
501 return;
502 };
503 if self.aliases.contains(callee.name.as_str()) {
504 for arg in &call.arguments {
505 if let Argument::ObjectExpression(obj) = arg {
506 self.record_object(obj);
507 }
508 }
509 }
510 walk::walk_call_expression(self, call);
511 }
512}
513
514struct ThemeDefCollector<'a> {
515 lines: LineCounter<'a>,
516 defs: Vec<CssInJsTokenDef>,
517}
518
519impl<'a> ThemeDefCollector<'a> {
520 fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
521 let BindingPattern::BindingIdentifier(binding) = &decl.id else {
522 return;
523 };
524 let binding_name = binding.name.as_str();
525 if !is_theme_binding_name(binding_name) {
526 return;
527 }
528 let Some(Expression::ObjectExpression(obj)) = &decl.init else {
529 return;
530 };
531 let mut tokens = Vec::new();
532 collect_token_leaves(
533 &mut self.lines,
534 obj,
535 "",
536 CssInJsTokenOrigin::Theme,
537 &mut tokens,
538 );
539 if tokens.is_empty() {
540 return;
541 }
542 self.defs.push(CssInJsTokenDef {
543 binding: binding_name.to_owned(),
544 origin: CssInJsTokenOrigin::Theme,
545 tokens,
546 });
547 }
548}
549
550impl<'a> Visit<'a> for ThemeDefCollector<'a> {
551 fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
552 self.process_declarator(decl);
553 walk::walk_variable_declarator(self, decl);
554 }
555}
556
557struct ThemeConsumerCollector<'a, 'b> {
558 lines: LineCounter<'a>,
559 leaf_paths: &'b FxHashSet<String>,
560 hits: Vec<TokenConsumerHit>,
561}
562
563impl<'a> ThemeConsumerCollector<'a, '_> {
564 fn record(&mut self, chain: Option<(&'a str, Vec<&'a str>)>, span_start: u32) {
565 let Some((base, segments)) = chain else {
566 return;
567 };
568 let token_segments: &[&str] = match base {
569 "theme" => &segments,
570 "props" if segments.first().copied() == Some("theme") => &segments[1..],
571 _ => return,
572 };
573 if token_segments.is_empty() {
574 return;
575 }
576 let token_path = token_segments.join(".");
577 if self.leaf_paths.contains(&token_path) {
578 let line = self.lines.line_at(span_start);
579 self.hits.push(TokenConsumerHit { token_path, line });
580 }
581 }
582}
583
584impl<'a> Visit<'a> for ThemeConsumerCollector<'a, '_> {
585 fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
586 let mut chain = access_object_chain(&member.object);
587 if let Some((_, segments)) = chain.as_mut() {
588 segments.push(member.property.name.as_str());
589 }
590 self.record(chain, member.span().start);
591 walk::walk_static_member_expression(self, member);
592 }
593
594 fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
595 let mut chain = access_object_chain(&member.object);
596 if let (Some((_, segments)), Some(key)) =
597 (chain.as_mut(), string_literal_key(&member.expression))
598 {
599 segments.push(key);
600 } else {
601 chain = None;
602 }
603 self.record(chain, member.span().start);
604 walk::walk_computed_member_expression(self, member);
605 }
606}
607
608fn access_object_chain<'a>(expr: &Expression<'a>) -> Option<(&'a str, Vec<&'a str>)> {
614 match expr {
615 Expression::Identifier(id) => Some((id.name.as_str(), Vec::new())),
616 Expression::StaticMemberExpression(inner) => {
617 let (base, mut segments) = access_object_chain(&inner.object)?;
618 segments.push(inner.property.name.as_str());
619 Some((base, segments))
620 }
621 Expression::ComputedMemberExpression(inner) => {
622 let (base, mut segments) = access_object_chain(&inner.object)?;
623 segments.push(string_literal_key(&inner.expression)?);
624 Some((base, segments))
625 }
626 _ => None,
627 }
628}
629
630fn string_literal_key<'a>(expr: &Expression<'a>) -> Option<&'a str> {
633 match expr {
634 Expression::StringLiteral(lit) => Some(lit.value.as_str()),
635 _ => None,
636 }
637}
638
639#[derive(Clone, Copy)]
641enum BindingSource {
642 LhsIdent,
644 TupleElement(usize),
646}
647
648#[derive(Clone, Copy)]
651struct Recognized {
652 binding_source: BindingSource,
653 tokens_arg: usize,
654 origin: CssInJsTokenOrigin,
655}
656
657struct TokenDefCollector<'a> {
659 lines: LineCounter<'a>,
660 imports: FxHashMap<&'a str, (Lib, &'a str)>,
663 defs: Vec<CssInJsTokenDef>,
664}
665
666impl<'a> TokenDefCollector<'a> {
667 fn new(source: &'a str) -> Self {
668 Self {
669 lines: LineCounter::new(source),
670 imports: FxHashMap::default(),
671 defs: Vec::new(),
672 }
673 }
674
675 fn build_import_map(&mut self, program: &Program<'a>) {
680 for stmt in &program.body {
681 let Statement::ImportDeclaration(decl) = stmt else {
682 continue;
683 };
684 if decl.import_kind.is_type() {
685 continue;
686 }
687 let Some(lib) = module_library(decl.source.value.as_str()) else {
688 continue;
689 };
690 let Some(specifiers) = &decl.specifiers else {
691 continue;
692 };
693 for specifier in specifiers {
694 let (local, role) = match specifier {
695 ImportDeclarationSpecifier::ImportSpecifier(s) => {
696 (s.local.name.as_str(), s.imported.name().as_str())
697 }
698 ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
699 (s.local.name.as_str(), s.local.name.as_str())
700 }
701 ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
702 (s.local.name.as_str(), s.local.name.as_str())
703 }
704 };
705 self.imports.insert(local, (lib, role));
706 }
707 }
708 }
709
710 fn callee_role(&self, callee: &Expression<'a>) -> Option<(Lib, &'a str)> {
714 match callee {
715 Expression::Identifier(id) => self.imports.get(id.name.as_str()).copied(),
716 Expression::StaticMemberExpression(member) => {
717 let Expression::Identifier(obj) = &member.object else {
718 return None;
719 };
720 let (lib, _) = *self.imports.get(obj.name.as_str())?;
721 Some((lib, member.property.name.as_str()))
723 }
724 _ => None,
725 }
726 }
727
728 fn recognize(lib: Lib, role: &str, arg_count: usize) -> Option<Recognized> {
732 let single = |tokens_arg, origin| {
733 Some(Recognized {
734 binding_source: BindingSource::LhsIdent,
735 tokens_arg,
736 origin,
737 })
738 };
739 match (lib, role) {
740 (Lib::StyleX, "defineVars") if arg_count >= 1 => single(0, CssInJsTokenOrigin::StyleX),
743 (Lib::VanillaExtract, "createThemeContract") if arg_count >= 1 => {
744 single(0, CssInJsTokenOrigin::VanillaExtract)
745 }
746 (Lib::VanillaExtract, "createTheme") if arg_count == 1 => Some(Recognized {
750 binding_source: BindingSource::TupleElement(1),
751 tokens_arg: 0,
752 origin: CssInJsTokenOrigin::VanillaExtract,
753 }),
754 (Lib::VanillaExtract, "createGlobalTheme") if arg_count == 2 => {
758 single(1, CssInJsTokenOrigin::VanillaExtract)
759 }
760 (Lib::Panda, "defineTokens") if arg_count >= 1 => single(0, CssInJsTokenOrigin::Panda),
761 _ => None,
762 }
763 }
764
765 fn binding_name(decl: &VariableDeclarator<'a>, source: BindingSource) -> Option<&'a str> {
768 match source {
769 BindingSource::LhsIdent => match &decl.id {
770 BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
771 _ => None,
772 },
773 BindingSource::TupleElement(index) => {
774 let BindingPattern::ArrayPattern(arr) = &decl.id else {
775 return None;
776 };
777 let element = arr.elements.get(index)?.as_ref()?;
778 match element {
779 BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
780 _ => None,
781 }
782 }
783 }
784 }
785
786 fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
787 let Some(Expression::CallExpression(call)) = &decl.init else {
788 return;
789 };
790 if self.process_panda_config_call(call) {
791 return;
792 }
793 let Some((lib, role)) = self.callee_role(&call.callee) else {
794 return;
795 };
796 let Some(recognized) = Self::recognize(lib, role, call.arguments.len()) else {
797 return;
798 };
799 let Some(binding) = Self::binding_name(decl, recognized.binding_source) else {
800 return;
801 };
802 let Some(Argument::ObjectExpression(obj)) = call.arguments.get(recognized.tokens_arg)
803 else {
804 return;
805 };
806 let mut tokens = Vec::new();
807 collect_token_leaves(&mut self.lines, obj, "", recognized.origin, &mut tokens);
808 if tokens.is_empty() {
809 return;
810 }
811 self.defs.push(CssInJsTokenDef {
812 binding: binding.to_owned(),
813 origin: recognized.origin,
814 tokens,
815 });
816 }
817
818 fn process_panda_config_call(&mut self, call: &oxc_ast::ast::CallExpression<'a>) -> bool {
819 let Some((Lib::Panda, "defineConfig")) = self.callee_role(&call.callee) else {
820 return false;
821 };
822 let Some(Argument::ObjectExpression(obj)) = call.arguments.first() else {
823 return true;
824 };
825 let mut tokens = Vec::new();
826 collect_panda_config_token_leaves(&mut self.lines, obj, &mut tokens);
827 if !tokens.is_empty() {
828 self.defs.push(CssInJsTokenDef {
829 binding: PANDA_CONFIG_BINDING.to_string(),
830 origin: CssInJsTokenOrigin::Panda,
831 tokens,
832 });
833 }
834 true
835 }
836}
837
838fn collect_token_leaves(
848 lines: &mut LineCounter<'_>,
849 obj: &ObjectExpression<'_>,
850 prefix: &str,
851 origin: CssInJsTokenOrigin,
852 out: &mut Vec<CssInJsToken>,
853) {
854 for prop in &obj.properties {
855 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
856 continue;
857 };
858 let Some(key) = prop.key.static_name() else {
859 continue;
860 };
861 let path = if prefix.is_empty() {
862 key.to_string()
863 } else {
864 format!("{prefix}.{key}")
865 };
866 match &prop.value {
867 Expression::ObjectExpression(nested)
868 if origin == CssInJsTokenOrigin::Panda
869 && !prefix.is_empty()
870 && object_has_static_key(nested, "value") =>
871 {
872 out.push(CssInJsToken {
873 path,
874 def_line: lines.line_at(prop.key.span().start),
875 value: object_static_property_value(nested, "value"),
876 });
877 }
878 Expression::ObjectExpression(nested) => {
879 collect_token_leaves(lines, nested, &path, origin, out);
880 }
881 Expression::Identifier(_) => {}
884 _ => out.push(CssInJsToken {
885 value: static_token_value(&prop.value),
886 path,
887 def_line: lines.line_at(prop.key.span().start),
888 }),
889 }
890 }
891}
892
893fn object_static_property_value(obj: &ObjectExpression<'_>, wanted: &str) -> Option<String> {
894 obj.properties.iter().find_map(|prop| {
895 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
896 return None;
897 };
898 (prop.key.static_name().as_deref() == Some(wanted))
899 .then(|| static_token_value(&prop.value))
900 .flatten()
901 })
902}
903
904fn static_token_value(value: &Expression<'_>) -> Option<String> {
905 match value {
906 Expression::StringLiteral(lit) => {
907 let text = lit.value.as_str().trim();
908 (!text.is_empty()).then(|| text.to_string())
909 }
910 Expression::NumericLiteral(num) => Some(format_numeric_token(num)),
911 Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
912 if let Expression::NumericLiteral(num) = &unary.argument {
913 Some(format!("-{}", format_numeric_token(num)))
914 } else {
915 None
916 }
917 }
918 _ => None,
919 }
920}
921
922fn format_numeric_token(num: &NumericLiteral<'_>) -> String {
923 if num.value.fract() == 0.0 {
924 format!("{:.0}", num.value)
925 } else {
926 num.value.to_string()
927 }
928}
929
930fn is_theme_binding_name(name: &str) -> bool {
931 let lower = name.to_ascii_lowercase();
932 lower == "theme" || lower.ends_with("theme")
933}
934
935fn object_has_static_key(obj: &ObjectExpression<'_>, wanted: &str) -> bool {
936 obj.properties.iter().any(|prop| {
937 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
938 return false;
939 };
940 prop.key.static_name().is_some_and(|key| key == wanted)
941 })
942}
943
944fn object_static_property_object<'a>(
945 obj: &'a ObjectExpression<'a>,
946 wanted: &str,
947) -> Option<&'a ObjectExpression<'a>> {
948 obj.properties.iter().find_map(|prop| {
949 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
950 return None;
951 };
952 if prop.key.static_name().as_deref() == Some(wanted)
953 && let Expression::ObjectExpression(value) = &prop.value
954 {
955 Some(&**value)
956 } else {
957 None
958 }
959 })
960}
961
962fn collect_panda_config_token_leaves(
963 lines: &mut LineCounter<'_>,
964 obj: &ObjectExpression<'_>,
965 out: &mut Vec<CssInJsToken>,
966) {
967 let Some(theme) = object_static_property_object(obj, "theme") else {
968 return;
969 };
970 for key in ["tokens", "semanticTokens"] {
971 if let Some(tokens) = object_static_property_object(theme, key) {
972 collect_token_leaves(lines, tokens, "", CssInJsTokenOrigin::Panda, out);
973 }
974 }
975}
976
977impl<'a> Visit<'a> for TokenDefCollector<'a> {
978 fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
979 self.process_declarator(decl);
980 walk::walk_variable_declarator(self, decl);
981 }
982
983 fn visit_export_default_declaration(
984 &mut self,
985 decl: &oxc_ast::ast::ExportDefaultDeclaration<'a>,
986 ) {
987 if let Some(Expression::CallExpression(call)) = decl.declaration.as_expression() {
988 self.process_panda_config_call(call);
989 }
990 walk::walk_export_default_declaration(self, decl);
991 }
992}
993
994fn count_newlines_u32(s: &str) -> u32 {
996 u32::try_from(s.bytes().filter(|&b| b == b'\n').count()).unwrap_or(u32::MAX)
997}
998
999struct LineCounter<'a> {
1011 source: &'a str,
1012 last_offset: usize,
1015 last_line: u32,
1018}
1019
1020impl<'a> LineCounter<'a> {
1021 fn new(source: &'a str) -> Self {
1022 Self {
1023 source,
1024 last_offset: 0,
1025 last_line: 1,
1026 }
1027 }
1028
1029 fn line_at(&mut self, offset: u32) -> u32 {
1032 let end = (offset as usize).min(self.source.len());
1033 if !self.source.is_char_boundary(end) {
1036 return 1;
1037 }
1038 if end >= self.last_offset {
1039 let delta = count_newlines_u32(&self.source[self.last_offset..end]);
1040 self.last_line = self.last_line.saturating_add(delta);
1041 } else {
1042 let delta = count_newlines_u32(&self.source[end..self.last_offset]);
1043 self.last_line = self.last_line.saturating_sub(delta);
1044 }
1045 self.last_offset = end;
1046 self.last_line
1047 }
1048}
1049
1050#[cfg(all(test, not(miri)))]
1051mod tests {
1052 use super::*;
1053
1054 fn defs(source: &str) -> Vec<CssInJsTokenDef> {
1055 css_in_js_token_defs(source, Path::new("tokens.ts"))
1056 }
1057
1058 fn paths(defs: &[CssInJsTokenDef], binding: &str) -> Vec<String> {
1059 defs.iter()
1060 .find(|d| d.binding == binding)
1061 .map(|d| d.tokens.iter().map(|t| t.path.clone()).collect())
1062 .unwrap_or_default()
1063 }
1064
1065 fn token_values(defs: &[CssInJsTokenDef], binding: &str) -> Vec<(String, Option<String>)> {
1066 defs.iter()
1067 .find(|d| d.binding == binding)
1068 .map(|d| {
1069 d.tokens
1070 .iter()
1071 .map(|t| (t.path.clone(), t.value.clone()))
1072 .collect()
1073 })
1074 .unwrap_or_default()
1075 }
1076
1077 fn theme_defs(source: &str) -> Vec<CssInJsTokenDef> {
1078 css_in_js_theme_token_defs(source, Path::new("theme.ts"))
1079 }
1080
1081 #[test]
1082 fn incremental_def_lines_match_source_order() {
1083 let src = "import { defineVars } from '@stylexjs/stylex';\n\
1089export const colors = defineVars({\n\
1090primary: '#000',\n\
1091secondary: '#fff',\n\
1092});\n\
1093export const space = defineVars({\n\
1094sm: '4px',\n\
1095lg: '16px',\n\
1096});\n";
1097 let d = defs(src);
1098 let line_of = |binding: &str, path: &str| {
1099 d.iter()
1100 .find(|def| def.binding == binding)
1101 .and_then(|def| def.tokens.iter().find(|t| t.path == path))
1102 .unwrap_or_else(|| panic!("token {binding}.{path} present"))
1103 .def_line
1104 };
1105 assert_eq!(line_of("colors", "primary"), 3);
1106 assert_eq!(line_of("colors", "secondary"), 4);
1107 assert_eq!(line_of("space", "sm"), 7);
1108 assert_eq!(line_of("space", "lg"), 8);
1109 }
1110
1111 #[test]
1112 fn stylex_define_vars_flat_namespace_call() {
1113 let d = defs(
1114 r"
1115import * as stylex from '@stylexjs/stylex';
1116export const vars = stylex.defineVars({ primaryColor: '#3b82f6', spacingSm: '4px' });
1117",
1118 );
1119 assert_eq!(paths(&d, "vars"), vec!["primaryColor", "spacingSm"]);
1120 assert_eq!(
1121 token_values(&d, "vars"),
1122 vec![
1123 ("primaryColor".to_string(), Some("#3b82f6".to_string())),
1124 ("spacingSm".to_string(), Some("4px".to_string())),
1125 ]
1126 );
1127 }
1128
1129 #[test]
1130 fn stylex_define_vars_named_import_nested() {
1131 let d = defs(
1132 r"
1133import { defineVars } from '@stylexjs/stylex';
1134export const vars = defineVars({ color: { primary: '#000', secondary: '#fff' } });
1135",
1136 );
1137 assert_eq!(paths(&d, "vars"), vec!["color.primary", "color.secondary"]);
1138 }
1139
1140 #[test]
1141 fn panda_define_tokens_collapses_value_objects() {
1142 let d = defs(
1143 r"
1144import { defineTokens } from '@pandacss/dev';
1145export const tokens = defineTokens({
1146 colors: {
1147 brand: { value: '#f05a28' },
1148 accent: { value: '{colors.brand}' },
1149 },
1150 spacing: { card: { value: '1rem' } },
1151});
1152",
1153 );
1154 assert_eq!(
1155 paths(&d, "tokens"),
1156 vec!["colors.brand", "colors.accent", "spacing.card"]
1157 );
1158 assert_eq!(
1159 token_values(&d, "tokens"),
1160 vec![
1161 ("colors.brand".to_string(), Some("#f05a28".to_string())),
1162 (
1163 "colors.accent".to_string(),
1164 Some("{colors.brand}".to_string())
1165 ),
1166 ("spacing.card".to_string(), Some("1rem".to_string())),
1167 ]
1168 );
1169 assert_eq!(
1170 d.iter().find(|d| d.binding == "tokens").unwrap().origin,
1171 CssInJsTokenOrigin::Panda
1172 );
1173 }
1174
1175 #[test]
1176 fn panda_define_config_extracts_tokens_and_semantic_tokens() {
1177 let d = defs(
1178 r"
1179import { defineConfig } from '@pandacss/dev';
1180
1181export default defineConfig({
1182 theme: {
1183 tokens: {
1184 colors: {
1185 brand: { value: '#f05a28' },
1186 },
1187 },
1188 semanticTokens: {
1189 colors: {
1190 surface: { value: { base: '{colors.brand}', _dark: '#111111' } },
1191 },
1192 },
1193 recipes: {
1194 card: { base: { color: 'colors.brand' } },
1195 },
1196 },
1197});
1198",
1199 );
1200 assert_eq!(
1201 paths(&d, "pandaConfig"),
1202 vec!["colors.brand", "colors.surface"]
1203 );
1204 assert_eq!(
1205 token_values(&d, "pandaConfig"),
1206 vec![
1207 ("colors.brand".to_string(), Some("#f05a28".to_string())),
1208 ("colors.surface".to_string(), None),
1209 ]
1210 );
1211 assert_eq!(
1212 d.iter()
1213 .find(|d| d.binding == "pandaConfig")
1214 .unwrap()
1215 .origin,
1216 CssInJsTokenOrigin::Panda
1217 );
1218 }
1219
1220 #[test]
1221 fn theme_object_definitions_flatten_static_leaves() {
1222 let d = theme_defs(
1223 r"
1224export const appTheme = {
1225 colors: { brand: '#f05a28', accent: '#111' },
1226 space: { card: '1rem' },
1227 dynamic: palette,
1228};
1229",
1230 );
1231 assert_eq!(
1232 paths(&d, "appTheme"),
1233 vec!["colors.brand", "colors.accent", "space.card"]
1234 );
1235 assert_eq!(
1236 token_values(&d, "appTheme"),
1237 vec![
1238 ("colors.brand".to_string(), Some("#f05a28".to_string())),
1239 ("colors.accent".to_string(), Some("#111".to_string())),
1240 ("space.card".to_string(), Some("1rem".to_string())),
1241 ]
1242 );
1243 assert_eq!(
1244 d.iter().find(|d| d.binding == "appTheme").unwrap().origin,
1245 CssInJsTokenOrigin::Theme
1246 );
1247 }
1248
1249 #[test]
1250 fn theme_consumers_credit_props_and_destructured_theme_reads() {
1251 let leaves = ["colors.brand", "space.card"]
1252 .into_iter()
1253 .map(str::to_owned)
1254 .collect();
1255 let hits = css_in_js_theme_consumers(
1256 r"
1257import styled from 'styled-components';
1258export const Card = styled.div`
1259 color: ${({ theme }) => theme.colors.brand};
1260 margin: ${props => props.theme.space.card};
1261`;
1262",
1263 Path::new("card.tsx"),
1264 &leaves,
1265 );
1266 let mut token_paths: Vec<String> = hits.into_iter().map(|hit| hit.token_path).collect();
1267 token_paths.sort();
1268 assert_eq!(token_paths, vec!["colors.brand", "space.card"]);
1269 }
1270
1271 #[test]
1272 fn ve_create_theme_tuple_destructure_binds_element_one() {
1273 let d = defs(
1274 r"
1275import { createTheme } from '@vanilla-extract/css';
1276export const [themeClass, vars] = createTheme({
1277 color: { brand: 'red', accent: 'blue' },
1278 space: { small: '4px' },
1279});
1280",
1281 );
1282 assert_eq!(
1284 paths(&d, "vars"),
1285 vec!["color.brand", "color.accent", "space.small"]
1286 );
1287 assert!(paths(&d, "themeClass").is_empty());
1288 }
1289
1290 #[test]
1291 fn ve_create_theme_contract_null_leaves() {
1292 let d = defs(
1293 r"
1294import { createThemeContract } from '@vanilla-extract/css';
1295export const vars = createThemeContract({ color: { brand: null, accent: null } });
1296",
1297 );
1298 assert_eq!(paths(&d, "vars"), vec!["color.brand", "color.accent"]);
1300 }
1301
1302 #[test]
1303 fn ve_create_global_theme_two_arg_binds_lhs_tokens_in_second_arg() {
1304 let d = defs(
1305 r"
1306import { createGlobalTheme } from '@vanilla-extract/css';
1307export const vars = createGlobalTheme(':root', { color: { brand: 'red' } });
1308",
1309 );
1310 assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
1311 }
1312
1313 #[test]
1314 fn ve_create_theme_two_arg_contract_impl_is_not_a_definition_site() {
1315 let d = defs(
1318 r"
1319import { createTheme } from '@vanilla-extract/css';
1320export const themeClass = createTheme(vars, { color: { brand: 'red' } });
1321",
1322 );
1323 assert!(
1324 d.is_empty(),
1325 "2-arg createTheme must not define tokens, got {d:?}"
1326 );
1327 }
1328
1329 #[test]
1330 fn ve_create_global_theme_three_arg_contract_impl_is_not_a_definition_site() {
1331 let d = defs(
1332 r"
1333import { createGlobalTheme } from '@vanilla-extract/css';
1334createGlobalTheme(':root', vars, { color: { brand: 'red' } });
1335",
1336 );
1337 assert!(
1338 d.is_empty(),
1339 "3-arg createGlobalTheme must not define tokens, got {d:?}"
1340 );
1341 }
1342
1343 #[test]
1344 fn aliased_named_import_still_fires() {
1345 let d = defs(
1346 r"
1347import { createThemeContract as ct } from '@vanilla-extract/css';
1348export const vars = ct({ color: { brand: null } });
1349",
1350 );
1351 assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
1352 }
1353
1354 #[test]
1355 fn local_helper_not_from_library_does_not_fire() {
1356 let d = defs(
1358 r"
1359function defineVars(o) { return o; }
1360export const vars = defineVars({ color: { primary: '#000' } });
1361",
1362 );
1363 assert!(d.is_empty(), "local defineVars must not fire, got {d:?}");
1364 }
1365
1366 #[test]
1367 fn unrelated_create_theme_import_does_not_fire() {
1368 let d = defs(
1369 r"
1370import { createTheme } from '@mui/material/styles';
1371export const theme = createTheme({ palette: { primary: { main: '#000' } } });
1372",
1373 );
1374 assert!(d.is_empty(), "non-VE createTheme must not fire, got {d:?}");
1375 }
1376
1377 #[test]
1378 fn type_only_import_does_not_fire() {
1379 let d = defs(
1380 r"
1381import type { defineVars } from '@stylexjs/stylex';
1382export const vars = defineVars({ color: { primary: '#000' } });
1383",
1384 );
1385 assert!(
1386 d.is_empty(),
1387 "type-only import must not gate recognition, got {d:?}"
1388 );
1389 }
1390
1391 #[test]
1392 fn token_def_lines_are_per_leaf() {
1393 let src = "import { defineVars } from '@stylexjs/stylex';\nexport const vars = defineVars({\n color: {\n primary: '#000',\n secondary: '#fff',\n },\n});\n";
1394 let d = defs(src);
1395 let def = d.iter().find(|d| d.binding == "vars").unwrap();
1396 let primary = def
1397 .tokens
1398 .iter()
1399 .find(|t| t.path == "color.primary")
1400 .unwrap();
1401 let secondary = def
1402 .tokens
1403 .iter()
1404 .find(|t| t.path == "color.secondary")
1405 .unwrap();
1406 assert_eq!(primary.def_line, 4);
1407 assert_eq!(secondary.def_line, 5);
1408 }
1409
1410 #[test]
1411 fn spread_and_computed_keys_are_skipped() {
1412 let d = defs(
1413 r"
1414import { defineVars } from '@stylexjs/stylex';
1415const base = { a: '1' };
1416export const vars = defineVars({ ...base, ['x' + 'y']: '2', real: '#000' });
1417",
1418 );
1419 assert_eq!(paths(&d, "vars"), vec!["real"]);
1421 }
1422
1423 #[test]
1424 fn identifier_valued_key_is_not_a_leaf_but_call_and_member_values_are() {
1425 let d = defs(
1429 r"
1430import { createGlobalTheme } from '@vanilla-extract/css';
1431export const vars = createGlobalTheme(':root', {
1432 palette: tailwindPalette,
1433 radius: px(2),
1434 red: colors.red['500'],
1435});
1436",
1437 );
1438 let p = paths(&d, "vars");
1439 assert!(
1440 !p.contains(&"palette".to_string()),
1441 "identifier-valued key must not be a leaf: {p:?}"
1442 );
1443 assert!(
1444 p.contains(&"radius".to_string()),
1445 "call-valued key is a leaf: {p:?}"
1446 );
1447 assert!(
1448 p.contains(&"red".to_string()),
1449 "member-valued key is a leaf: {p:?}"
1450 );
1451 }
1452
1453 #[test]
1454 fn no_css_in_js_import_returns_empty() {
1455 let d = defs("export const vars = { color: { primary: '#000' } };");
1456 assert!(d.is_empty());
1457 }
1458
1459 fn leaves(paths: &[&str]) -> FxHashSet<String> {
1460 paths.iter().map(|s| (*s).to_string()).collect()
1461 }
1462
1463 fn consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
1464 css_in_js_token_consumers(source, Path::new("card.ts"), alias, &leaves(paths))
1465 }
1466
1467 fn panda_consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
1468 panda_token_call_consumers(source, Path::new("card.ts"), alias, &leaves(paths))
1469 }
1470
1471 fn panda_style_consumers(
1472 source: &str,
1473 aliases: &[&str],
1474 paths: &[&str],
1475 ) -> Vec<TokenConsumerHit> {
1476 let aliases = aliases.iter().map(|s| (*s).to_string()).collect();
1477 panda_style_value_consumers(source, Path::new("card.ts"), &aliases, &leaves(paths))
1478 }
1479
1480 #[test]
1481 fn consumer_matches_deepest_leaf_not_intermediate_group() {
1482 let hits = consumers(
1485 "const a = vars.color.primary;",
1486 "vars",
1487 &["color.primary", "color.secondary"],
1488 );
1489 assert_eq!(hits.len(), 1);
1490 assert_eq!(hits[0].token_path, "color.primary");
1491 assert_eq!(hits[0].line, 1);
1492 }
1493
1494 #[test]
1495 fn consumer_aliased_receiver() {
1496 let hits = consumers("const a = v.color.primary;", "v", &["color.primary"]);
1498 assert_eq!(hits.len(), 1);
1499 assert_eq!(hits[0].token_path, "color.primary");
1500 }
1501
1502 #[test]
1503 fn consumer_multiple_sites_distinct_lines() {
1504 let src = "const a = vars.color.primary;\nconst b = vars.space.sm;\nconst c = vars.color.primary;";
1505 let hits = consumers(src, "vars", &["color.primary", "space.sm"]);
1506 assert_eq!(hits.len(), 3);
1507 let lines: Vec<u32> = hits.iter().map(|h| h.line).collect();
1508 assert_eq!(lines, vec![1, 2, 3]);
1509 }
1510
1511 #[test]
1512 fn consumer_in_style_object_value_position() {
1513 let hits = consumers(
1515 "export const s = stylex.create({ root: { color: vars.color.primary } });",
1516 "vars",
1517 &["color.primary"],
1518 );
1519 assert_eq!(hits.len(), 1);
1520 assert_eq!(hits[0].token_path, "color.primary");
1521 }
1522
1523 #[test]
1524 fn panda_token_call_consumer_matches_string_literal() {
1525 let hits = panda_consumers(
1526 "export const c = css({ color: token('colors.brand') });",
1527 "token",
1528 &["colors.brand", "colors.accent"],
1529 );
1530 assert_eq!(hits.len(), 1);
1531 assert_eq!(hits[0].token_path, "colors.brand");
1532 }
1533
1534 #[test]
1535 fn panda_style_value_consumer_matches_known_token_string() {
1536 let hits = panda_style_consumers(
1537 "export const c = css({ color: 'colors.brand', _hover: { bg: 'colors.accent' } });",
1538 &["css"],
1539 &["colors.brand", "colors.accent", "colors.unused"],
1540 );
1541 let paths: Vec<_> = hits.iter().map(|hit| hit.token_path.as_str()).collect();
1542 assert_eq!(paths, vec!["colors.brand", "colors.accent"]);
1543 }
1544
1545 #[test]
1546 fn panda_style_value_consumer_ignores_unimported_alias() {
1547 let hits = panda_style_consumers(
1548 "export const c = notPanda({ color: 'colors.brand' });",
1549 &["css"],
1550 &["colors.brand"],
1551 );
1552 assert!(hits.is_empty());
1553 }
1554
1555 #[test]
1556 fn consumer_flat_stylex_depth_one() {
1557 let hits = consumers("const a = vars.primaryColor;", "vars", &["primaryColor"]);
1558 assert_eq!(hits.len(), 1);
1559 assert_eq!(hits[0].token_path, "primaryColor");
1560 }
1561
1562 #[test]
1563 fn consumer_other_binding_not_matched() {
1564 let hits = consumers("const a = other.color.primary;", "vars", &["color.primary"]);
1566 assert!(hits.is_empty());
1567 }
1568
1569 #[test]
1570 fn consumer_deeper_access_past_leaf_matches_leaf_subexpression_once() {
1571 let hits = consumers(
1574 "const a = vars.color.primary.toString();",
1575 "vars",
1576 &["color.primary"],
1577 );
1578 assert_eq!(hits.len(), 1);
1579 assert_eq!(hits[0].token_path, "color.primary");
1580 }
1581
1582 #[test]
1583 fn consumer_undefined_path_not_matched() {
1584 let hits = consumers("const a = vars.color.tertiary;", "vars", &["color.primary"]);
1585 assert!(hits.is_empty());
1586 }
1587
1588 #[test]
1589 fn consumer_bracket_notation_hyphenated_key() {
1590 let hits = consumers(
1593 "const a = vars.color['gray-100'];\nconst b = vars.borderRadius['0x'];",
1594 "vars",
1595 &["color.gray-100", "borderRadius.0x"],
1596 );
1597 let paths: Vec<&str> = hits.iter().map(|h| h.token_path.as_str()).collect();
1598 assert!(paths.contains(&"color.gray-100"));
1599 assert!(paths.contains(&"borderRadius.0x"));
1600 assert_eq!(hits.len(), 2);
1601 }
1602
1603 #[test]
1604 fn consumer_mixed_dot_and_bracket_chain() {
1605 let hits = consumers(
1608 "const a = vars['color'].primary;\nconst b = vars.color['primary'];",
1609 "vars",
1610 &["color.primary"],
1611 );
1612 assert_eq!(hits.len(), 2);
1613 assert!(hits.iter().all(|h| h.token_path == "color.primary"));
1614 }
1615
1616 #[test]
1617 fn consumer_non_literal_computed_key_not_matched() {
1618 let hits = consumers(
1620 "const k = 'primary'; const a = vars.color[k];",
1621 "vars",
1622 &["color.primary"],
1623 );
1624 assert!(hits.is_empty());
1625 }
1626
1627 #[test]
1628 fn consumer_empty_inputs_short_circuit() {
1629 assert!(consumers("const a = vars.color.primary;", "", &["color.primary"]).is_empty());
1630 assert!(consumers("const a = vars.color.primary;", "vars", &[]).is_empty());
1631 }
1632
1633 #[test]
1634 fn consumer_scan_matches_individual_calls() {
1635 let source = "const a = vars.color.primary;\nconst b = css({ color: token('colors.brand'), background: 'colors.accent' });\nconst c = theme.space.card;";
1639 let path = Path::new("card.tsx");
1640
1641 let member_leaves = leaves(&["color.primary"]);
1642 let panda_call_leaves = leaves(&["colors.brand"]);
1643 let panda_style_aliases = leaves(&["css"]);
1644 let panda_style_leaves = leaves(&["colors.accent"]);
1645 let theme_leaves = leaves(&["space.card"]);
1646
1647 let queries = [
1648 ConsumerQuery::MemberBinding {
1649 alias: "vars",
1650 leaf_paths: &member_leaves,
1651 },
1652 ConsumerQuery::PandaTokenCall {
1653 alias: "token",
1654 leaf_paths: &panda_call_leaves,
1655 },
1656 ConsumerQuery::PandaStyleValues {
1657 aliases: &panda_style_aliases,
1658 leaf_paths: &panda_style_leaves,
1659 },
1660 ConsumerQuery::ThemeReads {
1661 leaf_paths: &theme_leaves,
1662 },
1663 ];
1664 let scanned = css_in_js_consumer_scan(source, path, &queries);
1665
1666 let individual: Vec<(usize, TokenConsumerHit)> =
1667 css_in_js_token_consumers(source, path, "vars", &member_leaves)
1668 .into_iter()
1669 .map(|hit| (0, hit))
1670 .chain(
1671 panda_token_call_consumers(source, path, "token", &panda_call_leaves)
1672 .into_iter()
1673 .map(|hit| (1, hit)),
1674 )
1675 .chain(
1676 panda_style_value_consumers(
1677 source,
1678 path,
1679 &panda_style_aliases,
1680 &panda_style_leaves,
1681 )
1682 .into_iter()
1683 .map(|hit| (2, hit)),
1684 )
1685 .chain(
1686 css_in_js_theme_consumers(source, path, &theme_leaves)
1687 .into_iter()
1688 .map(|hit| (3, hit)),
1689 )
1690 .collect();
1691
1692 assert_eq!(scanned, individual);
1693 assert_eq!(scanned.len(), 4);
1694 assert_eq!(
1695 scanned[0],
1696 (
1697 0,
1698 TokenConsumerHit {
1699 token_path: "color.primary".to_string(),
1700 line: 1,
1701 }
1702 )
1703 );
1704 assert_eq!(
1705 scanned[3],
1706 (
1707 3,
1708 TokenConsumerHit {
1709 token_path: "space.card".to_string(),
1710 line: 3,
1711 }
1712 )
1713 );
1714 }
1715
1716 #[test]
1717 fn consumer_scan_empty_query_is_isolated() {
1718 let source = "const a = vars.color.primary;";
1721 let path = Path::new("card.ts");
1722 let empty_leaves = leaves(&["color.primary"]);
1723 let valid_leaves = leaves(&["color.primary"]);
1724 let queries = [
1725 ConsumerQuery::MemberBinding {
1726 alias: "",
1727 leaf_paths: &empty_leaves,
1728 },
1729 ConsumerQuery::MemberBinding {
1730 alias: "vars",
1731 leaf_paths: &valid_leaves,
1732 },
1733 ];
1734 let scanned = css_in_js_consumer_scan(source, path, &queries);
1735 assert_eq!(scanned.len(), 1);
1736 assert_eq!(scanned[0].0, 1);
1737 assert_eq!(scanned[0].1.token_path, "color.primary");
1738 }
1739
1740 #[test]
1741 fn consumer_scan_two_member_queries_same_source() {
1742 let source = "const a = brand.color.primary;\nconst b = accent.color.primary;";
1745 let path = Path::new("card.ts");
1746 let brand_leaves = leaves(&["color.primary"]);
1747 let accent_leaves = leaves(&["color.primary"]);
1748 let queries = [
1749 ConsumerQuery::MemberBinding {
1750 alias: "brand",
1751 leaf_paths: &brand_leaves,
1752 },
1753 ConsumerQuery::MemberBinding {
1754 alias: "accent",
1755 leaf_paths: &accent_leaves,
1756 },
1757 ];
1758 let scanned = css_in_js_consumer_scan(source, path, &queries);
1759 assert_eq!(scanned.len(), 2);
1760 assert!(scanned.contains(&(
1761 0,
1762 TokenConsumerHit {
1763 token_path: "color.primary".to_string(),
1764 line: 1,
1765 }
1766 )));
1767 assert!(scanned.contains(&(
1768 1,
1769 TokenConsumerHit {
1770 token_path: "color.primary".to_string(),
1771 line: 2,
1772 }
1773 )));
1774 }
1775}