1use std::path::Path;
76
77use oxc_allocator::Allocator;
78use oxc_ast::ast::{
79 Argument, Expression, ImportDeclarationSpecifier, NumericLiteral, ObjectExpression,
80 ObjectPropertyKind, Program, PropertyKey, Statement, UnaryOperator,
81};
82use oxc_ast_visit::{Visit, walk};
83use oxc_parser::Parser;
84use oxc_span::{GetSpan, SourceType};
85use rustc_hash::FxHashMap;
86
87use super::shared::{WRAPPER, count_newlines};
88
89const UNITLESS_PROPERTIES: &[&str] = &[
95 "animationIterationCount",
96 "aspectRatio",
97 "borderImageOutset",
98 "borderImageSlice",
99 "borderImageWidth",
100 "boxFlex",
101 "boxFlexGroup",
102 "boxOrdinalGroup",
103 "columnCount",
104 "columns",
105 "flex",
106 "flexGrow",
107 "flexPositive",
108 "flexShrink",
109 "flexNegative",
110 "flexOrder",
111 "gridArea",
112 "gridRow",
113 "gridRowEnd",
114 "gridRowSpan",
115 "gridRowStart",
116 "gridColumn",
117 "gridColumnEnd",
118 "gridColumnSpan",
119 "gridColumnStart",
120 "fontWeight",
121 "lineClamp",
122 "lineHeight",
123 "opacity",
124 "order",
125 "orphans",
126 "scale",
127 "tabSize",
128 "widows",
129 "zIndex",
130 "zoom",
131 "fillOpacity",
132 "floodOpacity",
133 "stopOpacity",
134 "strokeDasharray",
135 "strokeDashoffset",
136 "strokeMiterlimit",
137 "strokeOpacity",
138 "strokeWidth",
139];
140
141#[derive(Clone, Copy, PartialEq, Eq)]
145pub(super) enum Lib {
146 VanillaExtract,
149 Emotion,
151 EmotionStyled,
153 StyleX,
155 Panda,
157}
158
159impl Lib {
160 const fn is_atomic(self) -> bool {
164 matches!(self, Self::StyleX | Self::Panda)
165 }
166}
167
168#[derive(Debug, Default, PartialEq, Eq)]
174pub struct CssInJsObjectSheets {
175 pub structural: Option<String>,
178 pub structural_partial: Option<String>,
181 pub atomic: Option<String>,
184}
185
186impl CssInJsObjectSheets {
187 #[must_use]
189 pub const fn is_empty(&self) -> bool {
190 self.structural.is_none() && self.structural_partial.is_none() && self.atomic.is_none()
191 }
192}
193
194#[derive(Clone, Copy, PartialEq, Eq)]
196enum Stream {
197 Structural,
198 StructuralPartial,
199 Atomic,
200}
201
202struct Bucket {
206 offset: u32,
207 rule: String,
208 stream: Stream,
209}
210
211#[must_use]
217pub fn css_in_js_object_sheets(source: &str, path: &Path) -> CssInJsObjectSheets {
218 let source_type = SourceType::from_path(path).unwrap_or_default();
219 let allocator = Allocator::default();
220 let ret = Parser::new(&allocator, source, source_type).parse();
224
225 let mut collector = ObjectStyleCollector::new(source);
226 collector.build_import_map(&ret.program);
227 if collector.imports.is_empty() {
228 return CssInJsObjectSheets::default();
231 }
232 collector.visit_program(&ret.program);
233 collector.finish()
234}
235
236struct ObjectStyleCollector<'a> {
239 source: &'a str,
240 imports: FxHashMap<&'a str, (Lib, &'a str)>,
246 buckets: Vec<Bucket>,
247}
248
249impl<'a> ObjectStyleCollector<'a> {
250 fn new(source: &'a str) -> Self {
251 Self {
252 source,
253 imports: FxHashMap::default(),
254 buckets: Vec::new(),
255 }
256 }
257
258 fn build_import_map(&mut self, program: &Program<'a>) {
264 for stmt in &program.body {
265 let Statement::ImportDeclaration(decl) = stmt else {
266 continue;
267 };
268 if decl.import_kind.is_type() {
269 continue;
270 }
271 let Some(lib) = module_library(decl.source.value.as_str()) else {
272 continue;
273 };
274 let Some(specifiers) = &decl.specifiers else {
275 continue;
276 };
277 for specifier in specifiers {
278 let (local, role) = match specifier {
279 ImportDeclarationSpecifier::ImportSpecifier(s) => {
282 (s.local.name.as_str(), s.imported.name().as_str())
283 }
284 ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
291 let role = if lib == Lib::Emotion {
292 "css"
293 } else {
294 s.local.name.as_str()
295 };
296 (s.local.name.as_str(), role)
297 }
298 ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
299 (s.local.name.as_str(), s.local.name.as_str())
300 }
301 };
302 self.imports.insert(local, (lib, role));
303 }
304 }
305 }
306
307 fn finish(self) -> CssInJsObjectSheets {
308 let source = self.source;
309 let mut buckets = self.buckets;
310 buckets.sort_by_key(|b| b.offset);
314 CssInJsObjectSheets {
315 structural: render(source, &buckets, Stream::Structural),
316 structural_partial: render(source, &buckets, Stream::StructuralPartial),
317 atomic: render(source, &buckets, Stream::Atomic),
318 }
319 }
320
321 fn recognize(&self, callee: &Expression<'a>) -> Option<(Lib, CallKind)> {
325 match callee {
326 Expression::Identifier(id) => {
327 let (lib, role) = *self.imports.get(id.name.as_str())?;
328 let kind = match (lib, role) {
329 (Lib::VanillaExtract, "style") | (Lib::Emotion | Lib::Panda, "css") => {
331 CallKind::SingleObject
332 }
333 (Lib::VanillaExtract, "styleVariants") => CallKind::ObjectOfObjects,
335 (Lib::VanillaExtract, "globalStyle") => CallKind::GlobalStyle,
337 (Lib::VanillaExtract, "recipe") | (Lib::Panda, "cva") => CallKind::RecipeBase,
339 _ => return None,
340 };
341 Some((lib, kind))
342 }
343 Expression::StaticMemberExpression(member) => {
346 let Expression::Identifier(obj) = &member.object else {
347 return None;
348 };
349 let (lib, _) = *self.imports.get(obj.name.as_str())?;
350 let kind = match (lib, member.property.name.as_str()) {
351 (Lib::EmotionStyled, _) => CallKind::SingleObject,
352 (Lib::StyleX, "create") => CallKind::ObjectOfObjects,
353 _ => return None,
354 };
355 Some((lib, kind))
356 }
357 Expression::CallExpression(inner) => {
359 let Expression::Identifier(id) = &inner.callee else {
360 return None;
361 };
362 matches!(
363 self.imports.get(id.name.as_str()),
364 Some((Lib::EmotionStyled, _))
365 )
366 .then_some((Lib::EmotionStyled, CallKind::SingleObject))
367 }
368 _ => None,
369 }
370 }
371
372 fn collect_call(&mut self, callee: &Expression<'a>, args: &[Argument<'a>]) {
374 let Some((lib, kind)) = self.recognize(callee) else {
375 return;
376 };
377 let atomic = lib.is_atomic();
378 match kind {
379 CallKind::SingleObject => {
380 if let Some(obj) = object_arg(args, 0) {
381 self.push_bucket(obj, WRAPPER, atomic, obj.span().start);
382 }
383 }
384 CallKind::ObjectOfObjects => {
385 if args.len() != 1 {
390 return;
391 }
392 let Some(obj) = object_arg(args, 0) else {
393 return;
394 };
395 for prop in &obj.properties {
396 if let ObjectPropertyKind::ObjectProperty(p) = prop
397 && let Expression::ObjectExpression(inner) = &p.value
398 {
399 self.push_bucket(inner, WRAPPER, atomic, p.key.span().start);
400 }
401 }
402 }
403 CallKind::RecipeBase => {
404 let Some(obj) = object_arg(args, 0) else {
409 return;
410 };
411 for prop in &obj.properties {
412 if let ObjectPropertyKind::ObjectProperty(p) = prop
413 && static_key(&p.key).as_deref() == Some("base")
414 && let Expression::ObjectExpression(inner) = &p.value
415 {
416 self.push_bucket(inner, WRAPPER, atomic, p.key.span().start);
417 }
418 }
419 }
420 CallKind::GlobalStyle => {
421 let (Some(selector), Some(obj)) = (string_arg(args, 0), object_arg(args, 1)) else {
423 return;
424 };
425 let selector = sanitize_selector(&selector);
426 if !selector.is_empty() {
427 self.push_bucket(obj, &selector, atomic, obj.span().start);
428 }
429 }
430 }
431 }
432
433 fn push_bucket(
438 &mut self,
439 obj: &ObjectExpression<'a>,
440 selector: &str,
441 atomic: bool,
442 offset: u32,
443 ) {
444 let mut body = String::new();
445 let mut dropped = false;
446 serialize_object_body(obj, &mut body, &mut dropped);
447 if body.is_empty() {
448 return;
449 }
450 let stream = if atomic {
451 Stream::Atomic
452 } else if dropped {
453 Stream::StructuralPartial
454 } else {
455 Stream::Structural
456 };
457 self.buckets.push(Bucket {
458 offset,
459 rule: format!("{selector}{{{body}}}"),
460 stream,
461 });
462 }
463}
464
465impl<'a> Visit<'a> for ObjectStyleCollector<'a> {
466 fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
467 self.collect_call(&call.callee, &call.arguments);
468 walk::walk_call_expression(self, call);
469 }
470}
471
472fn render(source: &str, buckets: &[Bucket], stream: Stream) -> Option<String> {
476 let mut out = String::new();
477 let mut current_line: usize = 1;
478 let mut found = false;
479 for bucket in buckets.iter().filter(|b| b.stream == stream) {
480 let block_line = 1 + count_newlines(&source[..bucket.offset as usize]);
481 while current_line < block_line {
482 out.push('\n');
483 current_line += 1;
484 }
485 out.push_str(&bucket.rule);
486 current_line += count_newlines(&bucket.rule);
487 found = true;
488 }
489 found.then_some(out)
490}
491
492enum CallKind {
494 SingleObject,
497 ObjectOfObjects,
500 RecipeBase,
503 GlobalStyle,
506}
507
508pub(super) fn module_library(specifier: &str) -> Option<Lib> {
514 match specifier {
515 "@pandacss/dev" => Some(Lib::Panda),
516 "@vanilla-extract/css" | "@vanilla-extract/recipes" => Some(Lib::VanillaExtract),
517 "@emotion/react" | "@emotion/css" => Some(Lib::Emotion),
518 "@emotion/styled" => Some(Lib::EmotionStyled),
519 "@stylexjs/stylex" => Some(Lib::StyleX),
520 _ if specifier
521 .split(['/', '\\'])
522 .any(|segment| segment == "styled-system") =>
523 {
524 Some(Lib::Panda)
525 }
526 _ => None,
527 }
528}
529
530fn object_arg<'a, 'b>(args: &'b [Argument<'a>], index: usize) -> Option<&'b ObjectExpression<'a>> {
532 match args.get(index) {
533 Some(Argument::ObjectExpression(obj)) => Some(obj),
534 _ => None,
535 }
536}
537
538fn string_arg(args: &[Argument<'_>], index: usize) -> Option<String> {
540 match args.get(index) {
541 Some(Argument::StringLiteral(lit)) => Some(lit.value.to_string()),
542 _ => None,
543 }
544}
545
546fn serialize_object_body(obj: &ObjectExpression<'_>, out: &mut String, dropped: &mut bool) {
554 for prop in &obj.properties {
555 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
556 *dropped = true;
558 continue;
559 };
560 let Some(key) = static_key(&prop.key) else {
561 *dropped = true;
563 continue;
564 };
565 match &prop.value {
566 Expression::ObjectExpression(nested) if is_selector_key(&key) => {
567 serialize_nested(&key, nested, out, dropped);
568 }
569 Expression::ObjectExpression(_) => {
570 *dropped = true;
573 }
574 value => {
575 if let Some(rendered) = serialize_value(&key, value) {
576 out.push_str(&rendered);
577 } else {
578 *dropped = true;
579 }
580 }
581 }
582 }
583}
584
585fn serialize_nested(
589 key: &str,
590 nested: &ObjectExpression<'_>,
591 out: &mut String,
592 dropped: &mut bool,
593) {
594 if key == "selectors" {
597 for prop in &nested.properties {
598 match prop {
599 ObjectPropertyKind::ObjectProperty(p) => {
600 if let (Some(inner_key), Expression::ObjectExpression(inner)) =
601 (static_key(&p.key), &p.value)
602 {
603 serialize_nested(&inner_key, inner, out, dropped);
604 } else {
605 *dropped = true;
606 }
607 }
608 ObjectPropertyKind::SpreadProperty(_) => *dropped = true,
609 }
610 }
611 return;
612 }
613
614 let mut body = String::new();
615 serialize_object_body(nested, &mut body, dropped);
616 if body.is_empty() {
617 return;
618 }
619 out.push_str(&nested_selector(key));
620 out.push('{');
621 out.push_str(&body);
622 out.push('}');
623}
624
625fn is_selector_key(key: &str) -> bool {
632 if key == "selectors" {
633 return true;
634 }
635 matches!(
636 key.trim_start().chars().next(),
637 Some(':' | '&' | '@' | '>' | '+' | '~' | '.' | '#' | '[' | '*')
638 ) || key.starts_with(' ')
639}
640
641fn nested_selector(key: &str) -> String {
645 let trimmed = key.trim();
646 if trimmed.starts_with('@') || trimmed.starts_with('&') {
647 return trimmed.to_string();
648 }
649 format!("&{trimmed}")
650}
651
652fn serialize_value(key: &str, value: &Expression<'_>) -> Option<String> {
655 let rendered = static_value(key, value)?;
656 Some(format!("{}:{rendered};", kebab_case(key)))
657}
658
659fn static_value(key: &str, value: &Expression<'_>) -> Option<String> {
663 match value {
664 Expression::StringLiteral(lit) => {
665 let text = lit.value.as_str().trim();
666 (!text.is_empty()).then(|| text.to_string())
667 }
668 Expression::NumericLiteral(num) => Some(render_number(key, num)),
669 Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
670 if let Expression::NumericLiteral(num) = &unary.argument {
671 Some(format!("-{}", render_number(key, num)))
672 } else {
673 None
674 }
675 }
676 _ => None,
677 }
678}
679
680fn render_number(key: &str, num: &NumericLiteral<'_>) -> String {
690 let value = format_f64(num.value);
691 if is_unitless(key) || key.starts_with("--") || num.value == 0.0 {
692 value
693 } else {
694 format!("{value}px")
695 }
696}
697
698fn format_f64(value: f64) -> String {
699 if value.fract() == 0.0 {
700 format!("{value:.0}")
701 } else {
702 value.to_string()
703 }
704}
705
706fn is_unitless(key: &str) -> bool {
708 UNITLESS_PROPERTIES.contains(&key)
709}
710
711fn static_key(key: &PropertyKey<'_>) -> Option<String> {
714 key.static_name().map(|name| name.to_string())
715}
716
717fn kebab_case(name: &str) -> String {
723 if name.starts_with("--") || name.contains('-') {
724 return name.to_string();
725 }
726 let mut out = String::with_capacity(name.len() + 2);
727 if let Some(rest) = name.strip_prefix("ms")
731 && rest.chars().next().is_some_and(|c| c.is_ascii_uppercase())
732 {
733 out.push('-');
734 }
735 for ch in name.chars() {
736 if ch.is_ascii_uppercase() {
737 out.push('-');
738 out.push(ch.to_ascii_lowercase());
739 } else {
740 out.push(ch);
741 }
742 }
743 out
744}
745
746fn sanitize_selector(selector: &str) -> String {
751 selector
752 .chars()
753 .filter(|&c| c != '{' && c != '}' && c != ';')
754 .collect::<String>()
755 .trim()
756 .to_string()
757}
758
759#[cfg(all(test, not(miri)))]
760mod tests {
761 use super::*;
762 use crate::compute_css_analytics;
763
764 fn sheets(source: &str) -> CssInJsObjectSheets {
765 css_in_js_object_sheets(source, Path::new("styles.ts"))
766 }
767
768 #[test]
769 fn vanilla_extract_style_lifts_to_parseable_css() {
770 let src = "import { style } from '@vanilla-extract/css';\n\
771 export const box = style({\n\
772 backgroundColor: 'red',\n\
773 padding: 8,\n\
774 });\n";
775 let s = sheets(src);
776 let css = s.structural.expect("vanilla-extract style is structural");
777 assert!(css.contains("background-color:red;"), "css={css:?}");
779 assert!(css.contains("padding:8px;"), "px default: css={css:?}");
780 let a = compute_css_analytics(&css).expect("lifted CSS parses");
781 assert!(a.total_declarations >= 2, "declarations counted: {a:?}");
782 assert!(s.atomic.is_none(), "vanilla-extract is not atomic");
783 }
784
785 #[test]
786 fn unitless_properties_keep_bare_number() {
787 let src = "import { style } from '@vanilla-extract/css';\n\
788 const x = style({ lineHeight: 1.5, zIndex: 10, fontWeight: 700, padding: 4 });\n";
789 let css = sheets(src).structural.expect("structural");
790 assert!(css.contains("line-height:1.5;"), "css={css:?}");
791 assert!(css.contains("z-index:10;"), "css={css:?}");
792 assert!(css.contains("font-weight:700;"), "css={css:?}");
793 assert!(css.contains("padding:4px;"), "css={css:?}");
794 }
795
796 #[test]
797 fn one_level_nesting_via_relative_selector() {
798 let src = "import { style } from '@vanilla-extract/css';\n\
799 const x = style({ color: 'red', ':hover': { color: 'blue' } });\n";
800 let css = sheets(src).structural.expect("structural");
801 assert!(
802 css.contains("&:hover{color:blue;}"),
803 "nested rule: css={css:?}"
804 );
805 let a = compute_css_analytics(&css).expect("nested parses");
806 assert!(a.rule_count >= 2, "nested rule counted: {a:?}");
807 }
808
809 #[test]
810 fn vanilla_extract_selectors_wrapper_unwrapped() {
811 let src = "import { style } from '@vanilla-extract/css';\n\
812 const x = style({ color: 'red', selectors: { '&:hover': { color: 'blue' } } });\n";
813 let css = sheets(src).structural.expect("structural");
814 assert!(
815 css.contains("&:hover{color:blue;}"),
816 "selectors wrapper unwrapped: css={css:?}"
817 );
818 assert!(
820 !css.contains("selectors{"),
821 "no literal selectors rule: css={css:?}"
822 );
823 }
824
825 #[test]
826 fn global_style_keeps_real_selector() {
827 let src = "import { globalStyle } from '@vanilla-extract/css';\n\
828 globalStyle('html, body', { margin: 0 });\n";
829 let css = sheets(src).structural.expect("structural");
830 assert!(
831 css.contains("html, body{margin:0;}"),
832 "real selector: css={css:?}"
833 );
834 let a = compute_css_analytics(&css).expect("parses");
835 assert_eq!(a.rule_count, 1);
836 }
837
838 #[test]
839 fn stylex_create_is_atomic_one_bucket_per_key() {
840 let src = "import * as stylex from '@stylexjs/stylex';\n\
841 export const styles = stylex.create({\n\
842 root: { color: 'red', padding: 16 },\n\
843 card: { color: 'blue' },\n\
844 });\n";
845 let s = sheets(src);
846 assert!(s.structural.is_none(), "stylex is atomic, not structural");
847 let css = s.atomic.expect("stylex.create is atomic");
848 assert!(css.contains("color:red;"), "css={css:?}");
849 assert!(css.contains("padding:16px;"), "css={css:?}");
850 assert!(css.contains("color:blue;"), "second bucket: css={css:?}");
851 let a = compute_css_analytics(&css).expect("parses");
852 assert!(a.rule_count >= 2, "two buckets: {a:?}");
853 }
854
855 #[test]
856 fn panda_css_from_styled_system_is_atomic() {
857 let src = "import { css } from '../styled-system/css';\n\
858 const c = css({ display: 'flex', gap: 8 });\n";
859 let s = sheets(src);
860 let css = s.atomic.expect("panda css is atomic");
861 assert!(css.contains("display:flex;"), "css={css:?}");
862 assert!(css.contains("gap:8px;"), "css={css:?}");
863 }
864
865 #[test]
866 fn emotion_css_and_styled_are_structural() {
867 let src = "import { css } from '@emotion/react';\n\
868 import styled from '@emotion/styled';\n\
869 const a = css({ color: 'red' });\n\
870 const B = styled.div({ fontWeight: 700 });\n";
871 let css = sheets(src).structural.expect("emotion is structural");
872 assert!(css.contains("color:red;"), "css={css:?}");
873 assert!(css.contains("font-weight:700;"), "styled.div: css={css:?}");
874 }
875
876 #[test]
877 fn styled_call_form_is_lifted() {
878 let src = "import styled from '@emotion/styled';\n\
879 const Primary = styled(Button)({ fontWeight: 700 });\n";
880 let css = sheets(src)
881 .structural
882 .expect("styled(Component)({}) lifted");
883 assert!(css.contains("font-weight:700;"), "css={css:?}");
884 }
885
886 #[test]
887 fn dynamic_value_is_dropped_to_structural_partial() {
888 let src = "import { style } from '@vanilla-extract/css';\n\
889 import { theme } from './theme';\n\
890 const x = style({ color: theme.primary, padding: 8, margin: 4, top: 1, left: 2 });\n";
891 let s = sheets(src);
892 assert!(s.structural.is_none(), "bucket had a drop: {s:?}");
896 let css = s.structural_partial.expect("partial");
897 assert!(
898 !css.contains("fallowinterp"),
899 "no placeholder, value dropped: {css:?}"
900 );
901 assert!(
902 !css.contains("primary"),
903 "dynamic member not serialized: {css:?}"
904 );
905 assert!(css.contains("padding:8px;"), "static survives: {css:?}");
906 let a = compute_css_analytics(&css).expect("must parse, not None");
907 assert_eq!(a.important_declarations, 0, "no invented !important: {a:?}");
908 }
909
910 #[test]
911 fn spread_and_computed_key_dropped() {
912 let src = "import { style } from '@vanilla-extract/css';\n\
913 const base = {};\n\
914 const k = 'color';\n\
915 const x = style({ ...base, [k]: 'red', padding: 8, margin: 4, top: 1 });\n";
916 let s = sheets(src);
917 let css = s.structural_partial.expect("partial");
919 assert!(css.contains("padding:8px;"), "static survives: {css:?}");
920 }
921
922 #[test]
923 fn cva_variants_map_is_not_serialized_as_css() {
924 let cva = "import { cva } from 'class-variance-authority';\n\
927 const button = cva('base', { variants: { size: { sm: 'text-sm' } } });\n";
928 assert!(
929 sheets(cva).is_empty(),
930 "unrelated cva must not fire: {:?}",
931 sheets(cva)
932 );
933
934 let panda = "import { cva } from '../styled-system/css';\n\
937 const button = cva({ base: { color: 'red', padding: 8, margin: 4, top: 1 }, variants: { size: { sm: { fontSize: 12 } } } });\n";
938 let s = sheets(panda);
939 let css = s.atomic.expect("panda cva base is atomic");
940 assert!(css.contains("color:red;"), "base serialized: {css:?}");
941 assert!(
942 !css.contains("size"),
943 "variants config not serialized: {css:?}"
944 );
945 let a = compute_css_analytics(&css).expect("parses cleanly");
946 assert!(
947 a.notable_rules.is_empty(),
948 "no garbled structural finding: {a:?}"
949 );
950 }
951
952 #[test]
953 fn panda_cva_and_class_variance_authority_cva_coexist() {
954 let src = "import { cva } from '../styled-system/css';\n\
959 import { cva as cn } from 'class-variance-authority';\n\
960 const a = cva({ base: { color: 'red' } });\n\
961 const b = cn('base', { variants: { size: { sm: 'text-sm' } } });\n";
962 let css = sheets(src).atomic.expect("panda cva base is atomic");
963 assert!(css.contains("color:red;"), "panda base lifted: {css:?}");
964 assert!(!css.contains("text-sm"), "cva-lib not serialized: {css:?}");
965 }
966
967 #[test]
968 fn local_helper_with_recognized_name_does_not_fire() {
969 let src = "const css = (o) => o;\n\
972 const x = css({ color: 'red', padding: 8 });\n";
973 assert!(
974 sheets(src).is_empty(),
975 "local css helper must not fire: {:?}",
976 sheets(src)
977 );
978 }
979
980 #[test]
981 fn type_only_import_does_not_open_the_gate() {
982 let src = "import type { style } from '@vanilla-extract/css';\n\
983 const x = style({ color: 'red' });\n";
984 assert!(
985 sheets(src).is_empty(),
986 "type-only import must not open provenance: {:?}",
987 sheets(src)
988 );
989 }
990
991 #[test]
992 fn all_dynamic_bucket_emits_no_empty_rule() {
993 let src = "import { style } from '@vanilla-extract/css';\n\
994 import { v } from './v';\n\
995 const x = style({ color: v.a, background: v.b });\n";
996 let s = sheets(src);
997 assert!(s.is_empty(), "all-dynamic bucket dropped entirely: {s:?}");
1000 }
1001
1002 #[test]
1003 fn aliased_named_import_still_recognized() {
1004 let src = "import { style as s, globalStyle as gs } from '@vanilla-extract/css';\n\
1006 export const a = s({ color: 'red' });\n\
1007 gs('html', { margin: 0 });\n";
1008 let s = sheets(src);
1009 let css = s.structural.expect("aliased style/globalStyle recognized");
1010 assert!(css.contains("color:red;"), "aliased style fired: {css:?}");
1011 assert!(
1012 css.contains("html{margin:0;}"),
1013 "aliased globalStyle fired: {css:?}"
1014 );
1015 }
1016
1017 #[test]
1018 fn emotion_css_default_import_recognized() {
1019 let src = "import css from '@emotion/css';\n\
1021 const a = css({ color: 'red' });\n";
1022 let css = sheets(src)
1023 .structural
1024 .expect("default css import recognized");
1025 assert!(css.contains("color:red;"), "css={css:?}");
1026 }
1027
1028 #[test]
1029 fn emotion_css_default_import_aliased_recognized() {
1030 let src = "import emo from '@emotion/css';\n\
1033 const a = emo({ color: 'red' });\n";
1034 let css = sheets(src)
1035 .structural
1036 .expect("aliased default css import recognized");
1037 assert!(css.contains("color:red;"), "css={css:?}");
1038 }
1039
1040 #[test]
1041 fn non_decimal_numeric_literals_become_valid_css() {
1042 let src = "import { style } from '@vanilla-extract/css';\n\
1045 const x = style({ padding: 0xFF, zIndex: 1e3 });\n";
1046 let css = sheets(src).structural.expect("structural");
1047 assert!(
1048 css.contains("padding:255px;"),
1049 "hex -> decimal px: css={css:?}"
1050 );
1051 assert!(
1052 css.contains("z-index:1000;"),
1053 "scientific -> decimal: css={css:?}"
1054 );
1055 assert!(compute_css_analytics(&css).is_some(), "valid CSS");
1056 }
1057
1058 #[test]
1059 fn custom_property_numeric_value_keeps_no_unit() {
1060 let src = "import { css } from '@emotion/react';\n\
1064 const g = css({ ':root': { '--space': 8, '--ratio': 1.5 }, padding: 8 });\n";
1065 let sheet = sheets(src)
1067 .structural
1068 .or_else(|| sheets(src).structural_partial)
1069 .expect("structural output");
1070 assert!(
1071 sheet.contains("--space:8;"),
1072 "custom prop keeps no unit: {sheet:?}"
1073 );
1074 assert!(
1075 sheet.contains("--ratio:1.5;"),
1076 "custom prop float unchanged: {sheet:?}"
1077 );
1078 assert!(
1080 sheet.contains("padding:8px;"),
1081 "normal prop still px: {sheet:?}"
1082 );
1083 }
1084
1085 #[test]
1086 fn ms_vendor_prefix_kebabs_with_leading_dash() {
1087 assert_eq!(kebab_case("msFlexAlign"), "-ms-flex-align");
1088 assert_eq!(kebab_case("WebkitBoxShadow"), "-webkit-box-shadow");
1089 assert_eq!(kebab_case("backgroundColor"), "background-color");
1090 assert_eq!(kebab_case("msgType"), "msg-type");
1092 }
1093
1094 #[test]
1095 fn negative_numbers_handled() {
1096 let src = "import { style } from '@vanilla-extract/css';\n\
1097 const x = style({ marginTop: -8, zIndex: -1 });\n";
1098 let css = sheets(src).structural.expect("structural");
1099 assert!(css.contains("margin-top:-8px;"), "css={css:?}");
1100 assert!(
1101 css.contains("z-index:-1;"),
1102 "unitless negative: css={css:?}"
1103 );
1104 }
1105
1106 #[test]
1107 fn none_without_any_object_css_in_js() {
1108 assert!(sheets("const x = 1; function f() {}").is_empty());
1109 assert!(sheets("import React from 'react'; const x = <div/>;").is_empty());
1110 }
1111
1112 #[test]
1113 fn line_numbers_map_back_to_source() {
1114 let src = "import { style } from '@vanilla-extract/css';\n\
1117 \n\
1118 const a = style({\n\
1119 color: 'red',\n\
1120 });\n";
1121 let css = sheets(src).structural.expect("structural");
1122 let pos = css.find("color").expect("color present");
1123 let css_line = 1 + css[..pos].bytes().filter(|&b| b == b'\n').count();
1124 assert_eq!(
1125 css_line, 3,
1126 "bucket maps to the style() object line: css={css:?}"
1127 );
1128 }
1129
1130 #[test]
1131 fn multibyte_content_value_preserved() {
1132 let src = "import { style } from '@vanilla-extract/css';\n\
1133 const x = style({ content: '\"café 日本 €\"', fontFamily: '\"Ñoño\"' });\n";
1134 let css = sheets(src).structural.expect("structural");
1135 assert!(
1136 css.contains("café 日本 €"),
1137 "multibyte preserved: css={css:?}"
1138 );
1139 assert!(
1140 compute_css_analytics(&css).is_some(),
1141 "valid UTF-8 / parses"
1142 );
1143 }
1144
1145 #[test]
1146 fn distinct_colors_fall_out_of_object_styles() {
1147 let src = "import * as stylex from '@stylexjs/stylex';\n\
1148 const s = stylex.create({ a: { color: 'red' }, b: { color: 'blue' }, c: { color: 'red' } });\n";
1149 let css = sheets(src).atomic.expect("atomic");
1150 let a = compute_css_analytics(&css).expect("parses");
1151 assert_eq!(a.colors.len(), 2, "distinct colors counted: {:?}", a.colors);
1152 }
1153
1154 #[test]
1155 fn multi_bucket_padding_uses_key_line() {
1156 let src = "import * as stylex from '@stylexjs/stylex';\n\
1159 const s = stylex.create({\n\
1160 root: { color: 'red' },\n\
1161 card: { color: 'blue' },\n\
1162 });\n";
1163 let css = sheets(src).atomic.expect("atomic");
1164 let red = css.find("color:red").expect("root present");
1165 let blue = css.find("color:blue").expect("card present");
1166 let red_line = 1 + css[..red].bytes().filter(|&b| b == b'\n').count();
1167 let blue_line = 1 + css[..blue].bytes().filter(|&b| b == b'\n').count();
1168 assert_eq!(red_line, 3, "root on its key line: css={css:?}");
1169 assert_eq!(blue_line, 4, "card on its own key line: css={css:?}");
1170 }
1171}