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