use std::path::Path;
use oxc_allocator::Allocator;
use oxc_ast::ast::{
Argument, BindingPattern, ComputedMemberExpression, Expression, ImportDeclarationSpecifier,
NumericLiteral, ObjectExpression, ObjectPropertyKind, Program, Statement,
StaticMemberExpression, UnaryOperator, VariableDeclarator,
};
use oxc_ast_visit::{Visit, walk};
use oxc_parser::Parser;
use oxc_span::{GetSpan, SourceType};
use rustc_hash::{FxHashMap, FxHashSet};
use super::object::{Lib, module_library};
const PANDA_CONFIG_BINDING: &str = "pandaConfig";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CssInJsToken {
pub path: String,
pub def_line: u32,
pub value: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CssInJsTokenDef {
pub binding: String,
pub origin: CssInJsTokenOrigin,
pub tokens: Vec<CssInJsToken>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CssInJsTokenOrigin {
StyleX,
VanillaExtract,
Panda,
Theme,
}
#[must_use]
pub fn css_in_js_token_defs(source: &str, path: &Path) -> Vec<CssInJsTokenDef> {
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
let mut collector = TokenDefCollector::new(source);
collector.build_import_map(&ret.program);
if collector.imports.is_empty() {
return Vec::new();
}
collector.visit_program(&ret.program);
collector.defs
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenConsumerHit {
pub token_path: String,
pub line: u32,
}
#[must_use]
#[expect(
clippy::implicit_hasher,
reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
)]
pub fn css_in_js_token_consumers(
source: &str,
path: &Path,
alias: &str,
leaf_paths: &FxHashSet<String>,
) -> Vec<TokenConsumerHit> {
if alias.is_empty() || leaf_paths.is_empty() {
return Vec::new();
}
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
let mut collector = ConsumerCollector {
source,
alias,
leaf_paths,
hits: Vec::new(),
};
collector.visit_program(&ret.program);
collector.hits
}
#[must_use]
#[expect(
clippy::implicit_hasher,
reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
)]
pub fn panda_token_call_consumers(
source: &str,
path: &Path,
alias: &str,
leaf_paths: &FxHashSet<String>,
) -> Vec<TokenConsumerHit> {
if alias.is_empty() || leaf_paths.is_empty() {
return Vec::new();
}
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
let mut collector = PandaTokenCallCollector {
source,
alias,
leaf_paths,
hits: Vec::new(),
};
collector.visit_program(&ret.program);
collector.hits
}
#[must_use]
#[expect(
clippy::implicit_hasher,
reason = "callers build FxHashSet values; std HashSet is a disallowed type here"
)]
pub fn panda_style_value_consumers(
source: &str,
path: &Path,
aliases: &FxHashSet<String>,
leaf_paths: &FxHashSet<String>,
) -> Vec<TokenConsumerHit> {
if aliases.is_empty() || leaf_paths.is_empty() {
return Vec::new();
}
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
let mut collector = PandaStyleValueCollector {
source,
aliases,
leaf_paths,
hits: Vec::new(),
};
collector.visit_program(&ret.program);
collector.hits
}
#[must_use]
pub fn css_in_js_theme_token_defs(source: &str, path: &Path) -> Vec<CssInJsTokenDef> {
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
let mut collector = ThemeDefCollector {
source,
defs: Vec::new(),
};
collector.visit_program(&ret.program);
collector.defs
}
#[must_use]
#[expect(
clippy::implicit_hasher,
reason = "callers build an FxHashSet; std HashSet is a disallowed type here"
)]
pub fn css_in_js_theme_consumers(
source: &str,
path: &Path,
leaf_paths: &FxHashSet<String>,
) -> Vec<TokenConsumerHit> {
if leaf_paths.is_empty() {
return Vec::new();
}
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
let mut collector = ThemeConsumerCollector {
source,
leaf_paths,
hits: Vec::new(),
};
collector.visit_program(&ret.program);
collector.hits
}
struct ConsumerCollector<'a, 'b> {
source: &'a str,
alias: &'b str,
leaf_paths: &'b FxHashSet<String>,
hits: Vec<TokenConsumerHit>,
}
impl<'a> ConsumerCollector<'a, '_> {
fn record(&mut self, chain: Option<(&'a str, Vec<&'a str>)>, span_start: u32) {
if let Some((base, segments)) = chain
&& base == self.alias
&& !segments.is_empty()
{
let token_path = segments.join(".");
if self.leaf_paths.contains(&token_path) {
self.hits.push(TokenConsumerHit {
token_path,
line: line_at(self.source, span_start),
});
}
}
}
}
impl<'a> Visit<'a> for ConsumerCollector<'a, '_> {
fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
let mut chain = access_object_chain(&member.object);
if let Some((_, segments)) = chain.as_mut() {
segments.push(member.property.name.as_str());
}
self.record(chain, member.span().start);
walk::walk_static_member_expression(self, member);
}
fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
let mut chain = access_object_chain(&member.object);
if let (Some((_, segments)), Some(key)) =
(chain.as_mut(), string_literal_key(&member.expression))
{
segments.push(key);
} else {
chain = None;
}
self.record(chain, member.span().start);
walk::walk_computed_member_expression(self, member);
}
}
struct PandaTokenCallCollector<'a, 'b> {
source: &'a str,
alias: &'b str,
leaf_paths: &'b FxHashSet<String>,
hits: Vec<TokenConsumerHit>,
}
impl<'a> Visit<'a> for PandaTokenCallCollector<'a, '_> {
fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
let Expression::Identifier(callee) = &call.callee else {
walk::walk_call_expression(self, call);
return;
};
if callee.name.as_str() == self.alias
&& let Some(Argument::StringLiteral(lit)) = call.arguments.first()
{
let token_path = lit.value.as_str();
if self.leaf_paths.contains(token_path) {
self.hits.push(TokenConsumerHit {
token_path: token_path.to_owned(),
line: line_at(self.source, call.span().start),
});
}
}
walk::walk_call_expression(self, call);
}
}
struct PandaStyleValueCollector<'a, 'b> {
source: &'a str,
aliases: &'b FxHashSet<String>,
leaf_paths: &'b FxHashSet<String>,
hits: Vec<TokenConsumerHit>,
}
impl<'a> PandaStyleValueCollector<'a, '_> {
fn record_object(&mut self, obj: &ObjectExpression<'a>) {
for prop in &obj.properties {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
continue;
};
self.record_expression(&prop.value);
}
}
fn record_expression(&mut self, expr: &Expression<'a>) {
match expr {
Expression::StringLiteral(lit) => {
let token_path = lit.value.as_str();
if self.leaf_paths.contains(token_path) {
self.hits.push(TokenConsumerHit {
token_path: token_path.to_owned(),
line: line_at(self.source, lit.span().start),
});
}
}
Expression::ObjectExpression(obj) => self.record_object(obj),
_ => {}
}
}
}
impl<'a> Visit<'a> for PandaStyleValueCollector<'a, '_> {
fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
let Expression::Identifier(callee) = &call.callee else {
walk::walk_call_expression(self, call);
return;
};
if self.aliases.contains(callee.name.as_str()) {
for arg in &call.arguments {
if let Argument::ObjectExpression(obj) = arg {
self.record_object(obj);
}
}
}
walk::walk_call_expression(self, call);
}
}
struct ThemeDefCollector<'a> {
source: &'a str,
defs: Vec<CssInJsTokenDef>,
}
impl<'a> ThemeDefCollector<'a> {
fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
let BindingPattern::BindingIdentifier(binding) = &decl.id else {
return;
};
let binding_name = binding.name.as_str();
if !is_theme_binding_name(binding_name) {
return;
}
let Some(Expression::ObjectExpression(obj)) = &decl.init else {
return;
};
let mut tokens = Vec::new();
collect_token_leaves(self.source, obj, "", CssInJsTokenOrigin::Theme, &mut tokens);
if tokens.is_empty() {
return;
}
self.defs.push(CssInJsTokenDef {
binding: binding_name.to_owned(),
origin: CssInJsTokenOrigin::Theme,
tokens,
});
}
}
impl<'a> Visit<'a> for ThemeDefCollector<'a> {
fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
self.process_declarator(decl);
walk::walk_variable_declarator(self, decl);
}
}
struct ThemeConsumerCollector<'a, 'b> {
source: &'a str,
leaf_paths: &'b FxHashSet<String>,
hits: Vec<TokenConsumerHit>,
}
impl<'a> ThemeConsumerCollector<'a, '_> {
fn record(&mut self, chain: Option<(&'a str, Vec<&'a str>)>, span_start: u32) {
let Some((base, segments)) = chain else {
return;
};
let token_segments: &[&str] = match base {
"theme" => &segments,
"props" if segments.first().copied() == Some("theme") => &segments[1..],
_ => return,
};
if token_segments.is_empty() {
return;
}
let token_path = token_segments.join(".");
if self.leaf_paths.contains(&token_path) {
self.hits.push(TokenConsumerHit {
token_path,
line: line_at(self.source, span_start),
});
}
}
}
impl<'a> Visit<'a> for ThemeConsumerCollector<'a, '_> {
fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
let mut chain = access_object_chain(&member.object);
if let Some((_, segments)) = chain.as_mut() {
segments.push(member.property.name.as_str());
}
self.record(chain, member.span().start);
walk::walk_static_member_expression(self, member);
}
fn visit_computed_member_expression(&mut self, member: &ComputedMemberExpression<'a>) {
let mut chain = access_object_chain(&member.object);
if let (Some((_, segments)), Some(key)) =
(chain.as_mut(), string_literal_key(&member.expression))
{
segments.push(key);
} else {
chain = None;
}
self.record(chain, member.span().start);
walk::walk_computed_member_expression(self, member);
}
}
fn access_object_chain<'a>(expr: &Expression<'a>) -> Option<(&'a str, Vec<&'a str>)> {
match expr {
Expression::Identifier(id) => Some((id.name.as_str(), Vec::new())),
Expression::StaticMemberExpression(inner) => {
let (base, mut segments) = access_object_chain(&inner.object)?;
segments.push(inner.property.name.as_str());
Some((base, segments))
}
Expression::ComputedMemberExpression(inner) => {
let (base, mut segments) = access_object_chain(&inner.object)?;
segments.push(string_literal_key(&inner.expression)?);
Some((base, segments))
}
_ => None,
}
}
fn string_literal_key<'a>(expr: &Expression<'a>) -> Option<&'a str> {
match expr {
Expression::StringLiteral(lit) => Some(lit.value.as_str()),
_ => None,
}
}
#[derive(Clone, Copy)]
enum BindingSource {
LhsIdent,
TupleElement(usize),
}
#[derive(Clone, Copy)]
struct Recognized {
binding_source: BindingSource,
tokens_arg: usize,
origin: CssInJsTokenOrigin,
}
struct TokenDefCollector<'a> {
source: &'a str,
imports: FxHashMap<&'a str, (Lib, &'a str)>,
defs: Vec<CssInJsTokenDef>,
}
impl<'a> TokenDefCollector<'a> {
fn new(source: &'a str) -> Self {
Self {
source,
imports: FxHashMap::default(),
defs: Vec::new(),
}
}
fn build_import_map(&mut self, program: &Program<'a>) {
for stmt in &program.body {
let Statement::ImportDeclaration(decl) = stmt else {
continue;
};
if decl.import_kind.is_type() {
continue;
}
let Some(lib) = module_library(decl.source.value.as_str()) else {
continue;
};
let Some(specifiers) = &decl.specifiers else {
continue;
};
for specifier in specifiers {
let (local, role) = match specifier {
ImportDeclarationSpecifier::ImportSpecifier(s) => {
(s.local.name.as_str(), s.imported.name().as_str())
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
(s.local.name.as_str(), s.local.name.as_str())
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
(s.local.name.as_str(), s.local.name.as_str())
}
};
self.imports.insert(local, (lib, role));
}
}
}
fn callee_role(&self, callee: &Expression<'a>) -> Option<(Lib, &'a str)> {
match callee {
Expression::Identifier(id) => self.imports.get(id.name.as_str()).copied(),
Expression::StaticMemberExpression(member) => {
let Expression::Identifier(obj) = &member.object else {
return None;
};
let (lib, _) = *self.imports.get(obj.name.as_str())?;
Some((lib, member.property.name.as_str()))
}
_ => None,
}
}
fn recognize(lib: Lib, role: &str, arg_count: usize) -> Option<Recognized> {
let single = |tokens_arg, origin| {
Some(Recognized {
binding_source: BindingSource::LhsIdent,
tokens_arg,
origin,
})
};
match (lib, role) {
(Lib::StyleX, "defineVars") if arg_count >= 1 => single(0, CssInJsTokenOrigin::StyleX),
(Lib::VanillaExtract, "createThemeContract") if arg_count >= 1 => {
single(0, CssInJsTokenOrigin::VanillaExtract)
}
(Lib::VanillaExtract, "createTheme") if arg_count == 1 => Some(Recognized {
binding_source: BindingSource::TupleElement(1),
tokens_arg: 0,
origin: CssInJsTokenOrigin::VanillaExtract,
}),
(Lib::VanillaExtract, "createGlobalTheme") if arg_count == 2 => {
single(1, CssInJsTokenOrigin::VanillaExtract)
}
(Lib::Panda, "defineTokens") if arg_count >= 1 => single(0, CssInJsTokenOrigin::Panda),
_ => None,
}
}
fn binding_name(decl: &VariableDeclarator<'a>, source: BindingSource) -> Option<&'a str> {
match source {
BindingSource::LhsIdent => match &decl.id {
BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
_ => None,
},
BindingSource::TupleElement(index) => {
let BindingPattern::ArrayPattern(arr) = &decl.id else {
return None;
};
let element = arr.elements.get(index)?.as_ref()?;
match element {
BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
_ => None,
}
}
}
}
fn process_declarator(&mut self, decl: &VariableDeclarator<'a>) {
let Some(Expression::CallExpression(call)) = &decl.init else {
return;
};
if self.process_panda_config_call(call) {
return;
}
let Some((lib, role)) = self.callee_role(&call.callee) else {
return;
};
let Some(recognized) = Self::recognize(lib, role, call.arguments.len()) else {
return;
};
let Some(binding) = Self::binding_name(decl, recognized.binding_source) else {
return;
};
let Some(Argument::ObjectExpression(obj)) = call.arguments.get(recognized.tokens_arg)
else {
return;
};
let mut tokens = Vec::new();
collect_token_leaves(self.source, obj, "", recognized.origin, &mut tokens);
if tokens.is_empty() {
return;
}
self.defs.push(CssInJsTokenDef {
binding: binding.to_owned(),
origin: recognized.origin,
tokens,
});
}
fn process_panda_config_call(&mut self, call: &oxc_ast::ast::CallExpression<'a>) -> bool {
let Some((Lib::Panda, "defineConfig")) = self.callee_role(&call.callee) else {
return false;
};
let Some(Argument::ObjectExpression(obj)) = call.arguments.first() else {
return true;
};
let mut tokens = Vec::new();
collect_panda_config_token_leaves(self.source, obj, &mut tokens);
if !tokens.is_empty() {
self.defs.push(CssInJsTokenDef {
binding: PANDA_CONFIG_BINDING.to_string(),
origin: CssInJsTokenOrigin::Panda,
tokens,
});
}
true
}
}
fn collect_token_leaves(
source: &str,
obj: &ObjectExpression<'_>,
prefix: &str,
origin: CssInJsTokenOrigin,
out: &mut Vec<CssInJsToken>,
) {
for prop in &obj.properties {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
continue;
};
let Some(key) = prop.key.static_name() else {
continue;
};
let path = if prefix.is_empty() {
key.to_string()
} else {
format!("{prefix}.{key}")
};
match &prop.value {
Expression::ObjectExpression(nested)
if origin == CssInJsTokenOrigin::Panda
&& !prefix.is_empty()
&& object_has_static_key(nested, "value") =>
{
out.push(CssInJsToken {
path,
def_line: line_at(source, prop.key.span().start),
value: object_static_property_value(nested, "value"),
});
}
Expression::ObjectExpression(nested) => {
collect_token_leaves(source, nested, &path, origin, out);
}
Expression::Identifier(_) => {}
_ => out.push(CssInJsToken {
value: static_token_value(&prop.value),
path,
def_line: line_at(source, prop.key.span().start),
}),
}
}
}
fn object_static_property_value(obj: &ObjectExpression<'_>, wanted: &str) -> Option<String> {
obj.properties.iter().find_map(|prop| {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
return None;
};
(prop.key.static_name().as_deref() == Some(wanted))
.then(|| static_token_value(&prop.value))
.flatten()
})
}
fn static_token_value(value: &Expression<'_>) -> Option<String> {
match value {
Expression::StringLiteral(lit) => {
let text = lit.value.as_str().trim();
(!text.is_empty()).then(|| text.to_string())
}
Expression::NumericLiteral(num) => Some(format_numeric_token(num)),
Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
if let Expression::NumericLiteral(num) = &unary.argument {
Some(format!("-{}", format_numeric_token(num)))
} else {
None
}
}
_ => None,
}
}
fn format_numeric_token(num: &NumericLiteral<'_>) -> String {
if num.value.fract() == 0.0 {
format!("{:.0}", num.value)
} else {
num.value.to_string()
}
}
fn is_theme_binding_name(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
lower == "theme" || lower.ends_with("theme")
}
fn object_has_static_key(obj: &ObjectExpression<'_>, wanted: &str) -> bool {
obj.properties.iter().any(|prop| {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
return false;
};
prop.key.static_name().is_some_and(|key| key == wanted)
})
}
fn object_static_property_object<'a>(
obj: &'a ObjectExpression<'a>,
wanted: &str,
) -> Option<&'a ObjectExpression<'a>> {
obj.properties.iter().find_map(|prop| {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
return None;
};
if prop.key.static_name().as_deref() == Some(wanted)
&& let Expression::ObjectExpression(value) = &prop.value
{
Some(&**value)
} else {
None
}
})
}
fn collect_panda_config_token_leaves(
source: &str,
obj: &ObjectExpression<'_>,
out: &mut Vec<CssInJsToken>,
) {
let Some(theme) = object_static_property_object(obj, "theme") else {
return;
};
for key in ["tokens", "semanticTokens"] {
if let Some(tokens) = object_static_property_object(theme, key) {
collect_token_leaves(source, tokens, "", CssInJsTokenOrigin::Panda, out);
}
}
}
impl<'a> Visit<'a> for TokenDefCollector<'a> {
fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
self.process_declarator(decl);
walk::walk_variable_declarator(self, decl);
}
fn visit_export_default_declaration(
&mut self,
decl: &oxc_ast::ast::ExportDefaultDeclaration<'a>,
) {
if let Some(Expression::CallExpression(call)) = decl.declaration.as_expression() {
self.process_panda_config_call(call);
}
walk::walk_export_default_declaration(self, decl);
}
}
fn line_at(source: &str, offset: u32) -> u32 {
let end = (offset as usize).min(source.len());
let count = source
.get(..end)
.map_or(0, |s| s.bytes().filter(|&b| b == b'\n').count());
u32::try_from(1 + count).unwrap_or(u32::MAX)
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
fn defs(source: &str) -> Vec<CssInJsTokenDef> {
css_in_js_token_defs(source, Path::new("tokens.ts"))
}
fn paths(defs: &[CssInJsTokenDef], binding: &str) -> Vec<String> {
defs.iter()
.find(|d| d.binding == binding)
.map(|d| d.tokens.iter().map(|t| t.path.clone()).collect())
.unwrap_or_default()
}
fn token_values(defs: &[CssInJsTokenDef], binding: &str) -> Vec<(String, Option<String>)> {
defs.iter()
.find(|d| d.binding == binding)
.map(|d| {
d.tokens
.iter()
.map(|t| (t.path.clone(), t.value.clone()))
.collect()
})
.unwrap_or_default()
}
fn theme_defs(source: &str) -> Vec<CssInJsTokenDef> {
css_in_js_theme_token_defs(source, Path::new("theme.ts"))
}
#[test]
fn stylex_define_vars_flat_namespace_call() {
let d = defs(
r"
import * as stylex from '@stylexjs/stylex';
export const vars = stylex.defineVars({ primaryColor: '#3b82f6', spacingSm: '4px' });
",
);
assert_eq!(paths(&d, "vars"), vec!["primaryColor", "spacingSm"]);
assert_eq!(
token_values(&d, "vars"),
vec![
("primaryColor".to_string(), Some("#3b82f6".to_string())),
("spacingSm".to_string(), Some("4px".to_string())),
]
);
}
#[test]
fn stylex_define_vars_named_import_nested() {
let d = defs(
r"
import { defineVars } from '@stylexjs/stylex';
export const vars = defineVars({ color: { primary: '#000', secondary: '#fff' } });
",
);
assert_eq!(paths(&d, "vars"), vec!["color.primary", "color.secondary"]);
}
#[test]
fn panda_define_tokens_collapses_value_objects() {
let d = defs(
r"
import { defineTokens } from '@pandacss/dev';
export const tokens = defineTokens({
colors: {
brand: { value: '#f05a28' },
accent: { value: '{colors.brand}' },
},
spacing: { card: { value: '1rem' } },
});
",
);
assert_eq!(
paths(&d, "tokens"),
vec!["colors.brand", "colors.accent", "spacing.card"]
);
assert_eq!(
token_values(&d, "tokens"),
vec![
("colors.brand".to_string(), Some("#f05a28".to_string())),
(
"colors.accent".to_string(),
Some("{colors.brand}".to_string())
),
("spacing.card".to_string(), Some("1rem".to_string())),
]
);
assert_eq!(
d.iter().find(|d| d.binding == "tokens").unwrap().origin,
CssInJsTokenOrigin::Panda
);
}
#[test]
fn panda_define_config_extracts_tokens_and_semantic_tokens() {
let d = defs(
r"
import { defineConfig } from '@pandacss/dev';
export default defineConfig({
theme: {
tokens: {
colors: {
brand: { value: '#f05a28' },
},
},
semanticTokens: {
colors: {
surface: { value: { base: '{colors.brand}', _dark: '#111111' } },
},
},
recipes: {
card: { base: { color: 'colors.brand' } },
},
},
});
",
);
assert_eq!(
paths(&d, "pandaConfig"),
vec!["colors.brand", "colors.surface"]
);
assert_eq!(
token_values(&d, "pandaConfig"),
vec![
("colors.brand".to_string(), Some("#f05a28".to_string())),
("colors.surface".to_string(), None),
]
);
assert_eq!(
d.iter()
.find(|d| d.binding == "pandaConfig")
.unwrap()
.origin,
CssInJsTokenOrigin::Panda
);
}
#[test]
fn theme_object_definitions_flatten_static_leaves() {
let d = theme_defs(
r"
export const appTheme = {
colors: { brand: '#f05a28', accent: '#111' },
space: { card: '1rem' },
dynamic: palette,
};
",
);
assert_eq!(
paths(&d, "appTheme"),
vec!["colors.brand", "colors.accent", "space.card"]
);
assert_eq!(
token_values(&d, "appTheme"),
vec![
("colors.brand".to_string(), Some("#f05a28".to_string())),
("colors.accent".to_string(), Some("#111".to_string())),
("space.card".to_string(), Some("1rem".to_string())),
]
);
assert_eq!(
d.iter().find(|d| d.binding == "appTheme").unwrap().origin,
CssInJsTokenOrigin::Theme
);
}
#[test]
fn theme_consumers_credit_props_and_destructured_theme_reads() {
let leaves = ["colors.brand", "space.card"]
.into_iter()
.map(str::to_owned)
.collect();
let hits = css_in_js_theme_consumers(
r"
import styled from 'styled-components';
export const Card = styled.div`
color: ${({ theme }) => theme.colors.brand};
margin: ${props => props.theme.space.card};
`;
",
Path::new("card.tsx"),
&leaves,
);
let mut token_paths: Vec<String> = hits.into_iter().map(|hit| hit.token_path).collect();
token_paths.sort();
assert_eq!(token_paths, vec!["colors.brand", "space.card"]);
}
#[test]
fn ve_create_theme_tuple_destructure_binds_element_one() {
let d = defs(
r"
import { createTheme } from '@vanilla-extract/css';
export const [themeClass, vars] = createTheme({
color: { brand: 'red', accent: 'blue' },
space: { small: '4px' },
});
",
);
assert_eq!(
paths(&d, "vars"),
vec!["color.brand", "color.accent", "space.small"]
);
assert!(paths(&d, "themeClass").is_empty());
}
#[test]
fn ve_create_theme_contract_null_leaves() {
let d = defs(
r"
import { createThemeContract } from '@vanilla-extract/css';
export const vars = createThemeContract({ color: { brand: null, accent: null } });
",
);
assert_eq!(paths(&d, "vars"), vec!["color.brand", "color.accent"]);
}
#[test]
fn ve_create_global_theme_two_arg_binds_lhs_tokens_in_second_arg() {
let d = defs(
r"
import { createGlobalTheme } from '@vanilla-extract/css';
export const vars = createGlobalTheme(':root', { color: { brand: 'red' } });
",
);
assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
}
#[test]
fn ve_create_theme_two_arg_contract_impl_is_not_a_definition_site() {
let d = defs(
r"
import { createTheme } from '@vanilla-extract/css';
export const themeClass = createTheme(vars, { color: { brand: 'red' } });
",
);
assert!(
d.is_empty(),
"2-arg createTheme must not define tokens, got {d:?}"
);
}
#[test]
fn ve_create_global_theme_three_arg_contract_impl_is_not_a_definition_site() {
let d = defs(
r"
import { createGlobalTheme } from '@vanilla-extract/css';
createGlobalTheme(':root', vars, { color: { brand: 'red' } });
",
);
assert!(
d.is_empty(),
"3-arg createGlobalTheme must not define tokens, got {d:?}"
);
}
#[test]
fn aliased_named_import_still_fires() {
let d = defs(
r"
import { createThemeContract as ct } from '@vanilla-extract/css';
export const vars = ct({ color: { brand: null } });
",
);
assert_eq!(paths(&d, "vars"), vec!["color.brand"]);
}
#[test]
fn local_helper_not_from_library_does_not_fire() {
let d = defs(
r"
function defineVars(o) { return o; }
export const vars = defineVars({ color: { primary: '#000' } });
",
);
assert!(d.is_empty(), "local defineVars must not fire, got {d:?}");
}
#[test]
fn unrelated_create_theme_import_does_not_fire() {
let d = defs(
r"
import { createTheme } from '@mui/material/styles';
export const theme = createTheme({ palette: { primary: { main: '#000' } } });
",
);
assert!(d.is_empty(), "non-VE createTheme must not fire, got {d:?}");
}
#[test]
fn type_only_import_does_not_fire() {
let d = defs(
r"
import type { defineVars } from '@stylexjs/stylex';
export const vars = defineVars({ color: { primary: '#000' } });
",
);
assert!(
d.is_empty(),
"type-only import must not gate recognition, got {d:?}"
);
}
#[test]
fn token_def_lines_are_per_leaf() {
let src = "import { defineVars } from '@stylexjs/stylex';\nexport const vars = defineVars({\n color: {\n primary: '#000',\n secondary: '#fff',\n },\n});\n";
let d = defs(src);
let def = d.iter().find(|d| d.binding == "vars").unwrap();
let primary = def
.tokens
.iter()
.find(|t| t.path == "color.primary")
.unwrap();
let secondary = def
.tokens
.iter()
.find(|t| t.path == "color.secondary")
.unwrap();
assert_eq!(primary.def_line, 4);
assert_eq!(secondary.def_line, 5);
}
#[test]
fn spread_and_computed_keys_are_skipped() {
let d = defs(
r"
import { defineVars } from '@stylexjs/stylex';
const base = { a: '1' };
export const vars = defineVars({ ...base, ['x' + 'y']: '2', real: '#000' });
",
);
assert_eq!(paths(&d, "vars"), vec!["real"]);
}
#[test]
fn identifier_valued_key_is_not_a_leaf_but_call_and_member_values_are() {
let d = defs(
r"
import { createGlobalTheme } from '@vanilla-extract/css';
export const vars = createGlobalTheme(':root', {
palette: tailwindPalette,
radius: px(2),
red: colors.red['500'],
});
",
);
let p = paths(&d, "vars");
assert!(
!p.contains(&"palette".to_string()),
"identifier-valued key must not be a leaf: {p:?}"
);
assert!(
p.contains(&"radius".to_string()),
"call-valued key is a leaf: {p:?}"
);
assert!(
p.contains(&"red".to_string()),
"member-valued key is a leaf: {p:?}"
);
}
#[test]
fn no_css_in_js_import_returns_empty() {
let d = defs("export const vars = { color: { primary: '#000' } };");
assert!(d.is_empty());
}
fn leaves(paths: &[&str]) -> FxHashSet<String> {
paths.iter().map(|s| (*s).to_string()).collect()
}
fn consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
css_in_js_token_consumers(source, Path::new("card.ts"), alias, &leaves(paths))
}
fn panda_consumers(source: &str, alias: &str, paths: &[&str]) -> Vec<TokenConsumerHit> {
panda_token_call_consumers(source, Path::new("card.ts"), alias, &leaves(paths))
}
fn panda_style_consumers(
source: &str,
aliases: &[&str],
paths: &[&str],
) -> Vec<TokenConsumerHit> {
let aliases = aliases.iter().map(|s| (*s).to_string()).collect();
panda_style_value_consumers(source, Path::new("card.ts"), &aliases, &leaves(paths))
}
#[test]
fn consumer_matches_deepest_leaf_not_intermediate_group() {
let hits = consumers(
"const a = vars.color.primary;",
"vars",
&["color.primary", "color.secondary"],
);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "color.primary");
assert_eq!(hits[0].line, 1);
}
#[test]
fn consumer_aliased_receiver() {
let hits = consumers("const a = v.color.primary;", "v", &["color.primary"]);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "color.primary");
}
#[test]
fn consumer_multiple_sites_distinct_lines() {
let src = "const a = vars.color.primary;\nconst b = vars.space.sm;\nconst c = vars.color.primary;";
let hits = consumers(src, "vars", &["color.primary", "space.sm"]);
assert_eq!(hits.len(), 3);
let lines: Vec<u32> = hits.iter().map(|h| h.line).collect();
assert_eq!(lines, vec![1, 2, 3]);
}
#[test]
fn consumer_in_style_object_value_position() {
let hits = consumers(
"export const s = stylex.create({ root: { color: vars.color.primary } });",
"vars",
&["color.primary"],
);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "color.primary");
}
#[test]
fn panda_token_call_consumer_matches_string_literal() {
let hits = panda_consumers(
"export const c = css({ color: token('colors.brand') });",
"token",
&["colors.brand", "colors.accent"],
);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "colors.brand");
}
#[test]
fn panda_style_value_consumer_matches_known_token_string() {
let hits = panda_style_consumers(
"export const c = css({ color: 'colors.brand', _hover: { bg: 'colors.accent' } });",
&["css"],
&["colors.brand", "colors.accent", "colors.unused"],
);
let paths: Vec<_> = hits.iter().map(|hit| hit.token_path.as_str()).collect();
assert_eq!(paths, vec!["colors.brand", "colors.accent"]);
}
#[test]
fn panda_style_value_consumer_ignores_unimported_alias() {
let hits = panda_style_consumers(
"export const c = notPanda({ color: 'colors.brand' });",
&["css"],
&["colors.brand"],
);
assert!(hits.is_empty());
}
#[test]
fn consumer_flat_stylex_depth_one() {
let hits = consumers("const a = vars.primaryColor;", "vars", &["primaryColor"]);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "primaryColor");
}
#[test]
fn consumer_other_binding_not_matched() {
let hits = consumers("const a = other.color.primary;", "vars", &["color.primary"]);
assert!(hits.is_empty());
}
#[test]
fn consumer_deeper_access_past_leaf_matches_leaf_subexpression_once() {
let hits = consumers(
"const a = vars.color.primary.toString();",
"vars",
&["color.primary"],
);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].token_path, "color.primary");
}
#[test]
fn consumer_undefined_path_not_matched() {
let hits = consumers("const a = vars.color.tertiary;", "vars", &["color.primary"]);
assert!(hits.is_empty());
}
#[test]
fn consumer_bracket_notation_hyphenated_key() {
let hits = consumers(
"const a = vars.color['gray-100'];\nconst b = vars.borderRadius['0x'];",
"vars",
&["color.gray-100", "borderRadius.0x"],
);
let paths: Vec<&str> = hits.iter().map(|h| h.token_path.as_str()).collect();
assert!(paths.contains(&"color.gray-100"));
assert!(paths.contains(&"borderRadius.0x"));
assert_eq!(hits.len(), 2);
}
#[test]
fn consumer_mixed_dot_and_bracket_chain() {
let hits = consumers(
"const a = vars['color'].primary;\nconst b = vars.color['primary'];",
"vars",
&["color.primary"],
);
assert_eq!(hits.len(), 2);
assert!(hits.iter().all(|h| h.token_path == "color.primary"));
}
#[test]
fn consumer_non_literal_computed_key_not_matched() {
let hits = consumers(
"const k = 'primary'; const a = vars.color[k];",
"vars",
&["color.primary"],
);
assert!(hits.is_empty());
}
#[test]
fn consumer_empty_inputs_short_circuit() {
assert!(consumers("const a = vars.color.primary;", "", &["color.primary"]).is_empty());
assert!(consumers("const a = vars.color.primary;", "vars", &[]).is_empty());
}
}