use std::path::Path;
use oxc_allocator::Allocator;
use oxc_ast::ast::{
Argument, Expression, ImportDeclarationSpecifier, NumericLiteral, ObjectExpression,
ObjectPropertyKind, Program, PropertyKey, Statement, UnaryOperator,
};
use oxc_ast_visit::{Visit, walk};
use oxc_parser::Parser;
use oxc_span::{GetSpan, SourceType};
use rustc_hash::FxHashMap;
use super::shared::{WRAPPER, count_newlines};
const UNITLESS_PROPERTIES: &[&str] = &[
"animationIterationCount",
"aspectRatio",
"borderImageOutset",
"borderImageSlice",
"borderImageWidth",
"boxFlex",
"boxFlexGroup",
"boxOrdinalGroup",
"columnCount",
"columns",
"flex",
"flexGrow",
"flexPositive",
"flexShrink",
"flexNegative",
"flexOrder",
"gridArea",
"gridRow",
"gridRowEnd",
"gridRowSpan",
"gridRowStart",
"gridColumn",
"gridColumnEnd",
"gridColumnSpan",
"gridColumnStart",
"fontWeight",
"lineClamp",
"lineHeight",
"opacity",
"order",
"orphans",
"scale",
"tabSize",
"widows",
"zIndex",
"zoom",
"fillOpacity",
"floodOpacity",
"stopOpacity",
"strokeDasharray",
"strokeDashoffset",
"strokeMiterlimit",
"strokeOpacity",
"strokeWidth",
];
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Lib {
VanillaExtract,
Emotion,
EmotionStyled,
StyleX,
Panda,
}
impl Lib {
const fn is_atomic(self) -> bool {
matches!(self, Self::StyleX | Self::Panda)
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct CssInJsObjectSheets {
pub structural: Option<String>,
pub structural_partial: Option<String>,
pub atomic: Option<String>,
}
impl CssInJsObjectSheets {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.structural.is_none() && self.structural_partial.is_none() && self.atomic.is_none()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Stream {
Structural,
StructuralPartial,
Atomic,
}
struct Bucket {
offset: u32,
rule: String,
stream: Stream,
}
#[must_use]
pub fn css_in_js_object_sheets(source: &str, path: &Path) -> CssInJsObjectSheets {
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
let mut collector = ObjectStyleCollector::new(source);
collector.build_import_map(&ret.program);
if collector.imports.is_empty() {
return CssInJsObjectSheets::default();
}
collector.visit_program(&ret.program);
collector.finish()
}
struct ObjectStyleCollector<'a> {
source: &'a str,
imports: FxHashMap<&'a str, (Lib, &'a str)>,
buckets: Vec<Bucket>,
}
impl<'a> ObjectStyleCollector<'a> {
fn new(source: &'a str) -> Self {
Self {
source,
imports: FxHashMap::default(),
buckets: Vec::new(),
}
}
fn build_import_map(&mut self, program: &Program<'a>) {
for stmt in &program.body {
let Statement::ImportDeclaration(decl) = stmt else {
continue;
};
if decl.import_kind.is_type() {
continue;
}
let Some(lib) = module_library(decl.source.value.as_str()) else {
continue;
};
let Some(specifiers) = &decl.specifiers else {
continue;
};
for specifier in specifiers {
let (local, role) = match specifier {
ImportDeclarationSpecifier::ImportSpecifier(s) => {
(s.local.name.as_str(), s.imported.name().as_str())
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
let role = if lib == Lib::Emotion {
"css"
} else {
s.local.name.as_str()
};
(s.local.name.as_str(), role)
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
(s.local.name.as_str(), s.local.name.as_str())
}
};
self.imports.insert(local, (lib, role));
}
}
}
fn finish(self) -> CssInJsObjectSheets {
let source = self.source;
let mut buckets = self.buckets;
buckets.sort_by_key(|b| b.offset);
CssInJsObjectSheets {
structural: render(source, &buckets, Stream::Structural),
structural_partial: render(source, &buckets, Stream::StructuralPartial),
atomic: render(source, &buckets, Stream::Atomic),
}
}
fn recognize(&self, callee: &Expression<'a>) -> Option<(Lib, CallKind)> {
match callee {
Expression::Identifier(id) => {
let (lib, role) = *self.imports.get(id.name.as_str())?;
let kind = match (lib, role) {
(Lib::VanillaExtract, "style") | (Lib::Emotion | Lib::Panda, "css") => {
CallKind::SingleObject
}
(Lib::VanillaExtract, "styleVariants") => CallKind::ObjectOfObjects,
(Lib::VanillaExtract, "globalStyle") => CallKind::GlobalStyle,
(Lib::VanillaExtract, "recipe") | (Lib::Panda, "cva") => CallKind::RecipeBase,
_ => return None,
};
Some((lib, kind))
}
Expression::StaticMemberExpression(member) => {
let Expression::Identifier(obj) = &member.object else {
return None;
};
let (lib, _) = *self.imports.get(obj.name.as_str())?;
let kind = match (lib, member.property.name.as_str()) {
(Lib::EmotionStyled, _) => CallKind::SingleObject,
(Lib::StyleX, "create") => CallKind::ObjectOfObjects,
_ => return None,
};
Some((lib, kind))
}
Expression::CallExpression(inner) => {
let Expression::Identifier(id) = &inner.callee else {
return None;
};
matches!(
self.imports.get(id.name.as_str()),
Some((Lib::EmotionStyled, _))
)
.then_some((Lib::EmotionStyled, CallKind::SingleObject))
}
_ => None,
}
}
fn collect_call(&mut self, callee: &Expression<'a>, args: &[Argument<'a>]) {
let Some((lib, kind)) = self.recognize(callee) else {
return;
};
let atomic = lib.is_atomic();
match kind {
CallKind::SingleObject => {
if let Some(obj) = object_arg(args, 0) {
self.push_bucket(obj, WRAPPER, atomic, obj.span().start);
}
}
CallKind::ObjectOfObjects => {
if args.len() != 1 {
return;
}
let Some(obj) = object_arg(args, 0) else {
return;
};
for prop in &obj.properties {
if let ObjectPropertyKind::ObjectProperty(p) = prop
&& let Expression::ObjectExpression(inner) = &p.value
{
self.push_bucket(inner, WRAPPER, atomic, p.key.span().start);
}
}
}
CallKind::RecipeBase => {
let Some(obj) = object_arg(args, 0) else {
return;
};
for prop in &obj.properties {
if let ObjectPropertyKind::ObjectProperty(p) = prop
&& static_key(&p.key).as_deref() == Some("base")
&& let Expression::ObjectExpression(inner) = &p.value
{
self.push_bucket(inner, WRAPPER, atomic, p.key.span().start);
}
}
}
CallKind::GlobalStyle => {
let (Some(selector), Some(obj)) = (string_arg(args, 0), object_arg(args, 1)) else {
return;
};
let selector = sanitize_selector(&selector);
if !selector.is_empty() {
self.push_bucket(obj, &selector, atomic, obj.span().start);
}
}
}
}
fn push_bucket(
&mut self,
obj: &ObjectExpression<'a>,
selector: &str,
atomic: bool,
offset: u32,
) {
let mut body = String::new();
let mut dropped = false;
serialize_object_body(obj, &mut body, &mut dropped);
if body.is_empty() {
return;
}
let stream = if atomic {
Stream::Atomic
} else if dropped {
Stream::StructuralPartial
} else {
Stream::Structural
};
self.buckets.push(Bucket {
offset,
rule: format!("{selector}{{{body}}}"),
stream,
});
}
}
impl<'a> Visit<'a> for ObjectStyleCollector<'a> {
fn visit_call_expression(&mut self, call: &oxc_ast::ast::CallExpression<'a>) {
self.collect_call(&call.callee, &call.arguments);
walk::walk_call_expression(self, call);
}
}
fn render(source: &str, buckets: &[Bucket], stream: Stream) -> Option<String> {
let mut out = String::new();
let mut current_line: usize = 1;
let mut found = false;
for bucket in buckets.iter().filter(|b| b.stream == stream) {
let block_line = 1 + count_newlines(&source[..bucket.offset as usize]);
while current_line < block_line {
out.push('\n');
current_line += 1;
}
out.push_str(&bucket.rule);
current_line += count_newlines(&bucket.rule);
found = true;
}
found.then_some(out)
}
enum CallKind {
SingleObject,
ObjectOfObjects,
RecipeBase,
GlobalStyle,
}
pub(super) fn module_library(specifier: &str) -> Option<Lib> {
match specifier {
"@vanilla-extract/css" | "@vanilla-extract/recipes" => Some(Lib::VanillaExtract),
"@emotion/react" | "@emotion/css" => Some(Lib::Emotion),
"@emotion/styled" => Some(Lib::EmotionStyled),
"@stylexjs/stylex" => Some(Lib::StyleX),
_ if specifier
.split(['/', '\\'])
.any(|segment| segment == "styled-system") =>
{
Some(Lib::Panda)
}
_ => None,
}
}
fn object_arg<'a, 'b>(args: &'b [Argument<'a>], index: usize) -> Option<&'b ObjectExpression<'a>> {
match args.get(index) {
Some(Argument::ObjectExpression(obj)) => Some(obj),
_ => None,
}
}
fn string_arg(args: &[Argument<'_>], index: usize) -> Option<String> {
match args.get(index) {
Some(Argument::StringLiteral(lit)) => Some(lit.value.to_string()),
_ => None,
}
}
fn serialize_object_body(obj: &ObjectExpression<'_>, out: &mut String, dropped: &mut bool) {
for prop in &obj.properties {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
*dropped = true;
continue;
};
let Some(key) = static_key(&prop.key) else {
*dropped = true;
continue;
};
match &prop.value {
Expression::ObjectExpression(nested) if is_selector_key(&key) => {
serialize_nested(&key, nested, out, dropped);
}
Expression::ObjectExpression(_) => {
*dropped = true;
}
value => {
if let Some(rendered) = serialize_value(&key, value) {
out.push_str(&rendered);
} else {
*dropped = true;
}
}
}
}
}
fn serialize_nested(
key: &str,
nested: &ObjectExpression<'_>,
out: &mut String,
dropped: &mut bool,
) {
if key == "selectors" {
for prop in &nested.properties {
match prop {
ObjectPropertyKind::ObjectProperty(p) => {
if let (Some(inner_key), Expression::ObjectExpression(inner)) =
(static_key(&p.key), &p.value)
{
serialize_nested(&inner_key, inner, out, dropped);
} else {
*dropped = true;
}
}
ObjectPropertyKind::SpreadProperty(_) => *dropped = true,
}
}
return;
}
let mut body = String::new();
serialize_object_body(nested, &mut body, dropped);
if body.is_empty() {
return;
}
out.push_str(&nested_selector(key));
out.push('{');
out.push_str(&body);
out.push('}');
}
fn is_selector_key(key: &str) -> bool {
if key == "selectors" {
return true;
}
matches!(
key.trim_start().chars().next(),
Some(':' | '&' | '@' | '>' | '+' | '~' | '.' | '#' | '[' | '*')
) || key.starts_with(' ')
}
fn nested_selector(key: &str) -> String {
let trimmed = key.trim();
if trimmed.starts_with('@') || trimmed.starts_with('&') {
return trimmed.to_string();
}
format!("&{trimmed}")
}
fn serialize_value(key: &str, value: &Expression<'_>) -> Option<String> {
let rendered = static_value(key, value)?;
Some(format!("{}:{rendered};", kebab_case(key)))
}
fn static_value(key: &str, value: &Expression<'_>) -> Option<String> {
match value {
Expression::StringLiteral(lit) => {
let text = lit.value.as_str().trim();
(!text.is_empty()).then(|| text.to_string())
}
Expression::NumericLiteral(num) => Some(render_number(key, num)),
Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::UnaryNegation => {
if let Expression::NumericLiteral(num) = &unary.argument {
Some(format!("-{}", render_number(key, num)))
} else {
None
}
}
_ => None,
}
}
fn render_number(key: &str, num: &NumericLiteral<'_>) -> String {
let value = format_f64(num.value);
if is_unitless(key) || key.starts_with("--") || num.value == 0.0 {
value
} else {
format!("{value}px")
}
}
fn format_f64(value: f64) -> String {
if value.fract() == 0.0 {
format!("{value:.0}")
} else {
value.to_string()
}
}
fn is_unitless(key: &str) -> bool {
UNITLESS_PROPERTIES.contains(&key)
}
fn static_key(key: &PropertyKey<'_>) -> Option<String> {
key.static_name().map(|name| name.to_string())
}
fn kebab_case(name: &str) -> String {
if name.starts_with("--") || name.contains('-') {
return name.to_string();
}
let mut out = String::with_capacity(name.len() + 2);
if let Some(rest) = name.strip_prefix("ms")
&& rest.chars().next().is_some_and(|c| c.is_ascii_uppercase())
{
out.push('-');
}
for ch in name.chars() {
if ch.is_ascii_uppercase() {
out.push('-');
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch);
}
}
out
}
fn sanitize_selector(selector: &str) -> String {
selector
.chars()
.filter(|&c| c != '{' && c != '}' && c != ';')
.collect::<String>()
.trim()
.to_string()
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use crate::compute_css_analytics;
fn sheets(source: &str) -> CssInJsObjectSheets {
css_in_js_object_sheets(source, Path::new("styles.ts"))
}
#[test]
fn vanilla_extract_style_lifts_to_parseable_css() {
let src = "import { style } from '@vanilla-extract/css';\n\
export const box = style({\n\
backgroundColor: 'red',\n\
padding: 8,\n\
});\n";
let s = sheets(src);
let css = s.structural.expect("vanilla-extract style is structural");
assert!(css.contains("background-color:red;"), "css={css:?}");
assert!(css.contains("padding:8px;"), "px default: css={css:?}");
let a = compute_css_analytics(&css).expect("lifted CSS parses");
assert!(a.total_declarations >= 2, "declarations counted: {a:?}");
assert!(s.atomic.is_none(), "vanilla-extract is not atomic");
}
#[test]
fn unitless_properties_keep_bare_number() {
let src = "import { style } from '@vanilla-extract/css';\n\
const x = style({ lineHeight: 1.5, zIndex: 10, fontWeight: 700, padding: 4 });\n";
let css = sheets(src).structural.expect("structural");
assert!(css.contains("line-height:1.5;"), "css={css:?}");
assert!(css.contains("z-index:10;"), "css={css:?}");
assert!(css.contains("font-weight:700;"), "css={css:?}");
assert!(css.contains("padding:4px;"), "css={css:?}");
}
#[test]
fn one_level_nesting_via_relative_selector() {
let src = "import { style } from '@vanilla-extract/css';\n\
const x = style({ color: 'red', ':hover': { color: 'blue' } });\n";
let css = sheets(src).structural.expect("structural");
assert!(
css.contains("&:hover{color:blue;}"),
"nested rule: css={css:?}"
);
let a = compute_css_analytics(&css).expect("nested parses");
assert!(a.rule_count >= 2, "nested rule counted: {a:?}");
}
#[test]
fn vanilla_extract_selectors_wrapper_unwrapped() {
let src = "import { style } from '@vanilla-extract/css';\n\
const x = style({ color: 'red', selectors: { '&:hover': { color: 'blue' } } });\n";
let css = sheets(src).structural.expect("structural");
assert!(
css.contains("&:hover{color:blue;}"),
"selectors wrapper unwrapped: css={css:?}"
);
assert!(
!css.contains("selectors{"),
"no literal selectors rule: css={css:?}"
);
}
#[test]
fn global_style_keeps_real_selector() {
let src = "import { globalStyle } from '@vanilla-extract/css';\n\
globalStyle('html, body', { margin: 0 });\n";
let css = sheets(src).structural.expect("structural");
assert!(
css.contains("html, body{margin:0;}"),
"real selector: css={css:?}"
);
let a = compute_css_analytics(&css).expect("parses");
assert_eq!(a.rule_count, 1);
}
#[test]
fn stylex_create_is_atomic_one_bucket_per_key() {
let src = "import * as stylex from '@stylexjs/stylex';\n\
export const styles = stylex.create({\n\
root: { color: 'red', padding: 16 },\n\
card: { color: 'blue' },\n\
});\n";
let s = sheets(src);
assert!(s.structural.is_none(), "stylex is atomic, not structural");
let css = s.atomic.expect("stylex.create is atomic");
assert!(css.contains("color:red;"), "css={css:?}");
assert!(css.contains("padding:16px;"), "css={css:?}");
assert!(css.contains("color:blue;"), "second bucket: css={css:?}");
let a = compute_css_analytics(&css).expect("parses");
assert!(a.rule_count >= 2, "two buckets: {a:?}");
}
#[test]
fn panda_css_from_styled_system_is_atomic() {
let src = "import { css } from '../styled-system/css';\n\
const c = css({ display: 'flex', gap: 8 });\n";
let s = sheets(src);
let css = s.atomic.expect("panda css is atomic");
assert!(css.contains("display:flex;"), "css={css:?}");
assert!(css.contains("gap:8px;"), "css={css:?}");
}
#[test]
fn emotion_css_and_styled_are_structural() {
let src = "import { css } from '@emotion/react';\n\
import styled from '@emotion/styled';\n\
const a = css({ color: 'red' });\n\
const B = styled.div({ fontWeight: 700 });\n";
let css = sheets(src).structural.expect("emotion is structural");
assert!(css.contains("color:red;"), "css={css:?}");
assert!(css.contains("font-weight:700;"), "styled.div: css={css:?}");
}
#[test]
fn styled_call_form_is_lifted() {
let src = "import styled from '@emotion/styled';\n\
const Primary = styled(Button)({ fontWeight: 700 });\n";
let css = sheets(src)
.structural
.expect("styled(Component)({}) lifted");
assert!(css.contains("font-weight:700;"), "css={css:?}");
}
#[test]
fn dynamic_value_is_dropped_to_structural_partial() {
let src = "import { style } from '@vanilla-extract/css';\n\
import { theme } from './theme';\n\
const x = style({ color: theme.primary, padding: 8, margin: 4, top: 1, left: 2 });\n";
let s = sheets(src);
assert!(s.structural.is_none(), "bucket had a drop: {s:?}");
let css = s.structural_partial.expect("partial");
assert!(
!css.contains("fallowinterp"),
"no placeholder, value dropped: {css:?}"
);
assert!(
!css.contains("primary"),
"dynamic member not serialized: {css:?}"
);
assert!(css.contains("padding:8px;"), "static survives: {css:?}");
let a = compute_css_analytics(&css).expect("must parse, not None");
assert_eq!(a.important_declarations, 0, "no invented !important: {a:?}");
}
#[test]
fn spread_and_computed_key_dropped() {
let src = "import { style } from '@vanilla-extract/css';\n\
const base = {};\n\
const k = 'color';\n\
const x = style({ ...base, [k]: 'red', padding: 8, margin: 4, top: 1 });\n";
let s = sheets(src);
let css = s.structural_partial.expect("partial");
assert!(css.contains("padding:8px;"), "static survives: {css:?}");
}
#[test]
fn cva_variants_map_is_not_serialized_as_css() {
let cva = "import { cva } from 'class-variance-authority';\n\
const button = cva('base', { variants: { size: { sm: 'text-sm' } } });\n";
assert!(
sheets(cva).is_empty(),
"unrelated cva must not fire: {:?}",
sheets(cva)
);
let panda = "import { cva } from '../styled-system/css';\n\
const button = cva({ base: { color: 'red', padding: 8, margin: 4, top: 1 }, variants: { size: { sm: { fontSize: 12 } } } });\n";
let s = sheets(panda);
let css = s.atomic.expect("panda cva base is atomic");
assert!(css.contains("color:red;"), "base serialized: {css:?}");
assert!(
!css.contains("size"),
"variants config not serialized: {css:?}"
);
let a = compute_css_analytics(&css).expect("parses cleanly");
assert!(
a.notable_rules.is_empty(),
"no garbled structural finding: {a:?}"
);
}
#[test]
fn panda_cva_and_class_variance_authority_cva_coexist() {
let src = "import { cva } from '../styled-system/css';\n\
import { cva as cn } from 'class-variance-authority';\n\
const a = cva({ base: { color: 'red' } });\n\
const b = cn('base', { variants: { size: { sm: 'text-sm' } } });\n";
let css = sheets(src).atomic.expect("panda cva base is atomic");
assert!(css.contains("color:red;"), "panda base lifted: {css:?}");
assert!(!css.contains("text-sm"), "cva-lib not serialized: {css:?}");
}
#[test]
fn local_helper_with_recognized_name_does_not_fire() {
let src = "const css = (o) => o;\n\
const x = css({ color: 'red', padding: 8 });\n";
assert!(
sheets(src).is_empty(),
"local css helper must not fire: {:?}",
sheets(src)
);
}
#[test]
fn type_only_import_does_not_open_the_gate() {
let src = "import type { style } from '@vanilla-extract/css';\n\
const x = style({ color: 'red' });\n";
assert!(
sheets(src).is_empty(),
"type-only import must not open provenance: {:?}",
sheets(src)
);
}
#[test]
fn all_dynamic_bucket_emits_no_empty_rule() {
let src = "import { style } from '@vanilla-extract/css';\n\
import { v } from './v';\n\
const x = style({ color: v.a, background: v.b });\n";
let s = sheets(src);
assert!(s.is_empty(), "all-dynamic bucket dropped entirely: {s:?}");
}
#[test]
fn aliased_named_import_still_recognized() {
let src = "import { style as s, globalStyle as gs } from '@vanilla-extract/css';\n\
export const a = s({ color: 'red' });\n\
gs('html', { margin: 0 });\n";
let s = sheets(src);
let css = s.structural.expect("aliased style/globalStyle recognized");
assert!(css.contains("color:red;"), "aliased style fired: {css:?}");
assert!(
css.contains("html{margin:0;}"),
"aliased globalStyle fired: {css:?}"
);
}
#[test]
fn emotion_css_default_import_recognized() {
let src = "import css from '@emotion/css';\n\
const a = css({ color: 'red' });\n";
let css = sheets(src)
.structural
.expect("default css import recognized");
assert!(css.contains("color:red;"), "css={css:?}");
}
#[test]
fn emotion_css_default_import_aliased_recognized() {
let src = "import emo from '@emotion/css';\n\
const a = emo({ color: 'red' });\n";
let css = sheets(src)
.structural
.expect("aliased default css import recognized");
assert!(css.contains("color:red;"), "css={css:?}");
}
#[test]
fn non_decimal_numeric_literals_become_valid_css() {
let src = "import { style } from '@vanilla-extract/css';\n\
const x = style({ padding: 0xFF, zIndex: 1e3 });\n";
let css = sheets(src).structural.expect("structural");
assert!(
css.contains("padding:255px;"),
"hex -> decimal px: css={css:?}"
);
assert!(
css.contains("z-index:1000;"),
"scientific -> decimal: css={css:?}"
);
assert!(compute_css_analytics(&css).is_some(), "valid CSS");
}
#[test]
fn custom_property_numeric_value_keeps_no_unit() {
let src = "import { css } from '@emotion/react';\n\
const g = css({ ':root': { '--space': 8, '--ratio': 1.5 }, padding: 8 });\n";
let sheet = sheets(src)
.structural
.or_else(|| sheets(src).structural_partial)
.expect("structural output");
assert!(
sheet.contains("--space:8;"),
"custom prop keeps no unit: {sheet:?}"
);
assert!(
sheet.contains("--ratio:1.5;"),
"custom prop float unchanged: {sheet:?}"
);
assert!(
sheet.contains("padding:8px;"),
"normal prop still px: {sheet:?}"
);
}
#[test]
fn ms_vendor_prefix_kebabs_with_leading_dash() {
assert_eq!(kebab_case("msFlexAlign"), "-ms-flex-align");
assert_eq!(kebab_case("WebkitBoxShadow"), "-webkit-box-shadow");
assert_eq!(kebab_case("backgroundColor"), "background-color");
assert_eq!(kebab_case("msgType"), "msg-type");
}
#[test]
fn negative_numbers_handled() {
let src = "import { style } from '@vanilla-extract/css';\n\
const x = style({ marginTop: -8, zIndex: -1 });\n";
let css = sheets(src).structural.expect("structural");
assert!(css.contains("margin-top:-8px;"), "css={css:?}");
assert!(
css.contains("z-index:-1;"),
"unitless negative: css={css:?}"
);
}
#[test]
fn none_without_any_object_css_in_js() {
assert!(sheets("const x = 1; function f() {}").is_empty());
assert!(sheets("import React from 'react'; const x = <div/>;").is_empty());
}
#[test]
fn line_numbers_map_back_to_source() {
let src = "import { style } from '@vanilla-extract/css';\n\
\n\
const a = style({\n\
color: 'red',\n\
});\n";
let css = sheets(src).structural.expect("structural");
let pos = css.find("color").expect("color present");
let css_line = 1 + css[..pos].bytes().filter(|&b| b == b'\n').count();
assert_eq!(
css_line, 3,
"bucket maps to the style() object line: css={css:?}"
);
}
#[test]
fn multibyte_content_value_preserved() {
let src = "import { style } from '@vanilla-extract/css';\n\
const x = style({ content: '\"café 日本 €\"', fontFamily: '\"Ñoño\"' });\n";
let css = sheets(src).structural.expect("structural");
assert!(
css.contains("café 日本 €"),
"multibyte preserved: css={css:?}"
);
assert!(
compute_css_analytics(&css).is_some(),
"valid UTF-8 / parses"
);
}
#[test]
fn distinct_colors_fall_out_of_object_styles() {
let src = "import * as stylex from '@stylexjs/stylex';\n\
const s = stylex.create({ a: { color: 'red' }, b: { color: 'blue' }, c: { color: 'red' } });\n";
let css = sheets(src).atomic.expect("atomic");
let a = compute_css_analytics(&css).expect("parses");
assert_eq!(a.colors.len(), 2, "distinct colors counted: {:?}", a.colors);
}
#[test]
fn multi_bucket_padding_uses_key_line() {
let src = "import * as stylex from '@stylexjs/stylex';\n\
const s = stylex.create({\n\
root: { color: 'red' },\n\
card: { color: 'blue' },\n\
});\n";
let css = sheets(src).atomic.expect("atomic");
let red = css.find("color:red").expect("root present");
let blue = css.find("color:blue").expect("card present");
let red_line = 1 + css[..red].bytes().filter(|&b| b == b'\n').count();
let blue_line = 1 + css[..blue].bytes().filter(|&b| b == b'\n').count();
assert_eq!(red_line, 3, "root on its key line: css={css:?}");
assert_eq!(blue_line, 4, "card on its own key line: css={css:?}");
}
}