1use std::path::Path;
77
78use oxc_allocator::Allocator;
79use oxc_ast::ast::{
80 Argument, BindingPattern, Expression, IdentifierReference, ImportDeclarationSpecifier,
81 NumericLiteral, ObjectExpression, ObjectPropertyKind, Program, PropertyKey, Statement,
82 UnaryOperator, VariableDeclaration, VariableDeclarationKind,
83};
84use oxc_ast_visit::{Visit, walk};
85use oxc_parser::Parser;
86use oxc_semantic::{ReferenceId, Scoping, SemanticBuilder};
87use oxc_span::{GetSpan, SourceType};
88use oxc_str::JSStr;
89use rustc_hash::FxHashMap;
90
91use super::shared::{WRAPPER, count_newlines};
92use crate::js_text::JsText;
93
94const UNITLESS_PROPERTIES: &[&str] = &[
100 "animationIterationCount",
101 "aspectRatio",
102 "borderImageOutset",
103 "borderImageSlice",
104 "borderImageWidth",
105 "boxFlex",
106 "boxFlexGroup",
107 "boxOrdinalGroup",
108 "columnCount",
109 "columns",
110 "flex",
111 "flexGrow",
112 "flexPositive",
113 "flexShrink",
114 "flexNegative",
115 "flexOrder",
116 "gridArea",
117 "gridRow",
118 "gridRowEnd",
119 "gridRowSpan",
120 "gridRowStart",
121 "gridColumn",
122 "gridColumnEnd",
123 "gridColumnSpan",
124 "gridColumnStart",
125 "fontWeight",
126 "lineClamp",
127 "lineHeight",
128 "opacity",
129 "order",
130 "orphans",
131 "scale",
132 "tabSize",
133 "widows",
134 "zIndex",
135 "zoom",
136 "fillOpacity",
137 "floodOpacity",
138 "stopOpacity",
139 "strokeDasharray",
140 "strokeDashoffset",
141 "strokeMiterlimit",
142 "strokeOpacity",
143 "strokeWidth",
144];
145
146#[derive(Clone, Copy, PartialEq, Eq)]
150pub(super) enum Lib {
151 VanillaExtract,
154 Emotion,
156 EmotionStyled,
158 StyleX,
160 Panda,
162}
163
164impl Lib {
165 const fn is_atomic(self) -> bool {
169 matches!(self, Self::StyleX | Self::Panda)
170 }
171}
172
173#[derive(Debug, Default, PartialEq, Eq)]
179pub struct CssInJsObjectSheets {
180 pub structural: Option<String>,
183 pub structural_partial: Option<String>,
186 pub atomic: Option<String>,
189}
190
191impl CssInJsObjectSheets {
192 #[must_use]
194 pub const fn is_empty(&self) -> bool {
195 self.structural.is_none() && self.structural_partial.is_none() && self.atomic.is_none()
196 }
197}
198
199#[derive(Clone, Copy, PartialEq, Eq)]
201enum Stream {
202 Structural,
203 StructuralPartial,
204 Atomic,
205}
206
207struct Bucket {
211 offset: u32,
212 rule: String,
213 stream: Stream,
214}
215
216#[must_use]
222pub fn css_in_js_object_sheets(source: &str, path: &Path) -> CssInJsObjectSheets {
223 let source_type = SourceType::from_path(path).unwrap_or_default();
224 crate::parse_bounded(source, || {
225 let allocator = Allocator::default();
226 let ret = Parser::new(&allocator, source, source_type).parse();
230 if !has_recognized_value_import(&ret.program) {
231 return CssInJsObjectSheets::default();
232 }
233 let semantic_ret = SemanticBuilder::new().build(&ret.program);
234 let scoping = semantic_ret.semantic.scoping();
235
236 let mut collector = ObjectStyleCollector::new(source);
237 collector.build_import_map(&ret.program, scoping);
238 if collector.imports.is_empty() {
239 return CssInJsObjectSheets::default();
242 }
243 if collector
244 .imports
245 .values()
246 .any(|(library, _)| *library == Lib::StyleX)
247 {
248 collector.build_const_string_map(&ret.program, scoping);
249 }
250 collector.visit_program(&ret.program);
251 collector.finish()
252 })
253 .unwrap_or_default()
254}
255
256fn has_recognized_value_import(program: &Program<'_>) -> bool {
257 program.body.iter().any(|statement| {
258 let Statement::ImportDeclaration(declaration) = statement else {
259 return false;
260 };
261 !declaration.import_kind.is_type()
262 && module_library(&declaration.source.value.text()).is_some()
263 && declaration.specifiers.as_ref().is_some_and(|specifiers| {
264 specifiers.iter().any(|specifier| {
265 !matches!(
266 specifier,
267 ImportDeclarationSpecifier::ImportSpecifier(specifier)
268 if specifier.import_kind.is_type()
269 )
270 })
271 })
272 })
273}
274
275struct ObjectStyleCollector<'a> {
278 source: &'a str,
279 imports: FxHashMap<ReferenceId, (Lib, &'a str)>,
283 const_strings: FxHashMap<ReferenceId, (u32, JSStr<'a>)>,
287 buckets: Vec<Bucket>,
288}
289
290impl<'a> ObjectStyleCollector<'a> {
291 fn new(source: &'a str) -> Self {
292 Self {
293 source,
294 imports: FxHashMap::default(),
295 const_strings: FxHashMap::default(),
296 buckets: Vec::new(),
297 }
298 }
299
300 fn build_import_map(&mut self, program: &Program<'a>, scoping: &Scoping) {
305 for stmt in &program.body {
306 let Statement::ImportDeclaration(decl) = stmt else {
307 continue;
308 };
309 if decl.import_kind.is_type() {
310 continue;
311 }
312 let Some(lib) = module_library(&decl.source.value.text()) else {
313 continue;
314 };
315 let Some(specifiers) = &decl.specifiers else {
316 continue;
317 };
318 for specifier in specifiers {
319 let (binding, role) = match specifier {
320 ImportDeclarationSpecifier::ImportSpecifier(s) if !s.import_kind.is_type() => {
323 (&s.local, s.imported.name().as_str())
324 }
325 ImportDeclarationSpecifier::ImportSpecifier(_) => continue,
326 ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
333 let role = if lib == Lib::Emotion {
334 "css"
335 } else {
336 s.local.name.as_str()
337 };
338 (&s.local, role)
339 }
340 ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
341 (&s.local, s.local.name.as_str())
342 }
343 };
344 let Some(symbol_id) = binding.symbol_id.get() else {
345 continue;
346 };
347 self.imports.extend(
348 scoping
349 .get_resolved_reference_ids(symbol_id)
350 .iter()
351 .copied()
352 .map(|reference_id| (reference_id, (lib, role))),
353 );
354 }
355 }
356 }
357
358 fn build_const_string_map(&mut self, program: &Program<'a>, scoping: &Scoping) {
359 let mut collector = ConstStringCollector {
360 scoping,
361 values: &mut self.const_strings,
362 };
363 collector.visit_program(program);
364 }
365
366 fn imported_binding(&self, id: &IdentifierReference<'_>) -> Option<(Lib, &'a str)> {
367 let reference_id = id.reference_id.get()?;
368 self.imports.get(&reference_id).copied()
369 }
370
371 fn finish(self) -> CssInJsObjectSheets {
372 let source = self.source;
373 let mut buckets = self.buckets;
374 buckets.sort_by_key(|b| b.offset);
378 CssInJsObjectSheets {
379 structural: render(source, &buckets, Stream::Structural),
380 structural_partial: render(source, &buckets, Stream::StructuralPartial),
381 atomic: render(source, &buckets, Stream::Atomic),
382 }
383 }
384
385 fn recognize(&self, callee: &Expression<'a>) -> Option<(Lib, CallKind)> {
389 match callee {
390 Expression::Identifier(id) => {
391 let (lib, role) = self.imported_binding(id)?;
392 let kind = match (lib, role) {
393 (Lib::VanillaExtract, "style") | (Lib::Emotion | Lib::Panda, "css") => {
395 CallKind::SingleObject
396 }
397 (Lib::VanillaExtract, "styleVariants") | (Lib::StyleX, "create") => {
399 CallKind::ObjectOfObjects
400 }
401 (Lib::VanillaExtract, "globalStyle") => CallKind::GlobalStyle,
403 (Lib::VanillaExtract, "recipe") | (Lib::Panda, "cva") => CallKind::RecipeBase,
405 _ => return None,
406 };
407 Some((lib, kind))
408 }
409 Expression::StaticMemberExpression(member) => {
412 let Expression::Identifier(obj) = &member.object else {
413 return None;
414 };
415 let (lib, _) = self.imported_binding(obj)?;
416 let kind = match (lib, member.property.name.as_str()) {
417 (Lib::EmotionStyled, _) => CallKind::SingleObject,
418 (Lib::StyleX, "create") => CallKind::ObjectOfObjects,
419 _ => return None,
420 };
421 Some((lib, kind))
422 }
423 Expression::CallExpression(inner) => {
425 let Expression::Identifier(id) = &inner.callee else {
426 return None;
427 };
428 matches!(self.imported_binding(id), Some((Lib::EmotionStyled, _)))
429 .then_some((Lib::EmotionStyled, CallKind::SingleObject))
430 }
431 _ => None,
432 }
433 }
434
435 fn collect_call(&mut self, callee: &Expression<'a>, args: &[Argument<'a>]) {
437 let Some((lib, kind)) = self.recognize(callee) else {
438 return;
439 };
440 match kind {
441 CallKind::SingleObject => {
442 if let Some(obj) = object_arg(args, 0) {
443 self.push_bucket(obj, WRAPPER, lib, obj.span().start);
444 }
445 }
446 CallKind::ObjectOfObjects => {
447 if args.len() != 1 {
452 return;
453 }
454 let Some(obj) = object_arg(args, 0) else {
455 return;
456 };
457 for prop in &obj.properties {
458 if let ObjectPropertyKind::ObjectProperty(p) = prop
459 && let Some(inner) = object_expression(&p.value)
460 {
461 self.push_bucket(inner, WRAPPER, lib, p.key.span().start);
462 }
463 }
464 }
465 CallKind::RecipeBase => {
466 let Some(obj) = object_arg(args, 0) else {
471 return;
472 };
473 for prop in &obj.properties {
474 if let ObjectPropertyKind::ObjectProperty(p) = prop
475 && static_key(&p.key).as_deref() == Some("base")
476 && let Some(inner) = object_expression(&p.value)
477 {
478 self.push_bucket(inner, WRAPPER, lib, p.key.span().start);
479 }
480 }
481 }
482 CallKind::GlobalStyle => {
483 let (Some(selector), Some(obj)) = (string_arg(args, 0), object_arg(args, 1)) else {
485 return;
486 };
487 let selector = sanitize_selector(&selector);
488 if !selector.is_empty() {
489 self.push_bucket(obj, &selector, lib, obj.span().start);
490 }
491 }
492 }
493 }
494
495 fn push_bucket(&mut self, obj: &ObjectExpression<'a>, selector: &str, lib: Lib, offset: u32) {
500 let mut body = String::new();
501 let mut dropped = false;
502 let context = ObjectSerializationContext {
503 stylex: lib == Lib::StyleX,
504 const_strings: &self.const_strings,
505 before: offset,
506 };
507 serialize_object_body(obj, &mut body, &mut dropped, &context);
508 if body.is_empty() {
509 return;
510 }
511 let stream = if lib.is_atomic() {
512 Stream::Atomic
513 } else if dropped {
514 Stream::StructuralPartial
515 } else {
516 Stream::Structural
517 };
518 self.buckets.push(Bucket {
519 offset,
520 rule: format!("{selector}{{{body}}}"),
521 stream,
522 });
523 }
524}
525
526impl<'a> Visit<'a> for ObjectStyleCollector<'a> {
527 fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
528 self.collect_call(&call.callee, &call.arguments);
529 walk::walk_call_expression(self, call);
530 }
531}
532
533struct ConstStringCollector<'a, 's, 'm> {
534 scoping: &'s Scoping,
535 values: &'m mut FxHashMap<ReferenceId, (u32, JSStr<'a>)>,
536}
537
538impl<'a> Visit<'a> for ConstStringCollector<'a, '_, '_> {
539 fn visit_variable_declaration(&mut self, declaration: &VariableDeclaration<'a>) {
540 if declaration.kind == VariableDeclarationKind::Const {
541 for declarator in &declaration.declarations {
542 let BindingPattern::BindingIdentifier(binding) = &declarator.id else {
543 continue;
544 };
545 let Some(Expression::StringLiteral(value)) = declarator
546 .init
547 .as_ref()
548 .map(Expression::get_inner_expression)
549 else {
550 continue;
551 };
552 let Some(symbol_id) = binding.symbol_id.get() else {
553 continue;
554 };
555 self.values.extend(
556 self.scoping
557 .get_resolved_reference_ids(symbol_id)
558 .iter()
559 .copied()
560 .map(|reference_id| (reference_id, (declarator.span.start, value.value))),
561 );
562 }
563 }
564 walk::walk_variable_declaration(self, declaration);
565 }
566}
567
568fn render(source: &str, buckets: &[Bucket], stream: Stream) -> Option<String> {
572 let mut out = String::new();
573 let mut current_line: usize = 1;
574 let mut found = false;
575 for bucket in buckets.iter().filter(|b| b.stream == stream) {
576 let block_line = 1 + count_newlines(&source[..bucket.offset as usize]);
577 while current_line < block_line {
578 out.push('\n');
579 current_line += 1;
580 }
581 out.push_str(&bucket.rule);
582 current_line += count_newlines(&bucket.rule);
583 found = true;
584 }
585 found.then_some(out)
586}
587
588enum CallKind {
590 SingleObject,
593 ObjectOfObjects,
596 RecipeBase,
599 GlobalStyle,
602}
603
604pub(super) fn module_library(specifier: &str) -> Option<Lib> {
610 match specifier {
611 "@pandacss/dev" => Some(Lib::Panda),
612 "@vanilla-extract/css" | "@vanilla-extract/recipes" => Some(Lib::VanillaExtract),
613 "@emotion/react" | "@emotion/css" => Some(Lib::Emotion),
614 "@emotion/styled" => Some(Lib::EmotionStyled),
615 "@stylexjs/stylex" | "stylex" => Some(Lib::StyleX),
616 _ if specifier
617 .split(['/', '\\'])
618 .any(|segment| segment == "styled-system") =>
619 {
620 Some(Lib::Panda)
621 }
622 _ => None,
623 }
624}
625
626fn object_arg<'a: 'b, 'b>(
628 args: &'b [Argument<'a>],
629 index: usize,
630) -> Option<&'b ObjectExpression<'a>> {
631 object_expression(args.get(index)?.as_expression()?)
632}
633
634fn object_expression<'a: 'b, 'b>(
638 expression: &'b Expression<'a>,
639) -> Option<&'b ObjectExpression<'a>> {
640 match expression.get_inner_expression() {
641 Expression::ObjectExpression(object) => Some(object),
642 _ => None,
643 }
644}
645
646fn string_arg(args: &[Argument<'_>], index: usize) -> Option<String> {
648 match args.get(index)?.as_expression()?.get_inner_expression() {
649 Expression::StringLiteral(lit) => Some(lit.value.text().into_owned()),
650 _ => None,
651 }
652}
653
654struct ObjectSerializationContext<'maps, 'ast> {
662 stylex: bool,
663 const_strings: &'maps FxHashMap<ReferenceId, (u32, JSStr<'ast>)>,
664 before: u32,
665}
666
667fn serialize_object_body(
668 obj: &ObjectExpression<'_>,
669 out: &mut String,
670 dropped: &mut bool,
671 context: &ObjectSerializationContext<'_, '_>,
672) {
673 for prop in &obj.properties {
674 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
675 *dropped = true;
677 continue;
678 };
679 let Some(key) = static_key(&prop.key) else {
680 *dropped = true;
682 continue;
683 };
684 let value = prop.value.get_inner_expression();
685 match value {
686 Expression::ObjectExpression(nested) if is_selector_key(&key) => {
687 serialize_nested(&key, nested, out, dropped, context);
688 }
689 Expression::ObjectExpression(nested) if context.stylex => {
690 let mut conditional_body = String::new();
691 if serialize_stylex_conditional_values(&key, nested, &mut conditional_body, context)
692 {
693 out.push_str(&conditional_body);
694 } else {
695 *dropped = true;
696 }
697 }
698 Expression::ObjectExpression(_) => {
699 *dropped = true;
700 }
701 value => {
702 if let Some(rendered) = serialize_value(&key, value) {
703 out.push_str(&rendered);
704 } else {
705 *dropped = true;
706 }
707 }
708 }
709 }
710}
711
712fn serialize_nested(
716 key: &str,
717 nested: &ObjectExpression<'_>,
718 out: &mut String,
719 dropped: &mut bool,
720 context: &ObjectSerializationContext<'_, '_>,
721) {
722 if key == "selectors" {
725 for prop in &nested.properties {
726 match prop {
727 ObjectPropertyKind::ObjectProperty(p) => {
728 if let (Some(inner_key), Some(inner)) =
729 (static_key(&p.key), object_expression(&p.value))
730 {
731 serialize_nested(&inner_key, inner, out, dropped, context);
732 } else {
733 *dropped = true;
734 }
735 }
736 ObjectPropertyKind::SpreadProperty(_) => *dropped = true,
737 }
738 }
739 return;
740 }
741
742 let mut body = String::new();
743 serialize_object_body(nested, &mut body, dropped, context);
744 if body.is_empty() {
745 return;
746 }
747 out.push_str(&nested_selector(key));
748 out.push('{');
749 out.push_str(&body);
750 out.push('}');
751}
752
753fn serialize_stylex_conditional_values(
754 property: &str,
755 conditional: &ObjectExpression<'_>,
756 out: &mut String,
757 context: &ObjectSerializationContext<'_, '_>,
758) -> bool {
759 for entry in &conditional.properties {
760 let ObjectPropertyKind::ObjectProperty(entry) = entry else {
761 return false;
762 };
763 if !is_static_stylex_condition(entry, context) {
764 return false;
765 }
766 match entry.value.get_inner_expression() {
767 Expression::ObjectExpression(nested) => {
768 if !serialize_stylex_conditional_values(property, nested, out, context) {
769 return false;
770 }
771 }
772 value => {
773 if let Some(rendered) = serialize_value(property, value) {
774 out.push_str(&rendered);
775 } else {
776 return false;
777 }
778 }
779 }
780 }
781 true
782}
783
784fn is_static_stylex_condition(
785 property: &oxc_ast::ast::ObjectProperty<'_>,
786 context: &ObjectSerializationContext<'_, '_>,
787) -> bool {
788 if !property.computed {
789 return property
790 .key
791 .static_name()
792 .is_some_and(|key| is_stylex_condition(&key.text()));
793 }
794 let Some(expression) = property.key.as_expression() else {
795 return false;
796 };
797 match expression.get_inner_expression() {
798 Expression::StringLiteral(value) => is_stylex_condition(&value.value.text()),
799 Expression::Identifier(id) => id
800 .reference_id
801 .get()
802 .and_then(|reference_id| context.const_strings.get(&reference_id))
803 .is_some_and(|(declaration_start, condition)| {
804 *declaration_start < context.before && is_stylex_condition(&condition.text())
805 }),
806 _ => false,
807 }
808}
809
810fn is_stylex_condition(value: &str) -> bool {
811 value == "default" || value.starts_with(':') || value.starts_with('@') || value.starts_with('[')
812}
813
814fn is_selector_key(key: &str) -> bool {
821 if key == "selectors" {
822 return true;
823 }
824 matches!(
825 key.trim_start().chars().next(),
826 Some(':' | '&' | '@' | '>' | '+' | '~' | '.' | '#' | '[' | '*')
827 ) || key.starts_with(' ')
828}
829
830fn nested_selector(key: &str) -> String {
834 let trimmed = key.trim();
835 if trimmed.starts_with('@') || trimmed.starts_with('&') {
836 return trimmed.to_string();
837 }
838 format!("&{trimmed}")
839}
840
841fn serialize_value(key: &str, value: &Expression<'_>) -> Option<String> {
844 let rendered = static_value(key, value)?;
845 Some(format!("{}:{rendered};", kebab_case(key)))
846}
847
848fn static_value(key: &str, value: &Expression<'_>) -> Option<String> {
852 match value.get_inner_expression() {
853 Expression::StringLiteral(lit) => {
854 let text = lit.value.text().trim().to_owned();
855 (!text.is_empty()).then_some(text)
856 }
857 Expression::NumericLiteral(num) => Some(render_number(key, num)),
858 Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
859 if let Expression::NumericLiteral(num) = &unary.argument {
860 Some(format!("-{}", render_number(key, num)))
861 } else {
862 None
863 }
864 }
865 _ => None,
866 }
867}
868
869fn render_number(key: &str, num: &NumericLiteral<'_>) -> String {
879 let value = format_f64(num.value);
880 if is_unitless(key) || key.starts_with("--") || num.value == 0.0 {
881 value
882 } else {
883 format!("{value}px")
884 }
885}
886
887fn format_f64(value: f64) -> String {
888 if value.fract() == 0.0 {
889 format!("{value:.0}")
890 } else {
891 value.to_string()
892 }
893}
894
895fn is_unitless(key: &str) -> bool {
897 UNITLESS_PROPERTIES.contains(&key)
898}
899
900fn static_key(key: &PropertyKey<'_>) -> Option<String> {
903 key.static_name().map(|name| name.text().into_owned())
904}
905
906fn kebab_case(name: &str) -> String {
912 if name.starts_with("--") || name.contains('-') {
913 return name.to_string();
914 }
915 let mut out = String::with_capacity(name.len() + 2);
916 if let Some(rest) = name.strip_prefix("ms")
920 && rest.chars().next().is_some_and(|c| c.is_ascii_uppercase())
921 {
922 out.push('-');
923 }
924 for ch in name.chars() {
925 if ch.is_ascii_uppercase() {
926 out.push('-');
927 out.push(ch.to_ascii_lowercase());
928 } else {
929 out.push(ch);
930 }
931 }
932 out
933}
934
935fn sanitize_selector(selector: &str) -> String {
940 selector
941 .chars()
942 .filter(|&c| c != '{' && c != '}' && c != ';')
943 .collect::<String>()
944 .trim()
945 .to_string()
946}
947
948#[cfg(all(test, not(miri)))]
949mod tests {
950 use super::*;
951 use crate::compute_css_analytics;
952
953 fn sheets(source: &str) -> CssInJsObjectSheets {
954 css_in_js_object_sheets(source, Path::new("styles.ts"))
955 }
956
957 #[test]
958 fn vanilla_extract_style_lifts_to_parseable_css() {
959 let src = "import { style } from '@vanilla-extract/css';\n\
960 export const box = style({\n\
961 backgroundColor: 'red',\n\
962 padding: 8,\n\
963 });\n";
964 let s = sheets(src);
965 let css = s.structural.expect("vanilla-extract style is structural");
966 assert!(css.contains("background-color:red;"), "css={css:?}");
968 assert!(css.contains("padding:8px;"), "px default: css={css:?}");
969 let a = compute_css_analytics(&css).expect("lifted CSS parses");
970 assert!(a.total_declarations >= 2, "declarations counted: {a:?}");
971 assert!(s.atomic.is_none(), "vanilla-extract is not atomic");
972 }
973
974 #[test]
975 fn unitless_properties_keep_bare_number() {
976 let src = "import { style } from '@vanilla-extract/css';\n\
977 const x = style({ lineHeight: 1.5, zIndex: 10, fontWeight: 700, padding: 4 });\n";
978 let css = sheets(src).structural.expect("structural");
979 assert!(css.contains("line-height:1.5;"), "css={css:?}");
980 assert!(css.contains("z-index:10;"), "css={css:?}");
981 assert!(css.contains("font-weight:700;"), "css={css:?}");
982 assert!(css.contains("padding:4px;"), "css={css:?}");
983 }
984
985 #[test]
986 fn one_level_nesting_via_relative_selector() {
987 let src = "import { style } from '@vanilla-extract/css';\n\
988 const x = style({ color: 'red', ':hover': { color: 'blue' } });\n";
989 let css = sheets(src).structural.expect("structural");
990 assert!(
991 css.contains("&:hover{color:blue;}"),
992 "nested rule: css={css:?}"
993 );
994 let a = compute_css_analytics(&css).expect("nested parses");
995 assert!(a.rule_count >= 2, "nested rule counted: {a:?}");
996 }
997
998 #[test]
999 fn vanilla_extract_selectors_wrapper_unwrapped() {
1000 let src = "import { style } from '@vanilla-extract/css';\n\
1001 const x = style({ color: 'red', selectors: { '&:hover': { color: 'blue' } } });\n";
1002 let css = sheets(src).structural.expect("structural");
1003 assert!(
1004 css.contains("&:hover{color:blue;}"),
1005 "selectors wrapper unwrapped: css={css:?}"
1006 );
1007 assert!(
1009 !css.contains("selectors{"),
1010 "no literal selectors rule: css={css:?}"
1011 );
1012 }
1013
1014 #[test]
1015 fn global_style_keeps_real_selector() {
1016 let src = "import { globalStyle } from '@vanilla-extract/css';\n\
1017 globalStyle('html, body', { margin: 0 });\n";
1018 let css = sheets(src).structural.expect("structural");
1019 assert!(
1020 css.contains("html, body{margin:0;}"),
1021 "real selector: css={css:?}"
1022 );
1023 let a = compute_css_analytics(&css).expect("parses");
1024 assert_eq!(a.rule_count, 1);
1025 }
1026
1027 #[test]
1028 fn stylex_create_is_atomic_one_bucket_per_key() {
1029 let src = "import * as stylex from '@stylexjs/stylex';\n\
1030 export const styles = stylex.create({\n\
1031 root: { color: 'red', padding: 16 },\n\
1032 card: { color: 'blue' },\n\
1033 });\n";
1034 let s = sheets(src);
1035 assert!(s.structural.is_none(), "stylex is atomic, not structural");
1036 let css = s.atomic.expect("stylex.create is atomic");
1037 assert!(css.contains("color:red;"), "css={css:?}");
1038 assert!(css.contains("padding:16px;"), "css={css:?}");
1039 assert!(css.contains("color:blue;"), "second bucket: css={css:?}");
1040 let a = compute_css_analytics(&css).expect("parses");
1041 assert!(a.rule_count >= 2, "two buckets: {a:?}");
1042 }
1043
1044 #[test]
1045 fn stylex_named_create_and_conditional_values_feed_atomic_vocabulary() {
1046 let src = "import { create as makeStyles } from 'stylex';\n\
1047 const DARK = '@media (prefers-color-scheme: dark)';\n\
1048 export const styles = makeStyles({\n\
1049 root: { color: { default: '#111', [DARK]: '#eee' } },\n\
1050 });\n";
1051 let sheets = sheets(src);
1052 assert!(sheets.structural.is_none());
1053 let css = sheets.atomic.expect("StyleX named create is atomic");
1054 assert!(css.contains("color:#111;"), "default value: {css:?}");
1055 assert!(css.contains("color:#eee;"), "conditional value: {css:?}");
1056 let analytics = compute_css_analytics(&css).expect("conditional sheet parses");
1057 assert!(analytics.colors.iter().any(|color| color == "#111"));
1058 assert!(analytics.colors.iter().any(|color| color == "#eee"));
1059 }
1060
1061 #[test]
1062 fn stylex_dynamic_conditional_key_does_not_feed_atomic_vocabulary() {
1063 let src = "import * as stylex from '@stylexjs/stylex';\n\
1064 export const styles = stylex.create({\n\
1065 root: { color: { default: '#111', [getCondition()]: '#eee' } },\n\
1066 });\n";
1067 assert!(sheets(src).is_empty());
1068 }
1069
1070 #[test]
1071 fn stylex_static_computed_conditional_keys_feed_atomic_vocabulary() {
1072 let src = "import * as stylex from '@stylexjs/stylex';\n\
1073 export const styles = stylex.create({\n\
1074 root: { color: { ['default']: '#111', ['@media (prefers-color-scheme: dark)']: '#eee' } },\n\
1075 });\n";
1076 let css = sheets(src)
1077 .atomic
1078 .expect("static computed StyleX conditions are recovered");
1079 assert!(css.contains("color:#111;"), "default value: {css:?}");
1080 assert!(css.contains("color:#eee;"), "media value: {css:?}");
1081 }
1082
1083 #[test]
1084 fn stylex_computed_condition_declared_after_create_abstains() {
1085 let src = "import * as stylex from '@stylexjs/stylex';\n\
1086 export const styles = stylex.create({\n\
1087 root: { color: { default: '#111', [DARK]: '#eee' } },\n\
1088 });\n\
1089 const DARK = '@media (prefers-color-scheme: dark)';\n";
1090 assert!(sheets(src).is_empty());
1091 }
1092
1093 #[test]
1094 fn stylex_pseudo_and_selector_conditions_feed_atomic_vocabulary() {
1095 let src = "import * as stylex from '@stylexjs/stylex';\n\
1096 export const styles = stylex.create({ root: { color: {\n\
1097 default: 'red', ':hover': 'blue', '[data-active]': 'green',\n\
1098 '@media (width > 10px)': { ':active': 'black' },\n\
1099 } } });\n";
1100 let css = sheets(src)
1101 .atomic
1102 .expect("static StyleX pseudo conditions are recovered");
1103 for value in ["red", "blue", "green", "black"] {
1104 assert!(css.contains(&format!("color:{value};")), "value: {css:?}");
1105 }
1106 }
1107
1108 #[test]
1109 fn stylex_shadowed_namespace_abstains_in_every_lexical_scope() {
1110 let src = "import * as stylex from '@stylexjs/stylex';\n\
1111 const valid = stylex.create({ root: { color: 'red' } });\n\
1112 function parameter(stylex) { stylex.create({ root: { color: 'blue' } }); }\n\
1113 { stylex.create({ root: { color: 'green' } }); const stylex = local; }\n\
1114 for (const stylex of libraries) { stylex.create({ root: { color: 'pink' } }); }\n\
1115 try {} catch (stylex) { stylex.create({ root: { color: 'orange' } }); }\n";
1116 let css = sheets(src).atomic.expect("unshadowed StyleX call survives");
1117 assert!(css.contains("color:red;"), "module import call: {css:?}");
1118 for shadowed in ["blue", "green", "pink", "orange"] {
1119 assert!(
1120 !css.contains(shadowed),
1121 "shadowed namespace must abstain for {shadowed}: {css:?}"
1122 );
1123 }
1124 }
1125
1126 #[test]
1127 fn stylex_shadowed_named_create_abstains_in_every_lexical_scope() {
1128 let src = "import { create as makeStyles } from '@stylexjs/stylex';\n\
1129 const valid = makeStyles({ root: { color: 'red' } });\n\
1130 function parameter(makeStyles) { makeStyles({ root: { color: 'blue' } }); }\n\
1131 { makeStyles({ root: { color: 'green' } }); const makeStyles = local; }\n\
1132 for (const makeStyles of factories) { makeStyles({ root: { color: 'pink' } }); }\n\
1133 try {} catch (makeStyles) { makeStyles({ root: { color: 'orange' } }); }\n";
1134 let css = sheets(src)
1135 .atomic
1136 .expect("unshadowed named create call survives");
1137 assert!(css.contains("color:red;"), "module import call: {css:?}");
1138 for shadowed in ["blue", "green", "pink", "orange"] {
1139 assert!(
1140 !css.contains(shadowed),
1141 "shadowed create alias must abstain for {shadowed}: {css:?}"
1142 );
1143 }
1144 }
1145
1146 #[test]
1147 fn stylex_transparent_typescript_wrappers_preserve_static_objects() {
1148 let src = "import * as stylex from '@stylexjs/stylex';\n\
1149 export const styles = stylex.create((({\n\
1150 root: ({ color: ('red' as const), padding: (8 satisfies number) } satisfies Record<string, unknown>),\n\
1151 } as const) satisfies Record<string, unknown>));\n";
1152 let css = sheets(src)
1153 .atomic
1154 .expect("wrapped static StyleX object is recovered");
1155 assert!(css.contains("color:red;"), "wrapped string: {css:?}");
1156 assert!(css.contains("padding:8px;"), "wrapped number: {css:?}");
1157 }
1158
1159 #[test]
1160 fn stylex_shadowed_computed_condition_binding_abstains() {
1161 let src = "import * as stylex from '@stylexjs/stylex';\n\
1162 const CONDITION = '@media (width > 10px)';\n\
1163 function styles() {\n\
1164 const CONDITION = getCondition();\n\
1165 return stylex.create({ root: { color: { default: 'red', [CONDITION]: 'blue' } } });\n\
1166 }\n";
1167 assert!(sheets(src).is_empty());
1168 }
1169
1170 #[test]
1171 fn stylex_type_only_named_create_does_not_open_gate() {
1172 let src = "import { type create } from '@stylexjs/stylex';\n\
1173 const styles = create({ root: { color: 'red' } });\n";
1174 assert!(sheets(src).is_empty());
1175 }
1176
1177 #[test]
1178 fn panda_css_from_styled_system_is_atomic() {
1179 let src = "import { css } from '../styled-system/css';\n\
1180 const c = css({ display: 'flex', gap: 8 });\n";
1181 let s = sheets(src);
1182 let css = s.atomic.expect("panda css is atomic");
1183 assert!(css.contains("display:flex;"), "css={css:?}");
1184 assert!(css.contains("gap:8px;"), "css={css:?}");
1185 }
1186
1187 #[test]
1188 fn emotion_css_and_styled_are_structural() {
1189 let src = "import { css } from '@emotion/react';\n\
1190 import styled from '@emotion/styled';\n\
1191 const a = css({ color: 'red' });\n\
1192 const B = styled.div({ fontWeight: 700 });\n";
1193 let css = sheets(src).structural.expect("emotion is structural");
1194 assert!(css.contains("color:red;"), "css={css:?}");
1195 assert!(css.contains("font-weight:700;"), "styled.div: css={css:?}");
1196 }
1197
1198 #[test]
1199 fn styled_call_form_is_lifted() {
1200 let src = "import styled from '@emotion/styled';\n\
1201 const Primary = styled(Button)({ fontWeight: 700 });\n";
1202 let css = sheets(src)
1203 .structural
1204 .expect("styled(Component)({}) lifted");
1205 assert!(css.contains("font-weight:700;"), "css={css:?}");
1206 }
1207
1208 #[test]
1209 fn dynamic_value_is_dropped_to_structural_partial() {
1210 let src = "import { style } from '@vanilla-extract/css';\n\
1211 import { theme } from './theme';\n\
1212 const x = style({ color: theme.primary, padding: 8, margin: 4, top: 1, left: 2 });\n";
1213 let s = sheets(src);
1214 assert!(s.structural.is_none(), "bucket had a drop: {s:?}");
1218 let css = s.structural_partial.expect("partial");
1219 assert!(
1220 !css.contains("fallowinterp"),
1221 "no placeholder, value dropped: {css:?}"
1222 );
1223 assert!(
1224 !css.contains("primary"),
1225 "dynamic member not serialized: {css:?}"
1226 );
1227 assert!(css.contains("padding:8px;"), "static survives: {css:?}");
1228 let a = compute_css_analytics(&css).expect("must parse, not None");
1229 assert_eq!(a.important_declarations, 0, "no invented !important: {a:?}");
1230 }
1231
1232 #[test]
1233 fn spread_and_computed_key_dropped() {
1234 let src = "import { style } from '@vanilla-extract/css';\n\
1235 const base = {};\n\
1236 const k = 'color';\n\
1237 const x = style({ ...base, [k]: 'red', padding: 8, margin: 4, top: 1 });\n";
1238 let s = sheets(src);
1239 let css = s.structural_partial.expect("partial");
1241 assert!(css.contains("padding:8px;"), "static survives: {css:?}");
1242 }
1243
1244 #[test]
1245 fn cva_variants_map_is_not_serialized_as_css() {
1246 let cva = "import { cva } from 'class-variance-authority';\n\
1249 const button = cva('base', { variants: { size: { sm: 'text-sm' } } });\n";
1250 assert!(
1251 sheets(cva).is_empty(),
1252 "unrelated cva must not fire: {:?}",
1253 sheets(cva)
1254 );
1255
1256 let panda = "import { cva } from '../styled-system/css';\n\
1259 const button = cva({ base: { color: 'red', padding: 8, margin: 4, top: 1 }, variants: { size: { sm: { fontSize: 12 } } } });\n";
1260 let s = sheets(panda);
1261 let css = s.atomic.expect("panda cva base is atomic");
1262 assert!(css.contains("color:red;"), "base serialized: {css:?}");
1263 assert!(
1264 !css.contains("size"),
1265 "variants config not serialized: {css:?}"
1266 );
1267 let a = compute_css_analytics(&css).expect("parses cleanly");
1268 assert!(
1269 a.notable_rules.is_empty(),
1270 "no garbled structural finding: {a:?}"
1271 );
1272 }
1273
1274 #[test]
1275 fn panda_cva_and_class_variance_authority_cva_coexist() {
1276 let src = "import { cva } from '../styled-system/css';\n\
1281 import { cva as cn } from 'class-variance-authority';\n\
1282 const a = cva({ base: { color: 'red' } });\n\
1283 const b = cn('base', { variants: { size: { sm: 'text-sm' } } });\n";
1284 let css = sheets(src).atomic.expect("panda cva base is atomic");
1285 assert!(css.contains("color:red;"), "panda base lifted: {css:?}");
1286 assert!(!css.contains("text-sm"), "cva-lib not serialized: {css:?}");
1287 }
1288
1289 #[test]
1290 fn local_helper_with_recognized_name_does_not_fire() {
1291 let src = "const css = (o) => o;\n\
1294 const x = css({ color: 'red', padding: 8 });\n";
1295 assert!(
1296 sheets(src).is_empty(),
1297 "local css helper must not fire: {:?}",
1298 sheets(src)
1299 );
1300 }
1301
1302 #[test]
1303 fn type_only_import_does_not_open_the_gate() {
1304 let src = "import type { style } from '@vanilla-extract/css';\n\
1305 const x = style({ color: 'red' });\n";
1306 assert!(
1307 sheets(src).is_empty(),
1308 "type-only import must not open provenance: {:?}",
1309 sheets(src)
1310 );
1311 }
1312
1313 #[test]
1314 fn all_dynamic_bucket_emits_no_empty_rule() {
1315 let src = "import { style } from '@vanilla-extract/css';\n\
1316 import { v } from './v';\n\
1317 const x = style({ color: v.a, background: v.b });\n";
1318 let s = sheets(src);
1319 assert!(s.is_empty(), "all-dynamic bucket dropped entirely: {s:?}");
1322 }
1323
1324 #[test]
1325 fn aliased_named_import_still_recognized() {
1326 let src = "import { style as s, globalStyle as gs } from '@vanilla-extract/css';\n\
1328 export const a = s({ color: 'red' });\n\
1329 gs('html', { margin: 0 });\n";
1330 let s = sheets(src);
1331 let css = s.structural.expect("aliased style/globalStyle recognized");
1332 assert!(css.contains("color:red;"), "aliased style fired: {css:?}");
1333 assert!(
1334 css.contains("html{margin:0;}"),
1335 "aliased globalStyle fired: {css:?}"
1336 );
1337 }
1338
1339 #[test]
1340 fn emotion_css_default_import_recognized() {
1341 let src = "import css from '@emotion/css';\n\
1343 const a = css({ color: 'red' });\n";
1344 let css = sheets(src)
1345 .structural
1346 .expect("default css import recognized");
1347 assert!(css.contains("color:red;"), "css={css:?}");
1348 }
1349
1350 #[test]
1351 fn emotion_css_default_import_aliased_recognized() {
1352 let src = "import emo from '@emotion/css';\n\
1355 const a = emo({ color: 'red' });\n";
1356 let css = sheets(src)
1357 .structural
1358 .expect("aliased default css import recognized");
1359 assert!(css.contains("color:red;"), "css={css:?}");
1360 }
1361
1362 #[test]
1363 fn non_decimal_numeric_literals_become_valid_css() {
1364 let src = "import { style } from '@vanilla-extract/css';\n\
1367 const x = style({ padding: 0xFF, zIndex: 1e3 });\n";
1368 let css = sheets(src).structural.expect("structural");
1369 assert!(
1370 css.contains("padding:255px;"),
1371 "hex -> decimal px: css={css:?}"
1372 );
1373 assert!(
1374 css.contains("z-index:1000;"),
1375 "scientific -> decimal: css={css:?}"
1376 );
1377 assert!(compute_css_analytics(&css).is_some(), "valid CSS");
1378 }
1379
1380 #[test]
1381 fn custom_property_numeric_value_keeps_no_unit() {
1382 let src = "import { css } from '@emotion/react';\n\
1386 const g = css({ ':root': { '--space': 8, '--ratio': 1.5 }, padding: 8 });\n";
1387 let sheet = sheets(src)
1389 .structural
1390 .or_else(|| sheets(src).structural_partial)
1391 .expect("structural output");
1392 assert!(
1393 sheet.contains("--space:8;"),
1394 "custom prop keeps no unit: {sheet:?}"
1395 );
1396 assert!(
1397 sheet.contains("--ratio:1.5;"),
1398 "custom prop float unchanged: {sheet:?}"
1399 );
1400 assert!(
1402 sheet.contains("padding:8px;"),
1403 "normal prop still px: {sheet:?}"
1404 );
1405 }
1406
1407 #[test]
1408 fn ms_vendor_prefix_kebabs_with_leading_dash() {
1409 assert_eq!(kebab_case("msFlexAlign"), "-ms-flex-align");
1410 assert_eq!(kebab_case("WebkitBoxShadow"), "-webkit-box-shadow");
1411 assert_eq!(kebab_case("backgroundColor"), "background-color");
1412 assert_eq!(kebab_case("msgType"), "msg-type");
1414 }
1415
1416 #[test]
1417 fn negative_numbers_handled() {
1418 let src = "import { style } from '@vanilla-extract/css';\n\
1419 const x = style({ marginTop: -8, zIndex: -1 });\n";
1420 let css = sheets(src).structural.expect("structural");
1421 assert!(css.contains("margin-top:-8px;"), "css={css:?}");
1422 assert!(
1423 css.contains("z-index:-1;"),
1424 "unitless negative: css={css:?}"
1425 );
1426 }
1427
1428 #[test]
1429 fn none_without_any_object_css_in_js() {
1430 assert!(sheets("const x = 1; function f() {}").is_empty());
1431 assert!(sheets("import React from 'react'; const x = <div/>;").is_empty());
1432 }
1433
1434 #[test]
1435 fn line_numbers_map_back_to_source() {
1436 let src = "import { style } from '@vanilla-extract/css';\n\
1439 \n\
1440 const a = style({\n\
1441 color: 'red',\n\
1442 });\n";
1443 let css = sheets(src).structural.expect("structural");
1444 let pos = css.find("color").expect("color present");
1445 let css_line = 1 + css[..pos].bytes().filter(|&b| b == b'\n').count();
1446 assert_eq!(
1447 css_line, 3,
1448 "bucket maps to the style() object line: css={css:?}"
1449 );
1450 }
1451
1452 #[test]
1453 fn multibyte_content_value_preserved() {
1454 let src = "import { style } from '@vanilla-extract/css';\n\
1455 const x = style({ content: '\"café 日本 €\"', fontFamily: '\"Ñoño\"' });\n";
1456 let css = sheets(src).structural.expect("structural");
1457 assert!(
1458 css.contains("café 日本 €"),
1459 "multibyte preserved: css={css:?}"
1460 );
1461 assert!(
1462 compute_css_analytics(&css).is_some(),
1463 "valid UTF-8 / parses"
1464 );
1465 }
1466
1467 #[test]
1468 fn distinct_colors_fall_out_of_object_styles() {
1469 let src = "import * as stylex from '@stylexjs/stylex';\n\
1470 const s = stylex.create({ a: { color: 'red' }, b: { color: 'blue' }, c: { color: 'red' } });\n";
1471 let css = sheets(src).atomic.expect("atomic");
1472 let a = compute_css_analytics(&css).expect("parses");
1473 assert_eq!(a.colors.len(), 2, "distinct colors counted: {:?}", a.colors);
1474 }
1475
1476 #[test]
1477 fn multi_bucket_padding_uses_key_line() {
1478 let src = "import * as stylex from '@stylexjs/stylex';\n\
1481 const s = stylex.create({\n\
1482 root: { color: 'red' },\n\
1483 card: { color: 'blue' },\n\
1484 });\n";
1485 let css = sheets(src).atomic.expect("atomic");
1486 let red = css.find("color:red").expect("root present");
1487 let blue = css.find("color:blue").expect("card present");
1488 let red_line = 1 + css[..red].bytes().filter(|&b| b == b'\n').count();
1489 let blue_line = 1 + css[..blue].bytes().filter(|&b| b == b'\n').count();
1490 assert_eq!(red_line, 3, "root on its key line: css={css:?}");
1491 assert_eq!(blue_line, 4, "card on its own key line: css={css:?}");
1492 }
1493}