diff --git a/.changepacks/changepack_log_emotion_rule_numbers.json b/.changepacks/changepack_log_emotion_rule_numbers.json
new file mode 100644
index 000000000..f12449dd0
--- /dev/null
+++ b/.changepacks/changepack_log_emotion_rule_numbers.json
@@ -0,0 +1,7 @@
+{
+ "changes": {
+ "bindings/devup-ui-wasm/package.json": "Patch"
+ },
+ "note": "Numbers in rules written against Emotion, styled-components or vanilla-extract now read as px lengths when a constant stands in for the literal, as they do for a literal written in place: css({ padding: gap }) with const gap = 8, styled.div({ margin: SIZE }), and the same with imported constants and constant style objects give 8px, while unitless properties keep bare numbers, 0 stays 0 and Devup shorthands such as p keep the spacing scale. css`${mixin}; color: red;` outside the css prop composes the mixin by styles, so a later declaration replaces the mixin's, and a css`...` template is a known style a later css() composes. Inline css() calls inside css() arguments (css(css({ ... }), { ... }), css(cond && css({ ... }), base)) are composed with the other parts, the later winning, instead of being compiled to a class of their own. No new build errors",
+ "date": "2026-10-01T00:00:00.000Z"
+}
diff --git a/libs/extractor/src/build_time_values.rs b/libs/extractor/src/build_time_values.rs
index 49d620cee..97f9a5daa 100644
--- a/libs/extractor/src/build_time_values.rs
+++ b/libs/extractor/src/build_time_values.rs
@@ -77,6 +77,7 @@ pub(crate) fn has_build_time_values(
option,
resolver,
aliased.css_prop,
+ &aliased.px,
);
let changes = crate::imported_constants::ChangeCheck::new(&program, filename, option, resolver);
!find(
diff --git a/libs/extractor/src/import_alias_visit.rs b/libs/extractor/src/import_alias_visit.rs
index dff94ddf3..117d84d50 100644
--- a/libs/extractor/src/import_alias_visit.rs
+++ b/libs/extractor/src/import_alias_visit.rs
@@ -62,9 +62,17 @@ struct LibraryNumbers<'n> {
class_names_calls: Vec,
scoping: Option,
replacements: Vec<(usize, usize, String)>,
+ /// Where a constant inlined later is a number or rules of the library, as
+ /// the source reads them
+ marks: Vec<(usize, usize)>,
}
impl LibraryNumbers<'_> {
+ fn mark(expression: &Expression) -> (usize, usize) {
+ let span = expression.span();
+ (span.start as usize, span.end as usize)
+ }
+
/// `styled.div`, `styled(tag)`, `styled(tag, options)` and their
/// `.attrs()` / `.withConfig()`: whatever a call of it passes are rules
fn is_styled_factory(&self, callee: &Expression) -> bool {
@@ -94,11 +102,17 @@ impl LibraryNumbers<'_> {
}
}
Expression::ObjectExpression(object) => {
+ self.marks.push(Self::mark(rules));
for property in &object.properties {
- if let ObjectPropertyKind::ObjectProperty(property) = property
- && let Some(key) = get_str_by_property_key(&property.key)
- {
- self.pixelify_value(&key, &property.value);
+ match property {
+ ObjectPropertyKind::ObjectProperty(property) => {
+ if let Some(key) = get_str_by_property_key(&property.key) {
+ self.pixelify_value(&key, &property.value);
+ }
+ }
+ ObjectPropertyKind::SpreadProperty(spread) => {
+ self.marks.push(Self::mark(&spread.argument));
+ }
}
}
}
@@ -108,7 +122,7 @@ impl LibraryNumbers<'_> {
}
Expression::LogicalExpression(logical) => self.pixelify(&logical.right),
Expression::ParenthesizedExpression(inner) => self.pixelify(&inner.expression),
- _ => {}
+ rules => self.marks.push(Self::mark(rules)),
}
}
@@ -184,6 +198,9 @@ impl LibraryNumbers<'_> {
}
return;
}
+ if !matches!(value, Expression::ObjectExpression(_)) && !keeps_bare_number(key) {
+ self.marks.push(Self::mark(value));
+ }
match value {
Expression::ArrayExpression(array) => self.fallbacks(key, array),
Expression::ObjectExpression(_) => {
@@ -195,7 +212,12 @@ impl LibraryNumbers<'_> {
self.pixelify_value(key, &conditional.consequent);
self.pixelify_value(key, &conditional.alternate);
}
- Expression::LogicalExpression(logical) => self.pixelify_value(key, &logical.right),
+ Expression::LogicalExpression(logical) => {
+ if logical.operator != LogicalOperator::And {
+ self.pixelify_value(key, &logical.left);
+ }
+ self.pixelify_value(key, &logical.right);
+ }
Expression::ParenthesizedExpression(inner) => {
self.pixelify_value(key, &inner.expression);
}
@@ -210,29 +232,9 @@ impl LibraryNumbers<'_> {
/// UI reads an array as breakpoints. Literal values become one value
/// declaring each in order; others stay as written.
fn fallbacks(&mut self, key: &str, array: &oxc_ast::ast::ArrayExpression) {
- let values: Option> = array
- .elements
- .iter()
- .map(|element| {
- let element = element.as_expression()?;
- if let Some(number) = js_number_literal(element) {
- return Some(if number == 0.0 || keeps_bare_number(key) {
- number.to_string()
- } else {
- format!("{number}px")
- });
- }
- match element {
- Expression::StringLiteral(literal) => Some(literal.value.to_string()),
- _ => None,
- }
- })
- .collect();
- let Some(values) = values.filter(|values| values.len() > 1) else {
+ let Some(joined) = fallback_value(key, array) else {
return;
};
- let property = kebab_property(key);
- let joined = values.join(&format!(";{property}:"));
let span = array.span;
self.replacements.push((
span.start as usize,
@@ -242,6 +244,30 @@ impl LibraryNumbers<'_> {
}
}
+pub(crate) fn fallback_value(key: &str, array: &oxc_ast::ast::ArrayExpression) -> Option {
+ let values: Option> = array
+ .elements
+ .iter()
+ .map(|element| {
+ let element = element.as_expression()?;
+ if let Some(number) = js_number_literal(element) {
+ return Some(if number == 0.0 || keeps_bare_number(key) {
+ number.to_string()
+ } else {
+ format!("{number}px")
+ });
+ }
+ match element {
+ Expression::StringLiteral(literal) => Some(literal.value.to_string()),
+ _ => None,
+ }
+ })
+ .collect();
+ let values = values.filter(|values| values.len() > 1)?;
+ let property = kebab_property(key);
+ Some(values.join(&format!(";{property}:")))
+}
+
/// `backgroundColor` as the CSS property `background-color`, `WebkitBoxShadow`
/// as `-webkit-box-shadow`
fn kebab_property(key: &str) -> String {
@@ -434,8 +460,16 @@ pub struct Aliased<'a> {
pub edits: Vec,
/// Which elements take Emotion's `css` prop
pub css_prop: CssProp,
+ /// Where, in `code`, a constant read is a number or rules the library
+ /// reads as `px` lengths
+ pub px: PxMarks,
}
+/// The spans of the style values and rules written as an expression a
+/// constant can stand in, which Emotion, styled-components and vanilla-extract
+/// read numbers in as `px` lengths
+pub type PxMarks = rustc_hash::FxHashSet<(u32, u32)>;
+
/// Packages generating CSS on top of vanilla-extract's, which Devup UI does not
/// compile
const VANILLA_EXTRACT_COMPANIONS: [&str; 2] =
@@ -496,10 +530,11 @@ pub fn transform_import_aliases_with_edits<'a>(
package: &str,
import_aliases: &HashMap,
) -> Aliased<'a> {
- let unchanged = |css_prop| Aliased {
+ let unchanged = |css_prop, px| Aliased {
code: Cow::Borrowed(code),
edits: Vec::new(),
css_prop,
+ px,
};
let emotion_aliased = import_aliases.contains_key(EMOTION_REACT);
let emotion_jsx = builds_jsx_with_emotion(import_aliases);
@@ -509,7 +544,7 @@ pub fn transform_import_aliases_with_edits<'a>(
&& CssProp::of(import_aliases, code, false) == CssProp::Off
&& (import_aliases.is_empty() || !import_aliases.keys().any(|alias| code.contains(alias)))
{
- return unchanged(CssProp::Off);
+ return unchanged(CssProp::Off, PxMarks::default());
}
let allocator = Allocator::default();
@@ -645,13 +680,32 @@ pub fn transform_import_aliases_with_edits<'a>(
// Apply transformations in reverse order to preserve positions
if transformations.is_empty() {
- return unchanged(numbers.css_prop);
+ let px = numbers
+ .marks
+ .iter()
+ .map(|&(start, end)| (start as u32, end as u32))
+ .collect();
+ return unchanged(numbers.css_prop, px);
}
let edits = transformations
.iter()
.map(|(start, end, replacement)| (*start, *end, replacement.len()))
.collect();
+ let shifted = |offset: usize| {
+ let before = transformations
+ .iter()
+ .take_while(|(_, end, _)| *end <= offset);
+ let (added, removed) = before.fold((0, 0), |(added, removed), (start, end, text)| {
+ (added + text.len(), removed + end - start)
+ });
+ (offset + added - removed) as u32
+ };
+ let px = numbers
+ .marks
+ .iter()
+ .map(|&(start, end)| (shifted(start), shifted(end)))
+ .collect();
let mut result = code.to_string();
for (start, end, replacement) in transformations.into_iter().rev() {
result.replace_range(start..end, &replacement);
@@ -661,6 +715,7 @@ pub fn transform_import_aliases_with_edits<'a>(
code: Cow::Owned(result),
edits,
css_prop: numbers.css_prop,
+ px,
}
}
diff --git a/libs/extractor/src/imported_constants.rs b/libs/extractor/src/imported_constants.rs
index df9bc4205..062a46272 100644
--- a/libs/extractor/src/imported_constants.rs
+++ b/libs/extractor/src/imported_constants.rs
@@ -372,6 +372,7 @@ pub(crate) fn inline_constants<'a>(
option: &ExtractOption,
resolver: Option<&ModuleResolver>,
css_prop: CssProp,
+ px: &crate::import_alias_visit::PxMarks,
) -> Inlined {
let is_style_package =
|source: &str| source.starts_with(&option.package) || source == crate::STYLEX_PACKAGE;
@@ -589,6 +590,8 @@ pub(crate) fn inline_constants<'a>(
objects: false,
styles: false,
px: false,
+ position: InlinePosition::Rules,
+ marks: px,
class_names: Vec::new(),
}
.visit_program(program);
@@ -1965,6 +1968,12 @@ fn fold_binary(operator: BinaryOperator, left: &Constant, right: &Constant) -> O
};
number.is_finite().then_some(Constant::Number(number))
}
+#[derive(Clone, Copy)]
+enum InlinePosition {
+ Rules,
+ Property,
+}
+
/// Replaces reads of constants where styles read them: primitives in the
/// arguments of style APIs and in style props, objects and arrays where they
/// are read as style objects, as a copy elsewhere would change their identity.
@@ -1983,6 +1992,10 @@ struct Inline<'s, 'a> {
styles: bool,
/// Inside a `css` prop, whose numbers Emotion reads as `px` lengths
px: bool,
+ position: InlinePosition,
+ /// Where a constant stands as a value or rules of a library reading
+ /// numbers as `px` lengths
+ marks: &'s crate::import_alias_visit::PxMarks,
/// The API bindings of the surrounding `` child functions.
class_names: Vec,
}
@@ -2150,22 +2163,40 @@ impl<'a> Inline<'_, 'a> {
}
}
+/// `value` as the `px` length Emotion reads it as when it is a number
+fn px_number<'a>(ast_builder: &AstBuilder<'a>, value: &mut Expression<'a>) {
+ if let Some(number) = crate::utils::js_number_literal(value)
+ && number != 0.0
+ {
+ *value = Expression::new_string_literal(
+ SPAN,
+ Str::from_in(format!("{number}px").as_str(), ast_builder.allocator()),
+ None,
+ ast_builder,
+ );
+ }
+}
+
/// `property` holding a number as the `px` length Emotion reads it as
fn px_value<'a>(ast_builder: &AstBuilder<'a>, property: &mut oxc_ast::ast::ObjectProperty<'a>) {
- if let Some(number) = crate::utils::js_number_literal(&property.value)
- && number != 0.0
- && property
- .key
- .static_name()
- .is_some_and(|key| !crate::utils::keeps_bare_number(&key))
+ if let Some(key) = property.key.static_name()
+ && let Expression::ArrayExpression(array) = &property.value
+ && let Some(value) = crate::import_alias_visit::fallback_value(&key, array)
{
property.value = Expression::new_string_literal(
SPAN,
- Str::from_in(format!("{number}px").as_str(), ast_builder.allocator()),
+ Str::from_in(value.as_str(), ast_builder.allocator()),
None,
ast_builder,
);
}
+ if property
+ .key
+ .static_name()
+ .is_some_and(|key| !crate::utils::keeps_bare_number(&key))
+ {
+ px_number(ast_builder, &mut property.value);
+ }
}
/// The rules `rules` with their numbers as the `px` lengths Emotion reads them
@@ -2173,24 +2204,24 @@ fn px_value<'a>(ast_builder: &AstBuilder<'a>, property: &mut oxc_ast::ast::Objec
fn px_rules<'a>(ast_builder: &AstBuilder<'a>, rules: &mut Expression<'a>) {
let mut pending = vec![rules];
while let Some(rules) = pending.pop() {
- match rules {
- Expression::ObjectExpression(object) => {
- for property in &mut object.properties {
- if let ObjectPropertyKind::ObjectProperty(property) = property {
- px_value(ast_builder, property);
+ if let Expression::ArrayExpression(array) = rules {
+ pending.extend(
+ array
+ .elements
+ .iter_mut()
+ .filter_map(|element| element.as_expression_mut()),
+ );
+ } else if let Expression::ObjectExpression(object) = rules {
+ for property in &mut object.properties {
+ if let ObjectPropertyKind::ObjectProperty(property) = property
+ && property.key.static_name().is_none_or(|key| key != "vars")
+ {
+ px_value(ast_builder, property);
+ if matches!(property.value, Expression::ObjectExpression(_)) {
pending.push(&mut property.value);
}
}
}
- Expression::ArrayExpression(array) => {
- pending.extend(
- array
- .elements
- .iter_mut()
- .filter_map(|element| element.as_expression_mut()),
- );
- }
- _ => {}
}
}
}
@@ -2258,8 +2289,16 @@ impl<'a> VisitMut<'a> for Inline<'_, 'a> {
return;
}
if let Some(literal) = self.literal(&constant) {
+ let span = expression.span();
+ let marked = self.marks.contains(&(span.start, span.end));
*expression = literal;
- if self.px {
+ if marked {
+ px_number(self.ast_builder, expression);
+ }
+ if (self.px || marked)
+ && (matches!(self.position, InlinePosition::Rules)
+ || matches!(expression, Expression::ObjectExpression(_)))
+ {
px_rules(self.ast_builder, expression);
}
return;
@@ -2271,7 +2310,12 @@ impl<'a> VisitMut<'a> for Inline<'_, 'a> {
return;
}
}
+ let span = expression.span();
+ let marked = self.styles && self.marks.contains(&(span.start, span.end));
walk_mut::walk_expression(self, expression);
+ if marked && matches!(expression, Expression::ObjectExpression(_)) {
+ px_rules(self.ast_builder, expression);
+ }
}
fn visit_tagged_template_expression(
@@ -2376,7 +2420,9 @@ impl<'a> VisitMut<'a> for Inline<'_, 'a> {
let inlined_number = self.px
&& self.styles
&& matches!(self.constant(&property.value), Some(Constant::Number(_)));
+ let outer = std::mem::replace(&mut self.position, InlinePosition::Property);
walk_mut::walk_object_property(self, property);
+ self.position = outer;
if property.shorthand && !matches!(property.value, Expression::Identifier(_)) {
property.shorthand = false;
}
diff --git a/libs/extractor/src/imported_styled.rs b/libs/extractor/src/imported_styled.rs
index bc9ed4325..29841de0a 100644
--- a/libs/extractor/src/imported_styled.rs
+++ b/libs/extractor/src/imported_styled.rs
@@ -225,6 +225,7 @@ impl<'a> Reader<'_, 'a> {
let Aliased {
code: transformed,
css_prop,
+ px,
..
} = transform_import_aliases_with_edits(
code,
@@ -251,6 +252,7 @@ impl<'a> Reader<'_, 'a> {
self.option,
Some(self.resolver),
css_prop,
+ &px,
);
self.dependencies.extend(inlined.dependencies);
let imports = self.imports(&program, path, css_prop, &sub);
diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs
index 10f315d47..3677c4e87 100644
--- a/libs/extractor/src/lib.rs
+++ b/libs/extractor/src/lib.rs
@@ -392,6 +392,7 @@ fn extract_source(
code: transformed_code,
edits: alias_edits,
css_prop,
+ px,
} = import_alias_visit::transform_import_aliases_with_edits(
&namespace_code,
filename,
@@ -565,6 +566,7 @@ fn extract_source(
&option,
resolver,
css_prop,
+ &px,
)
} else {
imported_constants::Inlined::default()
@@ -23838,6 +23840,165 @@ injectGlobal`body { margin: 0; }`;",
assert_debug_snapshot!(errors);
}
+ fn library_option() -> ExtractOption {
+ let mut option = emotion_option();
+ option.import_aliases.insert(
+ "styled-components".to_string(),
+ ImportAlias::DefaultToNamed("styled".to_string()),
+ );
+ option.import_aliases.insert(
+ "@vanilla-extract/css".to_string(),
+ ImportAlias::NamedToNamed,
+ );
+ option
+ }
+
+ const SIZE_MODULES: &[(&str, &str)] = &[
+ (
+ "/src/sizes.ts",
+ "export const SIZE = 12;\nexport const ZERO = 0;\nexport const LINE = 2;\nexport const RULES = { padding: 5, lineHeight: 2, vars: { gap: 3 } };\nexport const NAME = 'big';",
+ ),
+ ("/src/App.tsx", ""),
+ ];
+
+ fn library_outputs(
+ cases: &[&str],
+ modules: &'static [(&'static str, &'static str)],
+ ) -> Vec<(String, String)> {
+ cases
+ .iter()
+ .map(|code| {
+ reset_class_map();
+ reset_file_map();
+ let output = match extract_with_modules(
+ "/src/App.tsx",
+ code,
+ library_option(),
+ false,
+ &memory_resolver(modules),
+ ) {
+ Ok(output) => format!("{:?}", ToBTreeSet::from(output)),
+ Err(error) => format!("Error: {error}"),
+ };
+ ((*code).to_string(), output)
+ })
+ .collect()
+ }
+
+ #[rstest]
+ #[case::literal("css({ padding: [1, 2] })", &[("padding", "1px;padding:2px", 0)])]
+ #[case::local_items("css({ padding: [A, B] })", &[("padding", "1px;padding:2px", 0)])]
+ #[case::local_array("css({ padding: values })", &[("padding", "1px;padding:2px", 0)])]
+ #[case::imported_array("css({ padding: VALUES })", &[("padding", "1px;padding:2px", 0)])]
+ #[case::imported_record("css(RULES)", &[("padding", "1px;padding:2px", 0)])]
+ #[case::spread("css({ ...RULES })", &[("padding", "1px;padding:2px", 0)])]
+ #[case::nested("css({ '&:hover': RULES })", &[("padding", "1px;padding:2px", 0)])]
+ #[case::nested_items("css({ '&:hover': { padding: [A, B] } })", &[("padding", "1px;padding:2px", 0)])]
+ #[case::unitless("css({ opacity: [QUARTER, HALF] })", &[("opacity", ".25;opacity:.5", 0)])]
+ #[case::mixed_zero("css({ padding: [ZERO, A, '3em'] })", &[("padding", "0;padding:1px;padding:3em", 0)])]
+ #[case::strings("css({ display: [OLD, 'flex'] })", &[("display", "-webkit-box;display:flex", 0)])]
+ #[case::vendor("css({ WebkitBoxShadow: [NONE, '0 0 1px red'] })", &[("-webkit-box-shadow", "none;-webkit-box-shadow:0 0 1px red", 0)])]
+ #[case::single("css({ padding: [A] })", &[("padding", "4px", 0)])]
+ #[case::null("css({ padding: [A, null, B] })", &[("padding", "4px", 0), ("padding", "8px", 2)])]
+ #[case::bool("css({ padding: [A, false, B] })", &[("padding", "4px", 0), ("padding", "8px", 2), ("padding", "false", 1)])]
+ #[case::imported_rules_array("css(LIST)", &[("padding", "1px;padding:2px", 0)])]
+ #[case::native("nativeCss({ padding: [A, B] })", &[("padding", "4px", 0), ("padding", "8px", 1)])]
+ #[case::native_box("", &[("padding", "4px", 0), ("padding", "8px", 1)])]
+ #[case::vars("css({ vars: { padding: [A, B] } })", &[("padding", "4px", 0), ("padding", "8px", 1)])]
+ #[case::empty("css({ padding: [] })", &[])]
+ #[case::hole("css({ padding: [A, , B] })", &[("padding", "4px", 0), ("padding", "8px", 2)])]
+ #[case::selector_array("css({ _hover: [{ padding: A }] })", &[("padding", "4px", 0)])]
+ #[case::selector_constant_array("css({ _hover: selectorValues })", &[("padding", "4px", 0)])]
+ #[serial]
+ fn test_library_constant_fallback_arrays(
+ #[case] expression: &str,
+ #[case] expected: &[(&str, &str, u8)],
+ ) {
+ // Given: the same static leaves reached through local and imported constants.
+ const MODULES: &[(&str, &str)] = &[
+ (
+ "/src/fallbacks.ts",
+ "export const VALUES = [1, 2]; export const RULES = { padding: [1, 2] }; export const LIST = [RULES];",
+ ),
+ ("/src/App.tsx", ""),
+ ];
+ reset_class_map();
+ reset_file_map();
+ let code = format!(
+ "import {{ css }} from '@emotion/react';\nimport {{ css as nativeCss, Box }} from '@devup-ui/react';\nimport {{ VALUES, RULES, LIST }} from './fallbacks';\nconst A=1, B=2, ZERO=0, QUARTER=0.25, HALF=0.5, OLD='-webkit-box', NONE='none';\nconst values=[A,B], selectorValues=[{{padding:A}}];\nexport const a = {expression};"
+ );
+ // When: extract through the real alias/inlining/visitor pipeline.
+ let output = extract_with_modules(
+ "/src/App.tsx",
+ &code,
+ library_option(),
+ false,
+ &memory_resolver(MODULES),
+ )
+ .unwrap_or_else(|error| panic!("{expression}\n{error}"));
+ let mut styles: Vec<_> = output
+ .styles
+ .iter()
+ .map(|style| match style {
+ ExtractStyleValue::Static(style) => {
+ (style.property.as_str(), style.value.as_str(), style.level)
+ }
+ other => panic!("unexpected style: {other:?}"),
+ })
+ .collect();
+ styles.sort_unstable();
+ // Then: library fallbacks repeat at level zero; native and nonfallback arrays keep their levels.
+ assert_eq!(styles, expected, "{expression}");
+ }
+
+ #[rstest]
+ #[case("[A, value]", "value")]
+ #[case("[A, unknown()]", "unknown()")]
+ #[case("values", "values")]
+ #[serial]
+ fn test_library_constant_fallback_arrays_keep_unknown_guards(
+ #[case] value: &str,
+ #[case] rejected: &str,
+ ) {
+ // Given: one known number cannot make an unknown array or leaf static.
+ reset_class_map();
+ reset_file_map();
+ let code = format!(
+ "import {{ css }} from '@emotion/react';\nconst A=1;\nexport const a = (value, values) => css({{ padding: {value} }});"
+ );
+ // When: extract the unknown value through the same library API.
+ let error = extract("/src/App.tsx", &code, library_option())
+ .err()
+ .unwrap_or_else(|| panic!("expected rejection for {value}"))
+ .to_string();
+ // Then: the located build-time refusal still names the unknown input.
+ assert!(error.starts_with("/src/App.tsx:3:"), "{error}");
+ assert!(error.contains("at build time"), "{error}");
+ assert!(error.contains(rejected), "{error}");
+ }
+
+ #[test]
+ #[serial]
+ fn test_emotion_rules_read_local_constants_as_pixels() {
+ assert_debug_snapshot!(library_outputs(
+ &[
+ "import { css } from '@emotion/react';\nconst gap = 8;\nconst ZERO = 0;\nconst LINE = 2;\nexport const a = css({ padding: gap, margin: ZERO, lineHeight: LINE, opacity: LINE, p: gap, mx: gap });",
+ "import { css } from '@emotion/react';\nconst gap = 8;\nexport const a = css({ '&:hover': { top: -gap, left: gap * 2 }, width: gap > 4 ? gap : 1, height: gap || 3, minWidth: (gap) });",
+ "import { css } from '@emotion/react';\nconst gap = 8;\nconst rules = { padding: gap, lineHeight: 2, vars: { margin: 3 } };\nconst nested = { '&:hover': rules };\nexport const a = css(rules);\nexport const b = css({ ...rules, margin: 1 });\nexport const c = css({ '&:focus': rules });\nexport const d = css(nested);",
+ "import { css } from '@emotion/react';\nconst gap = 8;\nconst label = 'a';\nexport const a = css({ padding: gap, content: label, margin: label });",
+ "import styled from '@emotion/styled';\nconst gap = 8;\nexport const A = styled.div({ margin: gap, flex: gap, zIndex: gap });\nexport const B = styled('div')({ margin: gap });",
+ "import styled, { css } from 'styled-components';\nconst gap = 8;\nexport const A = styled.div({ margin: gap });\nexport const B = css({ margin: gap });",
+ "import { Global } from '@emotion/react';\nconst gap = 8;\nexport const App = () => ;",
+ "import { ClassNames } from '@emotion/react';\nconst gap = 8;\nexport const App = () => {({ css }) => };",
+ "import { style } from '@vanilla-extract/css';\nconst gap = 8;\nexport const a = style({ margin: gap, lineHeight: gap, vars: { x: gap } });",
+ "import { css } from '@emotion/react';\nconst gap = 8;\nexport const App = () => ;",
+ "import { css } from '@devup-ui/react';\nconst gap = 8;\nexport const a = css({ padding: gap, margin: 8 });",
+ "import { css } from '@emotion/react';\nexport const a = css({ padding: unknown, margin: 8 });",
+ ],
+ SIZE_MODULES
+ ));
+ }
+
#[test]
#[serial]
fn test_emotion_cx_takes_class_names() {
@@ -23856,6 +24017,21 @@ export const j = cx({ a: null, b: 'x', c: '', d: 0, e: undefined, f: 2, g: false
));
}
+ #[test]
+ #[serial]
+ fn test_emotion_rules_read_imported_constants_as_pixels() {
+ assert_debug_snapshot!(library_outputs(
+ &[
+ "import { css } from '@emotion/react';\nimport { SIZE, ZERO, LINE, RULES, NAME } from './sizes';\nexport const a = css({ padding: SIZE, margin: ZERO, lineHeight: LINE, p: SIZE, content: NAME });\nexport const b = css(RULES);\nexport const c = css({ ...RULES, margin: SIZE });",
+ "import styled from '@emotion/styled';\nimport { SIZE } from './sizes';\nexport const A = styled.div({ margin: SIZE });",
+ "import { Global } from '@emotion/react';\nimport { SIZE } from './sizes';\nexport const App = () => ;",
+ "import { style } from '@vanilla-extract/css';\nimport { SIZE } from './sizes';\nexport const a = style({ margin: SIZE });",
+ "import { css } from '@devup-ui/react';\nimport { SIZE } from './sizes';\nexport const a = css({ padding: SIZE });",
+ ],
+ SIZE_MODULES
+ ));
+ }
+
#[test]
#[serial]
fn test_emotion_merge_splits_what_it_can() {
@@ -23875,6 +24051,25 @@ export const k = (cls) => merge([cls]);"
));
}
+ #[test]
+ #[serial]
+ fn test_emotion_css_text_composes_mixins_by_styles() {
+ assert_debug_snapshot!(library_outputs(
+ &[
+ "import { css } from '@emotion/react';\nconst mixin = css`color: red; margin: 1px;`;\nexport const a = css`${mixin}; color: blue;`;",
+ "import { css } from '@emotion/react';\nconst mixin = css({ color: 'red', margin: 1 });\nexport const a = css`color: green; ${mixin}; padding: 2px;`;",
+ "import { css } from '@emotion/react';\nconst mixin = css`color: red;`;\nconst more = css`${mixin}; margin: 2px;`;\nexport const a = css`${more}; color: blue;`;\nexport const b = css(more, { color: 'green' });",
+ "import { css } from '@emotion/react';\nexport const a = ({ mixin }) => css`${mixin}; color: blue;`;",
+ "import { css } from '@emotion/react';\nconst mixin = css`color: red;`;\nexport const a = ({ other }) => css`${mixin}; ${other}; color: blue;`;",
+ "import { css } from '@emotion/react';\nexport const a = css`${css({ color: 'red' })}; color: blue;`;",
+ "import { css } from '@emotion/react';\nexport const a = ({ on }) => css`${on && css({ color: 'red', margin: 1 })}; color: blue;`;",
+ "import { css } from '@emotion/react';\nexport const a = css`${css`color: red;`}; color: blue;`;",
+ "import { css } from '@emotion/react';\nexport const a = ({ on }) => css`${on ? css`color: red;` : css({ margin: 1 })}; color: blue;`;",
+ ],
+ SIZE_MODULES
+ ));
+ }
+
#[test]
#[serial]
fn test_emotion_cx_and_merge_report_what_they_cannot_compile() {
@@ -23929,6 +24124,27 @@ export const k = (cls) => merge([cls]);"
}
}
+ #[test]
+ #[serial]
+ fn test_emotion_css_flattens_inline_css_calls() {
+ assert_debug_snapshot!(library_outputs(
+ &[
+ "import { css } from '@emotion/react';\nexport const a = css(css({ color: 'red' }), { color: 'blue' });",
+ "import { css } from '@emotion/react';\nconst base = css({ color: 'green', margin: 4 });\nexport const a = (cond) => css(cond && css({ color: 'red' }), base);",
+ "import { css } from '@emotion/react';\nexport const a = (cond) => css(cond ? css({ color: 'red' }) : null, { color: 'blue' });",
+ "import { css } from '@emotion/react';\nexport const a = (cond) => css(cond || css({ color: 'red' }), { margin: 1 });",
+ "import { css } from '@emotion/react';\nexport const a = css([css({ color: 'red' }), [css({ margin: 1 }), { color: 'blue' }]]);",
+ "import { css } from '@emotion/react';\nexport const a = css(css(css({ color: 'red' }), { margin: 1 }), { color: 'blue' });",
+ "import { css } from '@emotion/react';\nexport const a = (cond) => css(cond && [css({ color: 'red' }), { margin: 1 }], { color: 'blue' });",
+ "import { css } from '@emotion/react';\nexport const a = (rest) => css(css(...rest), { color: 'blue' });",
+ "import { css } from '@emotion/react';\nexport const a = (cond, cls) => css(css({ color: 'red' }), cls, cond && css({ color: 'blue' }));",
+ "import { css } from '@emotion/react';\nexport const a = css(css({ color: 'red' }));",
+ "import { css } from '@devup-ui/react';\nexport const a = css(css({ color: 'red' }), { color: 'blue' });",
+ ],
+ SIZE_MODULES
+ ));
+ }
+
#[test]
#[serial]
fn test_emotion_class_calls_follow_the_package_namespace_and_aliases() {
diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_css_flattens_inline_css_calls.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_css_flattens_inline_css_calls.snap
new file mode 100644
index 000000000..10b1d1f72
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__emotion_css_flattens_inline_css_calls.snap
@@ -0,0 +1,50 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "library_outputs(&[\"import { css } from '@emotion/react';\\nexport const a = css(css({ color: 'red' }), { color: 'blue' });\",\n\"import { css } from '@emotion/react';\\nconst base = css({ color: 'green', margin: 4 });\\nexport const a = (cond) => css(cond && css({ color: 'red' }), base);\",\n\"import { css } from '@emotion/react';\\nexport const a = (cond) => css(cond ? css({ color: 'red' }) : null, { color: 'blue' });\",\n\"import { css } from '@emotion/react';\\nexport const a = (cond) => css(cond || css({ color: 'red' }), { margin: 1 });\",\n\"import { css } from '@emotion/react';\\nexport const a = css([css({ color: 'red' }), [css({ margin: 1 }), { color: 'blue' }]]);\",\n\"import { css } from '@emotion/react';\\nexport const a = css(css(css({ color: 'red' }), { margin: 1 }), { color: 'blue' });\",\n\"import { css } from '@emotion/react';\\nexport const a = (cond) => css(cond && [css({ color: 'red' }), { margin: 1 }], { color: 'blue' });\",\n\"import { css } from '@emotion/react';\\nexport const a = (rest) => css(css(...rest), { color: 'blue' });\",\n\"import { css } from '@emotion/react';\\nexport const a = (cond, cls) => css(css({ color: 'red' }), cls, cond && css({ color: 'blue' }));\",\n\"import { css } from '@emotion/react';\\nexport const a = css(css({ color: 'red' }));\",\n\"import { css } from '@devup-ui/react';\\nexport const a = css(css({ color: 'red' }), { color: 'blue' });\",],\nSIZE_MODULES)"
+---
+[
+ (
+ "import { css } from '@emotion/react';\nexport const a = css(css({ color: 'red' }), { color: 'blue' });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = \\\"a\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nconst base = css({ color: 'green', margin: 4 });\nexport const a = (cond) => css(cond && css({ color: 'red' }), base);",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"4px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst base = \\\"a b\\\";\\nexport const a = (cond) => \\\"b a\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = (cond) => css(cond ? css({ color: 'red' }) : null, { color: 'blue' });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = (cond) => \\\"a\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = (cond) => css(cond || css({ color: 'red' }), { margin: 1 });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = (cond) => `${cond ? typeof cond === \\\"string\\\" ? cond : \\\"\\\" : \\\"\\\"} ${`b ${cond ? \\\"\\\" : \\\"a\\\"}`}`;\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = css([css({ color: 'red' }), [css({ margin: 1 }), { color: 'blue' }]]);",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = \\\"a b\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = css(css(css({ color: 'red' }), { margin: 1 }), { color: 'blue' });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = \\\"a b\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = (cond) => css(cond && [css({ color: 'red' }), { margin: 1 }], { color: 'blue' });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = (cond) => `b ${cond ? \\\"a\\\" : \\\"\\\"}`;\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = (rest) => css(css(...rest), { color: 'blue' });",
+ "Error: /src/App.tsx:2:32: Cannot compose `...rest` at build time: each style must be a rule object, a class, or a condition choosing between them",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = (cond, cls) => css(css({ color: 'red' }), cls, cond && css({ color: 'blue' }));",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = (cond, cls) => `${cls} ${cond ? \\\"a\\\" : \\\"b\\\"}`;\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = css(css({ color: 'red' }));",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = \\\"a\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@devup-ui/react';\nexport const a = css(css({ color: 'red' }), { color: 'blue' });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = \\\"a\\\";\\n\" }",
+ ),
+]
diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_css_text_composes_mixins_by_styles.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_css_text_composes_mixins_by_styles.snap
new file mode 100644
index 000000000..9c407a429
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__emotion_css_text_composes_mixins_by_styles.snap
@@ -0,0 +1,42 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "library_outputs(&[\"import { css } from '@emotion/react';\\nconst mixin = css`color: red; margin: 1px;`;\\nexport const a = css`${mixin}; color: blue;`;\",\n\"import { css } from '@emotion/react';\\nconst mixin = css({ color: 'red', margin: 1 });\\nexport const a = css`color: green; ${mixin}; padding: 2px;`;\",\n\"import { css } from '@emotion/react';\\nconst mixin = css`color: red;`;\\nconst more = css`${mixin}; margin: 2px;`;\\nexport const a = css`${more}; color: blue;`;\\nexport const b = css(more, { color: 'green' });\",\n\"import { css } from '@emotion/react';\\nexport const a = ({ mixin }) => css`${mixin}; color: blue;`;\",\n\"import { css } from '@emotion/react';\\nconst mixin = css`color: red;`;\\nexport const a = ({ other }) => css`${mixin}; ${other}; color: blue;`;\",\n\"import { css } from '@emotion/react';\\nexport const a = css`${css({ color: 'red' })}; color: blue;`;\",\n\"import { css } from '@emotion/react';\\nexport const a = ({ on }) => css`${on && css({ color: 'red', margin: 1 })}; color: blue;`;\",\n\"import { css } from '@emotion/react';\\nexport const a = css`${css`color: red;`}; color: blue;`;\",\n\"import { css } from '@emotion/react';\\nexport const a = ({ on }) => css`${on ? css`color: red;` : css({ margin: 1 })}; color: blue;`;\",],\nSIZE_MODULES)"
+---
+[
+ (
+ "import { css } from '@emotion/react';\nconst mixin = css`color: red; margin: 1px;`;\nexport const a = css`${mixin}; color: blue;`;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst mixin = \\\"a b\\\";\\nexport const a = \\\"a c\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nconst mixin = css({ color: 'red', margin: 1 });\nexport const a = css`color: green; ${mixin}; padding: 2px;`;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"2px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst mixin = \\\"a b\\\";\\nexport const a = \\\"b a c\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nconst mixin = css`color: red;`;\nconst more = css`${mixin}; margin: 2px;`;\nexport const a = css`${more}; color: blue;`;\nexport const b = css(more, { color: 'green' });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"green\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"2px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst mixin = \\\"a\\\";\\nconst more = \\\"a b\\\";\\nexport const a = \\\"b c\\\";\\nexport const b = \\\"b d\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = ({ mixin }) => css`${mixin}; color: blue;`;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = ({ mixin }) => `a ${mixin}`;\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nconst mixin = css`color: red;`;\nexport const a = ({ other }) => css`${mixin}; ${other}; color: blue;`;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst mixin = \\\"a\\\";\\nexport const a = ({ other }) => `b ${other}`;\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = css`${css({ color: 'red' })}; color: blue;`;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = \\\"a\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = ({ on }) => css`${on && css({ color: 'red', margin: 1 })}; color: blue;`;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = ({ on }) => `b ${on ? \\\"a\\\" : \\\"\\\"}`;\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = css`${css`color: red;`}; color: blue;`;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = \\\"a b\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = ({ on }) => css`${on ? css`color: red;` : css({ margin: 1 })}; color: blue;`;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"color\", value: \"blue\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"color\", value: \"red\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nexport const a = ({ on }) => `${on ? \\\"a\\\" : \\\"\\\"} ${`c ${on ? \\\"\\\" : \\\"b\\\"}`}`;\\n\" }",
+ ),
+]
diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_rules_read_imported_constants_as_pixels.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_rules_read_imported_constants_as_pixels.snap
new file mode 100644
index 000000000..cd119456f
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__emotion_rules_read_imported_constants_as_pixels.snap
@@ -0,0 +1,26 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "library_outputs(&[\"import { css } from '@emotion/react';\\nimport { SIZE, ZERO, LINE, RULES, NAME } from './sizes';\\nexport const a = css({ padding: SIZE, margin: ZERO, lineHeight: LINE, p: SIZE, content: NAME });\\nexport const b = css(RULES);\\nexport const c = css({ ...RULES, margin: SIZE });\",\n\"import styled from '@emotion/styled';\\nimport { SIZE } from './sizes';\\nexport const A = styled.div({ margin: SIZE });\",\n\"import { Global } from '@emotion/react';\\nimport { SIZE } from './sizes';\\nexport const App = () => ;\",\n\"import { style } from '@vanilla-extract/css';\\nimport { SIZE } from './sizes';\\nexport const a = style({ margin: SIZE });\",\n\"import { css } from '@devup-ui/react';\\nimport { SIZE } from './sizes';\\nexport const a = css({ padding: SIZE });\",],\nSIZE_MODULES)"
+---
+[
+ (
+ "import { css } from '@emotion/react';\nimport { SIZE, ZERO, LINE, RULES, NAME } from './sizes';\nexport const a = css({ padding: SIZE, margin: ZERO, lineHeight: LINE, p: SIZE, content: NAME });\nexport const b = css(RULES);\nexport const c = css({ ...RULES, margin: SIZE });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"content\", value: \"big\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"gap\", value: \"12px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"line-height\", value: \"2\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"0\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"12px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"12px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"48px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"5px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nimport { SIZE, ZERO, LINE, RULES, NAME } from \\\"./sizes\\\";\\nexport const a = \\\"a b c d e\\\";\\nexport const b = \\\"f c g\\\";\\nexport const c = \\\"f c g h\\\";\\n\" }",
+ ),
+ (
+ "import styled from '@emotion/styled';\nimport { SIZE } from './sizes';\nexport const A = styled.div({ margin: SIZE });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"margin\", value: \"12px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nimport { forwardRef as __devupForwardRef } from \\\"react\\\";\\nimport { SIZE } from \\\"./sizes\\\";\\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \\\"div\\\", forwardedAs, ...rest }) => __devupDom)(rest)).filter(([k]) => k[0] !== \\\"$\\\"))} as={forwardedAs} className={[\\\"a--a a\\\", className].filter(Boolean).join(\\\" \\\")} style={style} />)({\\n\\t...__devupRefProps,\\n\\tref: __devupRef\\n}));\\n\" }",
+ ),
+ (
+ "import { Global } from '@emotion/react';\nimport { SIZE } from './sizes';\nexport const App = () => ;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"margin\", value: \"12px\", level: 0, selector: Some(Global(\"body\", \"/src/App.tsx\")), style_order: Some(0), layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nimport { Global } from \\\"@devup-ui/react/compat\\\";\\nimport { SIZE } from \\\"./sizes\\\";\\nexport const App = () => ;\\n\" }",
+ ),
+ (
+ "import { style } from '@vanilla-extract/css';\nimport { SIZE } from './sizes';\nexport const a = style({ margin: SIZE });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"margin\", value: \"12px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nimport { SIZE } from \\\"./sizes\\\";\\nexport const a = \\\"a\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@devup-ui/react';\nimport { SIZE } from './sizes';\nexport const a = css({ padding: SIZE });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"padding\", value: \"48px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nimport { SIZE } from \\\"./sizes\\\";\\nexport const a = \\\"a\\\";\\n\" }",
+ ),
+]
diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_rules_read_local_constants_as_pixels.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_rules_read_local_constants_as_pixels.snap
new file mode 100644
index 000000000..1b8b7aff1
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__emotion_rules_read_local_constants_as_pixels.snap
@@ -0,0 +1,54 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "library_outputs(&[\"import { css } from '@emotion/react';\\nconst gap = 8;\\nconst ZERO = 0;\\nconst LINE = 2;\\nexport const a = css({ padding: gap, margin: ZERO, lineHeight: LINE, opacity: LINE, p: gap, mx: gap });\",\n\"import { css } from '@emotion/react';\\nconst gap = 8;\\nexport const a = css({ '&:hover': { top: -gap, left: gap * 2 }, width: gap > 4 ? gap : 1, height: gap || 3, minWidth: (gap) });\",\n\"import { css } from '@emotion/react';\\nconst gap = 8;\\nconst rules = { padding: gap, lineHeight: 2, vars: { margin: 3 } };\\nconst nested = { '&:hover': rules };\\nexport const a = css(rules);\\nexport const b = css({ ...rules, margin: 1 });\\nexport const c = css({ '&:focus': rules });\\nexport const d = css(nested);\",\n\"import { css } from '@emotion/react';\\nconst gap = 8;\\nconst label = 'a';\\nexport const a = css({ padding: gap, content: label, margin: label });\",\n\"import styled from '@emotion/styled';\\nconst gap = 8;\\nexport const A = styled.div({ margin: gap, flex: gap, zIndex: gap });\\nexport const B = styled('div')({ margin: gap });\",\n\"import styled, { css } from 'styled-components';\\nconst gap = 8;\\nexport const A = styled.div({ margin: gap });\\nexport const B = css({ margin: gap });\",\n\"import { Global } from '@emotion/react';\\nconst gap = 8;\\nexport const App = () => ;\",\n\"import { ClassNames } from '@emotion/react';\\nconst gap = 8;\\nexport const App = () => {({ css }) => };\",\n\"import { style } from '@vanilla-extract/css';\\nconst gap = 8;\\nexport const a = style({ margin: gap, lineHeight: gap, vars: { x: gap } });\",\n\"import { css } from '@emotion/react';\\nconst gap = 8;\\nexport const App = () => ;\",\n\"import { css } from '@devup-ui/react';\\nconst gap = 8;\\nexport const a = css({ padding: gap, margin: 8 });\",\n\"import { css } from '@emotion/react';\\nexport const a = css({ padding: unknown, margin: 8 });\",],\nSIZE_MODULES)"
+---
+[
+ (
+ "import { css } from '@emotion/react';\nconst gap = 8;\nconst ZERO = 0;\nconst LINE = 2;\nexport const a = css({ padding: gap, margin: ZERO, lineHeight: LINE, opacity: LINE, p: gap, mx: gap });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"line-height\", value: \"2\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"0\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin-left\", value: \"32px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin-right\", value: \"32px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"opacity\", value: \"2\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"32px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst gap = 8;\\nconst ZERO = 0;\\nconst LINE = 2;\\nexport const a = \\\"a b c d e f g\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nconst gap = 8;\nexport const a = css({ '&:hover': { top: -gap, left: gap * 2 }, width: gap > 4 ? gap : 1, height: gap || 3, minWidth: (gap) });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"height\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"left\", value: \"16px\", level: 0, selector: Some(Selector(\"&:hover\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"min-width\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"top\", value: \"-8px\", level: 0, selector: Some(Selector(\"&:hover\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"width\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"width\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst gap = 8;\\nexport const a = `a b e f ${8 > 4 ? \\\"c\\\" : \\\"d\\\"}`;\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nconst gap = 8;\nconst rules = { padding: gap, lineHeight: 2, vars: { margin: 3 } };\nconst nested = { '&:hover': rules };\nexport const a = css(rules);\nexport const b = css({ ...rules, margin: 1 });\nexport const c = css({ '&:focus': rules });\nexport const d = css(nested);",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"line-height\", value: \"2\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"line-height\", value: \"2\", level: 0, selector: Some(Selector(\"&:hover\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"line-height\", value: \"2\", level: 0, selector: Some(Selector(\"&:focus\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"12px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"12px\", level: 0, selector: Some(Selector(\"&:hover\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"12px\", level: 0, selector: Some(Selector(\"&:focus\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"1px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"8px\", level: 0, selector: Some(Selector(\"&:hover\")), style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"8px\", level: 0, selector: Some(Selector(\"&:focus\")), style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst gap = 8;\\nconst rules = {\\n\\tpadding: gap,\\n\\tlineHeight: 2,\\n\\tvars: { margin: 3 }\\n};\\nconst nested = { \\\"&:hover\\\": rules };\\nexport const a = \\\"a b c\\\";\\nexport const b = \\\"a b c d\\\";\\nexport const c = \\\"e f g\\\";\\nexport const d = \\\"h i j\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nconst gap = 8;\nconst label = 'a';\nexport const a = css({ padding: gap, content: label, margin: label });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"content\", value: \"a\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"a\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst gap = 8;\\nconst label = \\\"a\\\";\\nexport const a = \\\"a b c\\\";\\n\" }",
+ ),
+ (
+ "import styled from '@emotion/styled';\nconst gap = 8;\nexport const A = styled.div({ margin: gap, flex: gap, zIndex: gap });\nexport const B = styled('div')({ margin: gap });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"flex\", value: \"8\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"z-index\", value: \"8\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nimport { forwardRef as __devupForwardRef } from \\\"react\\\";\\nconst gap = 8;\\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \\\"div\\\", forwardedAs, ...rest }) => __devupDom)(rest)).filter(([k]) => k[0] !== \\\"$\\\"))} as={forwardedAs} className={[\\\"a--a a b c\\\", className].filter(Boolean).join(\\\" \\\")} style={style} />)({\\n\\t...__devupRefProps,\\n\\tref: __devupRef\\n}));\\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \\\"div\\\", forwardedAs, ...rest }) => __devupDom)(rest)).filter(([k]) => k[0] !== \\\"$\\\"))} as={forwardedAs} className={[\\\"a--b a\\\", className].filter(Boolean).join(\\\" \\\")} style={style} />)({\\n\\t...__devupRefProps,\\n\\tref: __devupRef\\n}));\\n\" }",
+ ),
+ (
+ "import styled, { css } from 'styled-components';\nconst gap = 8;\nexport const A = styled.div({ margin: gap });\nexport const B = css({ margin: gap });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"margin\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nimport { forwardRef as __devupForwardRef } from \\\"react\\\";\\nconst gap = 8;\\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \\\"div\\\", forwardedAs, ...rest }) => __devupDom)(rest)).filter(([k]) => k[0] !== \\\"$\\\"))} as={forwardedAs} className={[\\\"a--a a\\\", className].filter(Boolean).join(\\\" \\\")} style={style} />)({\\n\\t...__devupRefProps,\\n\\tref: __devupRef\\n}));\\nexport const B = \\\"a\\\";\\n\" }",
+ ),
+ (
+ "import { Global } from '@emotion/react';\nconst gap = 8;\nexport const App = () => ;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"line-height\", value: \"2\", level: 0, selector: Some(Global(\"body\", \"/src/App.tsx\")), style_order: Some(0), layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"8px\", level: 0, selector: Some(Global(\"body\", \"/src/App.tsx\")), style_order: Some(0), layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nimport { Global } from \\\"@devup-ui/react/compat\\\";\\nconst gap = 8;\\nexport const App = () => ;\\n\" }",
+ ),
+ (
+ "import { ClassNames } from '@emotion/react';\nconst gap = 8;\nexport const App = () => {({ css }) => };",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"margin\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst gap = 8;\\nexport const App = () => ;\\n\" }",
+ ),
+ (
+ "import { style } from '@vanilla-extract/css';\nconst gap = 8;\nexport const a = style({ margin: gap, lineHeight: gap, vars: { x: gap } });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"line-height\", value: \"8\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"margin\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"x\", value: \"32px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst gap = 8;\\nexport const a = \\\"a b c\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nconst gap = 8;\nexport const App = () => ;",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"margin\", value: \"8px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst gap = 8;\\nexport const App = () => ;\\n\" }",
+ ),
+ (
+ "import { css } from '@devup-ui/react';\nconst gap = 8;\nexport const a = css({ padding: gap, margin: 8 });",
+ "ToBTreeSet { styles: {Static(ExtractStaticStyle { property: \"margin\", value: \"32px\", level: 0, selector: None, style_order: None, layer: None }), Static(ExtractStaticStyle { property: \"padding\", value: \"32px\", level: 0, selector: None, style_order: None, layer: None })}, code: \"import \\\"@devup-ui/react/devup-ui.css\\\";\\nconst gap = 8;\\nexport const a = \\\"a b\\\";\\n\" }",
+ ),
+ (
+ "import { css } from '@emotion/react';\nexport const a = css({ padding: unknown, margin: 8 });",
+ "Error: /src/App.tsx:2:18: `css()` cannot use `unknown` at build time: its values must be literals, theme tokens or constants, or be computed from them",
+ ),
+]
diff --git a/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap b/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap
index 8758d1e79..d803008bc 100644
--- a/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap
+++ b/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap
@@ -99,5 +99,5 @@ ToBTreeSet {
},
),
},
- code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst C = \"red\";\nconst O = .5;\nconst SEL = \".x\";\nconst SNIPPET = \"display: flex;\";\nconst mixin = \"a-a\";\n;\n;\nexport const k = \"a-b\";\nexport const c = \"a-c a-d\";\nexport const d = `a-e ${mixin}`;\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[`a--a ${`a-d ${mixin} ${((p) => p.active && mixin)(rest) || \"\"}`}`, className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a--b a-f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n",
+ code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nconst C = \"red\";\nconst O = .5;\nconst SEL = \".x\";\nconst SNIPPET = \"display: flex;\";\nconst mixin = \"a-a\";\n;\n;\nexport const k = \"a-b\";\nexport const c = \"a-c a-d\";\nexport const d = \"a-a a-e\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[`a--a ${`a-d ${mixin} ${((p) => p.active && mixin)(rest) || \"\"}`}`, className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\nexport const B = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a--b a-f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n",
}
diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs
index a65902d2e..8fc5e94dc 100644
--- a/libs/extractor/src/visit.rs
+++ b/libs/extractor/src/visit.rs
@@ -477,7 +477,11 @@ impl<'a> DevupVisitor<'a> {
/// `css(...)` composing a class whose styles the build knows: the parts'
/// styles merge, a later declaration replacing an earlier one. `None` when
/// no part is such a class, or a part is one composing does not read.
- fn compose_known_styles(&mut self, call: &CallExpression<'a>) -> Option> {
+ fn compose_known_styles(
+ &mut self,
+ call: &CallExpression<'a>,
+ flattened: bool,
+ ) -> Option> {
let arguments: Vec<&Expression<'a>> = call
.arguments
.iter()
@@ -486,9 +490,10 @@ impl<'a> DevupVisitor<'a> {
argument => argument.to_expression(),
})
.collect();
- if !arguments
- .iter()
- .any(|argument| self.reads_known_styles(argument))
+ if !flattened
+ && !arguments
+ .iter()
+ .any(|argument| self.reads_known_styles(argument))
{
return None;
}
@@ -505,6 +510,106 @@ impl<'a> DevupVisitor<'a> {
Some(result)
}
+ /// Whether `callee` is a call of `css()`
+ fn is_css(&self, callee: &Expression<'a>) -> bool {
+ self.util_type(callee)
+ .is_some_and(|util| matches!(util.as_ref(), UtilType::Css))
+ }
+
+ /// `part` of what `css()` or CSS text composes, with each inline `css()`
+ /// call in it written as the array of what it composes, so that what it
+ /// composes is ordered with the rest rather than compiled to a class of
+ /// its own, and with `templates`, each inline CSS text in it written as an
+ /// array of itself, which keeps the class it compiles to from being read as
+ /// CSS text. Whether a part was written so.
+ fn flatten_css_calls(&self, part: &mut Expression<'a>, templates: bool) -> bool {
+ if templates
+ && matches!(unwrap_syntax_only(part), Expression::TaggedTemplateExpression(tag)
+ if self.is_css(&tag.tag))
+ {
+ let template = part.take_in(&self.ast);
+ *part = Expression::new_array_expression(
+ SPAN,
+ oxc_allocator::Vec::from_array_in([template.into()], &self.ast),
+ &self.ast,
+ );
+ return true;
+ }
+ let array = match unwrap_syntax_only_mut(part) {
+ Expression::ArrayExpression(array) => {
+ return array.elements.iter_mut().fold(false, |flattened, part| {
+ part.as_expression_mut()
+ .is_some_and(|part| self.flatten_css_calls(part, templates))
+ | flattened
+ });
+ }
+ Expression::ConditionalExpression(conditional) => {
+ return self.flatten_css_calls(&mut conditional.consequent, templates)
+ | self.flatten_css_calls(&mut conditional.alternate, templates);
+ }
+ Expression::LogicalExpression(logical) => {
+ return (logical.operator != LogicalOperator::And
+ && self.flatten_css_calls(&mut logical.left, templates))
+ | self.flatten_css_calls(&mut logical.right, templates);
+ }
+ Expression::CallExpression(call)
+ if self.is_css(&call.callee)
+ && call
+ .arguments
+ .iter()
+ .all(|argument| !matches!(argument, Argument::SpreadElement(_))) =>
+ {
+ let elements = call
+ .arguments
+ .drain(..)
+ .map(|argument| argument.into_expression().into());
+ Expression::new_array_expression(
+ SPAN,
+ oxc_allocator::Vec::from_iter_in(elements, &self.ast),
+ &self.ast,
+ )
+ }
+ _ => return false,
+ };
+ *part = array;
+ self.flatten_css_calls(part, templates);
+ true
+ }
+
+ /// `css` with CSS text composing mixins whose styles the build knows, as
+ /// the parts around them: the styles merge, a later declaration replacing
+ /// an earlier one. `None` when no mixin is such a class, or the text is
+ /// not read as parts.
+ fn compose_template(
+ &mut self,
+ tag: &oxc_ast::ast::TaggedTemplateExpression<'a>,
+ flattened: bool,
+ ) -> Option> {
+ let texts = template_parts(&self.ast, &tag.quasi, true).ok()?;
+ let is_mixin = |text: &Expression<'a>| !matches!(text, Expression::TemplateLiteral(_));
+ if !texts
+ .iter()
+ .any(|text| is_mixin(text) && (flattened || self.reads_known_styles(text)))
+ {
+ return None;
+ }
+ let mut parts = Vec::new();
+ for text in &texts {
+ let kind = if is_mixin(text) {
+ Text::Classes
+ } else {
+ Text::Rules
+ };
+ self.known_parts(text, &mut parts, kind)?;
+ }
+ let offset = tag.span.start;
+ let (result, known) = self.composed_class(offset, parts);
+ if let Some(known) = known {
+ self.css_styles = Some((offset, known));
+ }
+ Some(result)
+ }
+
/// The classes `parts`, composed at `offset`, give, with the styles they
/// always set when no class the build does not know joins them
fn composed_class(
@@ -727,9 +832,7 @@ impl<'a> DevupVisitor<'a> {
Expression::CallExpression(call) if self.class_names_text(&call.callee).is_some() => {
self.folded_side(expression, text)
}
- Expression::ArrayExpression(_) if !self.class_names_scope.is_empty() => {
- self.folded_side(expression, text)
- }
+ Expression::ArrayExpression(_) => self.folded_side(expression, text),
_ => None,
}
}
@@ -796,7 +899,9 @@ impl<'a> DevupVisitor<'a> {
css_prop_error(element, &code, requirement.unwrap_or(STYLE_OBJECT)),
));
}
- } else if let Some(value) = runtime_value(&styles) {
+ } else if let Some((offset, value)) =
+ crate::dead_properties::located_value(&styles, offset, false)
+ {
self.errors.push((offset, unused_error("css", &value)));
}
if let Some(order) = style_order {
@@ -2960,6 +3065,25 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
if let Some(error) = forward_error {
self.errors.push(error);
}
+ let flattened = match it {
+ Expression::CallExpression(call) if self.is_css(&call.callee) => call
+ .arguments
+ .iter_mut()
+ .fold(false, |flattened, argument| {
+ argument
+ .as_expression_mut()
+ .is_some_and(|part| self.flatten_css_calls(part, false))
+ | flattened
+ }),
+ Expression::TaggedTemplateExpression(tag) if self.is_css(&tag.tag) => tag
+ .quasi
+ .expressions
+ .iter_mut()
+ .fold(false, |flattened, part| {
+ self.flatten_css_calls(part, true) | flattened
+ }),
+ _ => false,
+ };
walk_expression(self, it);
// Handle styled function calls
@@ -3536,7 +3660,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
&& self
.util_type(&call.callee)
.is_some_and(|util| matches!(util.as_ref(), UtilType::Css))
- && let Some(mut composed) = self.compose_known_styles(call)
+ && let Some(mut composed) = self.compose_known_styles(call, flattened)
{
if let Expression::StringLiteral(text) = &mut composed {
text.span = call.span;
@@ -3841,13 +3965,20 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
*it = Expression::new_string_literal(SPAN, "", None, &self.ast);
return;
}
+ let composed = if matches!(r, UtilType::Css) {
+ self.compose_template(tag, flattened)
+ } else {
+ None
+ };
let mut build_css_str = || {
template_css_text(&tag.quasi, api).unwrap_or_else(|error| {
self.errors.push(error);
String::new()
})
};
- *it = if matches!(r, UtilType::Css) {
+ *it = if let Some(composed) = composed {
+ composed
+ } else if matches!(r, UtilType::Css) {
let TemplateStyles {
styles,
statements,
@@ -3866,12 +3997,28 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
self.errors
.push((expression.span().start, unplaced_error(expression)));
}
+ let known = statements.is_empty().then(|| {
+ let mut known = crate::composition::Composition::default();
+ known.apply(
+ &self.ast,
+ style_props
+ .iter()
+ .map(|prop| prop.clone_in(self.ast.allocator()))
+ .collect(),
+ );
+ known.unconditional()
+ });
let class_name = gen_class_names(
&self.ast,
&mut style_props,
None,
self.split_filename.as_deref(),
);
+ if let Some(Some(known)) = known
+ && matches!(class_name, Some(Expression::StringLiteral(_)))
+ {
+ self.css_styles = Some((tag.span.start, known));
+ }
for ex in style_props {
// every entry was built as `Static` above