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 "@vanilla-extract/css" | "@vanilla-extract/recipes" => Some(Lib::VanillaExtract),
516 "@emotion/react" | "@emotion/css" => Some(Lib::Emotion),
517 "@emotion/styled" => Some(Lib::EmotionStyled),
518 "@stylexjs/stylex" => Some(Lib::StyleX),
519 _ if specifier
520 .split(['/', '\\'])
521 .any(|segment| segment == "styled-system") =>
522 {
523 Some(Lib::Panda)
524 }
525 _ => None,
526 }
527}
528
529fn object_arg<'a, 'b>(args: &'b [Argument<'a>], index: usize) -> Option<&'b ObjectExpression<'a>> {
531 match args.get(index) {
532 Some(Argument::ObjectExpression(obj)) => Some(obj),
533 _ => None,
534 }
535}
536
537fn string_arg(args: &[Argument<'_>], index: usize) -> Option<String> {
539 match args.get(index) {
540 Some(Argument::StringLiteral(lit)) => Some(lit.value.to_string()),
541 _ => None,
542 }
543}
544
545fn serialize_object_body(obj: &ObjectExpression<'_>, out: &mut String, dropped: &mut bool) {
553 for prop in &obj.properties {
554 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
555 *dropped = true;
557 continue;
558 };
559 let Some(key) = static_key(&prop.key) else {
560 *dropped = true;
562 continue;
563 };
564 match &prop.value {
565 Expression::ObjectExpression(nested) if is_selector_key(&key) => {
566 serialize_nested(&key, nested, out, dropped);
567 }
568 Expression::ObjectExpression(_) => {
569 *dropped = true;
572 }
573 value => {
574 if let Some(rendered) = serialize_value(&key, value) {
575 out.push_str(&rendered);
576 } else {
577 *dropped = true;
578 }
579 }
580 }
581 }
582}
583
584fn serialize_nested(
588 key: &str,
589 nested: &ObjectExpression<'_>,
590 out: &mut String,
591 dropped: &mut bool,
592) {
593 if key == "selectors" {
596 for prop in &nested.properties {
597 match prop {
598 ObjectPropertyKind::ObjectProperty(p) => {
599 if let (Some(inner_key), Expression::ObjectExpression(inner)) =
600 (static_key(&p.key), &p.value)
601 {
602 serialize_nested(&inner_key, inner, out, dropped);
603 } else {
604 *dropped = true;
605 }
606 }
607 ObjectPropertyKind::SpreadProperty(_) => *dropped = true,
608 }
609 }
610 return;
611 }
612
613 let mut body = String::new();
614 serialize_object_body(nested, &mut body, dropped);
615 if body.is_empty() {
616 return;
617 }
618 out.push_str(&nested_selector(key));
619 out.push('{');
620 out.push_str(&body);
621 out.push('}');
622}
623
624fn is_selector_key(key: &str) -> bool {
631 if key == "selectors" {
632 return true;
633 }
634 matches!(
635 key.trim_start().chars().next(),
636 Some(':' | '&' | '@' | '>' | '+' | '~' | '.' | '#' | '[' | '*')
637 ) || key.starts_with(' ')
638}
639
640fn nested_selector(key: &str) -> String {
644 let trimmed = key.trim();
645 if trimmed.starts_with('@') || trimmed.starts_with('&') {
646 return trimmed.to_string();
647 }
648 format!("&{trimmed}")
649}
650
651fn serialize_value(key: &str, value: &Expression<'_>) -> Option<String> {
654 let rendered = static_value(key, value)?;
655 Some(format!("{}:{rendered};", kebab_case(key)))
656}
657
658fn static_value(key: &str, value: &Expression<'_>) -> Option<String> {
662 match value {
663 Expression::StringLiteral(lit) => {
664 let text = lit.value.as_str().trim();
665 (!text.is_empty()).then(|| text.to_string())
666 }
667 Expression::NumericLiteral(num) => Some(render_number(key, num)),
668 Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
669 if let Expression::NumericLiteral(num) = &unary.argument {
670 Some(format!("-{}", render_number(key, num)))
671 } else {
672 None
673 }
674 }
675 _ => None,
676 }
677}
678
679fn render_number(key: &str, num: &NumericLiteral<'_>) -> String {
689 let value = format_f64(num.value);
690 if is_unitless(key) || key.starts_with("--") || num.value == 0.0 {
691 value
692 } else {
693 format!("{value}px")
694 }
695}
696
697fn format_f64(value: f64) -> String {
698 if value.fract() == 0.0 {
699 format!("{value:.0}")
700 } else {
701 value.to_string()
702 }
703}
704
705fn is_unitless(key: &str) -> bool {
707 UNITLESS_PROPERTIES.contains(&key)
708}
709
710fn static_key(key: &PropertyKey<'_>) -> Option<String> {
713 key.static_name().map(|name| name.to_string())
714}
715
716fn kebab_case(name: &str) -> String {
722 if name.starts_with("--") || name.contains('-') {
723 return name.to_string();
724 }
725 let mut out = String::with_capacity(name.len() + 2);
726 if let Some(rest) = name.strip_prefix("ms")
730 && rest.chars().next().is_some_and(|c| c.is_ascii_uppercase())
731 {
732 out.push('-');
733 }
734 for ch in name.chars() {
735 if ch.is_ascii_uppercase() {
736 out.push('-');
737 out.push(ch.to_ascii_lowercase());
738 } else {
739 out.push(ch);
740 }
741 }
742 out
743}
744
745fn sanitize_selector(selector: &str) -> String {
750 selector
751 .chars()
752 .filter(|&c| c != '{' && c != '}' && c != ';')
753 .collect::<String>()
754 .trim()
755 .to_string()
756}
757
758#[cfg(all(test, not(miri)))]
759mod tests {
760 use super::*;
761 use crate::compute_css_analytics;
762
763 fn sheets(source: &str) -> CssInJsObjectSheets {
764 css_in_js_object_sheets(source, Path::new("styles.ts"))
765 }
766
767 #[test]
768 fn vanilla_extract_style_lifts_to_parseable_css() {
769 let src = "import { style } from '@vanilla-extract/css';\n\
770 export const box = style({\n\
771 backgroundColor: 'red',\n\
772 padding: 8,\n\
773 });\n";
774 let s = sheets(src);
775 let css = s.structural.expect("vanilla-extract style is structural");
776 assert!(css.contains("background-color:red;"), "css={css:?}");
778 assert!(css.contains("padding:8px;"), "px default: css={css:?}");
779 let a = compute_css_analytics(&css).expect("lifted CSS parses");
780 assert!(a.total_declarations >= 2, "declarations counted: {a:?}");
781 assert!(s.atomic.is_none(), "vanilla-extract is not atomic");
782 }
783
784 #[test]
785 fn unitless_properties_keep_bare_number() {
786 let src = "import { style } from '@vanilla-extract/css';\n\
787 const x = style({ lineHeight: 1.5, zIndex: 10, fontWeight: 700, padding: 4 });\n";
788 let css = sheets(src).structural.expect("structural");
789 assert!(css.contains("line-height:1.5;"), "css={css:?}");
790 assert!(css.contains("z-index:10;"), "css={css:?}");
791 assert!(css.contains("font-weight:700;"), "css={css:?}");
792 assert!(css.contains("padding:4px;"), "css={css:?}");
793 }
794
795 #[test]
796 fn one_level_nesting_via_relative_selector() {
797 let src = "import { style } from '@vanilla-extract/css';\n\
798 const x = style({ color: 'red', ':hover': { color: 'blue' } });\n";
799 let css = sheets(src).structural.expect("structural");
800 assert!(
801 css.contains("&:hover{color:blue;}"),
802 "nested rule: css={css:?}"
803 );
804 let a = compute_css_analytics(&css).expect("nested parses");
805 assert!(a.rule_count >= 2, "nested rule counted: {a:?}");
806 }
807
808 #[test]
809 fn vanilla_extract_selectors_wrapper_unwrapped() {
810 let src = "import { style } from '@vanilla-extract/css';\n\
811 const x = style({ color: 'red', selectors: { '&:hover': { color: 'blue' } } });\n";
812 let css = sheets(src).structural.expect("structural");
813 assert!(
814 css.contains("&:hover{color:blue;}"),
815 "selectors wrapper unwrapped: css={css:?}"
816 );
817 assert!(
819 !css.contains("selectors{"),
820 "no literal selectors rule: css={css:?}"
821 );
822 }
823
824 #[test]
825 fn global_style_keeps_real_selector() {
826 let src = "import { globalStyle } from '@vanilla-extract/css';\n\
827 globalStyle('html, body', { margin: 0 });\n";
828 let css = sheets(src).structural.expect("structural");
829 assert!(
830 css.contains("html, body{margin:0;}"),
831 "real selector: css={css:?}"
832 );
833 let a = compute_css_analytics(&css).expect("parses");
834 assert_eq!(a.rule_count, 1);
835 }
836
837 #[test]
838 fn stylex_create_is_atomic_one_bucket_per_key() {
839 let src = "import * as stylex from '@stylexjs/stylex';\n\
840 export const styles = stylex.create({\n\
841 root: { color: 'red', padding: 16 },\n\
842 card: { color: 'blue' },\n\
843 });\n";
844 let s = sheets(src);
845 assert!(s.structural.is_none(), "stylex is atomic, not structural");
846 let css = s.atomic.expect("stylex.create is atomic");
847 assert!(css.contains("color:red;"), "css={css:?}");
848 assert!(css.contains("padding:16px;"), "css={css:?}");
849 assert!(css.contains("color:blue;"), "second bucket: css={css:?}");
850 let a = compute_css_analytics(&css).expect("parses");
851 assert!(a.rule_count >= 2, "two buckets: {a:?}");
852 }
853
854 #[test]
855 fn panda_css_from_styled_system_is_atomic() {
856 let src = "import { css } from '../styled-system/css';\n\
857 const c = css({ display: 'flex', gap: 8 });\n";
858 let s = sheets(src);
859 let css = s.atomic.expect("panda css is atomic");
860 assert!(css.contains("display:flex;"), "css={css:?}");
861 assert!(css.contains("gap:8px;"), "css={css:?}");
862 }
863
864 #[test]
865 fn emotion_css_and_styled_are_structural() {
866 let src = "import { css } from '@emotion/react';\n\
867 import styled from '@emotion/styled';\n\
868 const a = css({ color: 'red' });\n\
869 const B = styled.div({ fontWeight: 700 });\n";
870 let css = sheets(src).structural.expect("emotion is structural");
871 assert!(css.contains("color:red;"), "css={css:?}");
872 assert!(css.contains("font-weight:700;"), "styled.div: css={css:?}");
873 }
874
875 #[test]
876 fn styled_call_form_is_lifted() {
877 let src = "import styled from '@emotion/styled';\n\
878 const Primary = styled(Button)({ fontWeight: 700 });\n";
879 let css = sheets(src)
880 .structural
881 .expect("styled(Component)({}) lifted");
882 assert!(css.contains("font-weight:700;"), "css={css:?}");
883 }
884
885 #[test]
886 fn dynamic_value_is_dropped_to_structural_partial() {
887 let src = "import { style } from '@vanilla-extract/css';\n\
888 import { theme } from './theme';\n\
889 const x = style({ color: theme.primary, padding: 8, margin: 4, top: 1, left: 2 });\n";
890 let s = sheets(src);
891 assert!(s.structural.is_none(), "bucket had a drop: {s:?}");
895 let css = s.structural_partial.expect("partial");
896 assert!(
897 !css.contains("fallowinterp"),
898 "no placeholder, value dropped: {css:?}"
899 );
900 assert!(
901 !css.contains("primary"),
902 "dynamic member not serialized: {css:?}"
903 );
904 assert!(css.contains("padding:8px;"), "static survives: {css:?}");
905 let a = compute_css_analytics(&css).expect("must parse, not None");
906 assert_eq!(a.important_declarations, 0, "no invented !important: {a:?}");
907 }
908
909 #[test]
910 fn spread_and_computed_key_dropped() {
911 let src = "import { style } from '@vanilla-extract/css';\n\
912 const base = {};\n\
913 const k = 'color';\n\
914 const x = style({ ...base, [k]: 'red', padding: 8, margin: 4, top: 1 });\n";
915 let s = sheets(src);
916 let css = s.structural_partial.expect("partial");
918 assert!(css.contains("padding:8px;"), "static survives: {css:?}");
919 }
920
921 #[test]
922 fn cva_variants_map_is_not_serialized_as_css() {
923 let cva = "import { cva } from 'class-variance-authority';\n\
926 const button = cva('base', { variants: { size: { sm: 'text-sm' } } });\n";
927 assert!(
928 sheets(cva).is_empty(),
929 "unrelated cva must not fire: {:?}",
930 sheets(cva)
931 );
932
933 let panda = "import { cva } from '../styled-system/css';\n\
936 const button = cva({ base: { color: 'red', padding: 8, margin: 4, top: 1 }, variants: { size: { sm: { fontSize: 12 } } } });\n";
937 let s = sheets(panda);
938 let css = s.atomic.expect("panda cva base is atomic");
939 assert!(css.contains("color:red;"), "base serialized: {css:?}");
940 assert!(
941 !css.contains("size"),
942 "variants config not serialized: {css:?}"
943 );
944 let a = compute_css_analytics(&css).expect("parses cleanly");
945 assert!(
946 a.notable_rules.is_empty(),
947 "no garbled structural finding: {a:?}"
948 );
949 }
950
951 #[test]
952 fn panda_cva_and_class_variance_authority_cva_coexist() {
953 let src = "import { cva } from '../styled-system/css';\n\
958 import { cva as cn } from 'class-variance-authority';\n\
959 const a = cva({ base: { color: 'red' } });\n\
960 const b = cn('base', { variants: { size: { sm: 'text-sm' } } });\n";
961 let css = sheets(src).atomic.expect("panda cva base is atomic");
962 assert!(css.contains("color:red;"), "panda base lifted: {css:?}");
963 assert!(!css.contains("text-sm"), "cva-lib not serialized: {css:?}");
964 }
965
966 #[test]
967 fn local_helper_with_recognized_name_does_not_fire() {
968 let src = "const css = (o) => o;\n\
971 const x = css({ color: 'red', padding: 8 });\n";
972 assert!(
973 sheets(src).is_empty(),
974 "local css helper must not fire: {:?}",
975 sheets(src)
976 );
977 }
978
979 #[test]
980 fn type_only_import_does_not_open_the_gate() {
981 let src = "import type { style } from '@vanilla-extract/css';\n\
982 const x = style({ color: 'red' });\n";
983 assert!(
984 sheets(src).is_empty(),
985 "type-only import must not open provenance: {:?}",
986 sheets(src)
987 );
988 }
989
990 #[test]
991 fn all_dynamic_bucket_emits_no_empty_rule() {
992 let src = "import { style } from '@vanilla-extract/css';\n\
993 import { v } from './v';\n\
994 const x = style({ color: v.a, background: v.b });\n";
995 let s = sheets(src);
996 assert!(s.is_empty(), "all-dynamic bucket dropped entirely: {s:?}");
999 }
1000
1001 #[test]
1002 fn aliased_named_import_still_recognized() {
1003 let src = "import { style as s, globalStyle as gs } from '@vanilla-extract/css';\n\
1005 export const a = s({ color: 'red' });\n\
1006 gs('html', { margin: 0 });\n";
1007 let s = sheets(src);
1008 let css = s.structural.expect("aliased style/globalStyle recognized");
1009 assert!(css.contains("color:red;"), "aliased style fired: {css:?}");
1010 assert!(
1011 css.contains("html{margin:0;}"),
1012 "aliased globalStyle fired: {css:?}"
1013 );
1014 }
1015
1016 #[test]
1017 fn emotion_css_default_import_recognized() {
1018 let src = "import css from '@emotion/css';\n\
1020 const a = css({ color: 'red' });\n";
1021 let css = sheets(src)
1022 .structural
1023 .expect("default css import recognized");
1024 assert!(css.contains("color:red;"), "css={css:?}");
1025 }
1026
1027 #[test]
1028 fn emotion_css_default_import_aliased_recognized() {
1029 let src = "import emo from '@emotion/css';\n\
1032 const a = emo({ color: 'red' });\n";
1033 let css = sheets(src)
1034 .structural
1035 .expect("aliased default css import recognized");
1036 assert!(css.contains("color:red;"), "css={css:?}");
1037 }
1038
1039 #[test]
1040 fn non_decimal_numeric_literals_become_valid_css() {
1041 let src = "import { style } from '@vanilla-extract/css';\n\
1044 const x = style({ padding: 0xFF, zIndex: 1e3 });\n";
1045 let css = sheets(src).structural.expect("structural");
1046 assert!(
1047 css.contains("padding:255px;"),
1048 "hex -> decimal px: css={css:?}"
1049 );
1050 assert!(
1051 css.contains("z-index:1000;"),
1052 "scientific -> decimal: css={css:?}"
1053 );
1054 assert!(compute_css_analytics(&css).is_some(), "valid CSS");
1055 }
1056
1057 #[test]
1058 fn custom_property_numeric_value_keeps_no_unit() {
1059 let src = "import { css } from '@emotion/react';\n\
1063 const g = css({ ':root': { '--space': 8, '--ratio': 1.5 }, padding: 8 });\n";
1064 let sheet = sheets(src)
1066 .structural
1067 .or_else(|| sheets(src).structural_partial)
1068 .expect("structural output");
1069 assert!(
1070 sheet.contains("--space:8;"),
1071 "custom prop keeps no unit: {sheet:?}"
1072 );
1073 assert!(
1074 sheet.contains("--ratio:1.5;"),
1075 "custom prop float unchanged: {sheet:?}"
1076 );
1077 assert!(
1079 sheet.contains("padding:8px;"),
1080 "normal prop still px: {sheet:?}"
1081 );
1082 }
1083
1084 #[test]
1085 fn ms_vendor_prefix_kebabs_with_leading_dash() {
1086 assert_eq!(kebab_case("msFlexAlign"), "-ms-flex-align");
1087 assert_eq!(kebab_case("WebkitBoxShadow"), "-webkit-box-shadow");
1088 assert_eq!(kebab_case("backgroundColor"), "background-color");
1089 assert_eq!(kebab_case("msgType"), "msg-type");
1091 }
1092
1093 #[test]
1094 fn negative_numbers_handled() {
1095 let src = "import { style } from '@vanilla-extract/css';\n\
1096 const x = style({ marginTop: -8, zIndex: -1 });\n";
1097 let css = sheets(src).structural.expect("structural");
1098 assert!(css.contains("margin-top:-8px;"), "css={css:?}");
1099 assert!(
1100 css.contains("z-index:-1;"),
1101 "unitless negative: css={css:?}"
1102 );
1103 }
1104
1105 #[test]
1106 fn none_without_any_object_css_in_js() {
1107 assert!(sheets("const x = 1; function f() {}").is_empty());
1108 assert!(sheets("import React from 'react'; const x = <div/>;").is_empty());
1109 }
1110
1111 #[test]
1112 fn line_numbers_map_back_to_source() {
1113 let src = "import { style } from '@vanilla-extract/css';\n\
1116 \n\
1117 const a = style({\n\
1118 color: 'red',\n\
1119 });\n";
1120 let css = sheets(src).structural.expect("structural");
1121 let pos = css.find("color").expect("color present");
1122 let css_line = 1 + css[..pos].bytes().filter(|&b| b == b'\n').count();
1123 assert_eq!(
1124 css_line, 3,
1125 "bucket maps to the style() object line: css={css:?}"
1126 );
1127 }
1128
1129 #[test]
1130 fn multibyte_content_value_preserved() {
1131 let src = "import { style } from '@vanilla-extract/css';\n\
1132 const x = style({ content: '\"café 日本 €\"', fontFamily: '\"Ñoño\"' });\n";
1133 let css = sheets(src).structural.expect("structural");
1134 assert!(
1135 css.contains("café 日本 €"),
1136 "multibyte preserved: css={css:?}"
1137 );
1138 assert!(
1139 compute_css_analytics(&css).is_some(),
1140 "valid UTF-8 / parses"
1141 );
1142 }
1143
1144 #[test]
1145 fn distinct_colors_fall_out_of_object_styles() {
1146 let src = "import * as stylex from '@stylexjs/stylex';\n\
1147 const s = stylex.create({ a: { color: 'red' }, b: { color: 'blue' }, c: { color: 'red' } });\n";
1148 let css = sheets(src).atomic.expect("atomic");
1149 let a = compute_css_analytics(&css).expect("parses");
1150 assert_eq!(a.colors.len(), 2, "distinct colors counted: {:?}", a.colors);
1151 }
1152
1153 #[test]
1154 fn multi_bucket_padding_uses_key_line() {
1155 let src = "import * as stylex from '@stylexjs/stylex';\n\
1158 const s = stylex.create({\n\
1159 root: { color: 'red' },\n\
1160 card: { color: 'blue' },\n\
1161 });\n";
1162 let css = sheets(src).atomic.expect("atomic");
1163 let red = css.find("color:red").expect("root present");
1164 let blue = css.find("color:blue").expect("card present");
1165 let red_line = 1 + css[..red].bytes().filter(|&b| b == b'\n').count();
1166 let blue_line = 1 + css[..blue].bytes().filter(|&b| b == b'\n').count();
1167 assert_eq!(red_line, 3, "root on its key line: css={css:?}");
1168 assert_eq!(blue_line, 4, "card on its own key line: css={css:?}");
1169 }
1170}