Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_emotion_fallback_arrays.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "In rule objects written against Emotion, styled-components or vanilla-extract, a property whose value is an array of literals (`background: ['red', 'linear-gradient(...)']`) declares each value in order as a browser fallback, as those libraries do, instead of being read as Devup UI breakpoints; numbers get px as the library gives them. Arrays holding runtime values stay as written",
"date": "2026-10-01T00:00:00.000Z"
}
82 changes: 82 additions & 0 deletions libs/extractor/src/import_alias_visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ impl LibraryNumbers<'_> {
return;
}
match value {
Expression::ArrayExpression(array) => self.fallbacks(key, array),
Expression::ObjectExpression(_) => {
if key != "vars" {
self.pixelify(value);
Expand All @@ -126,6 +127,63 @@ impl LibraryNumbers<'_> {
}
}

impl LibraryNumbers<'_> {
/// `key: [a, b]` in these libraries declares `key` once per value, later
/// ones as fallbacks a browser keeps when it understands them, where Devup
/// 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<Vec<String>> = 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 {
return;
};
let property = kebab_property(key);
let joined = values.join(&format!(";{property}:"));
let span = array.span;
self.replacements.push((
span.start as usize,
span.end as usize,
format!("{joined:?}"),
));
}
}

/// `backgroundColor` as the CSS property `background-color`, `WebkitBoxShadow`
/// as `-webkit-box-shadow`
fn kebab_property(key: &str) -> String {
let mut property = String::with_capacity(key.len() + 4);
for (index, c) in key.chars().enumerate() {
if c.is_ascii_uppercase() {
if index > 0
|| matches!(key, k if k.starts_with("Webkit") || k.starts_with("Moz") || k.starts_with("Ms"))
{
property.push('-');
}
property.push(c.to_ascii_lowercase());
} else {
property.push(c);
}
}
property
}

impl<'a> Visit<'a> for LibraryNumbers<'_> {
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
let rules_at = match &call.callee {
Expand Down Expand Up @@ -861,6 +919,30 @@ mod tests {
));
}

// A property's array of values declares fallbacks in these libraries
#[test]
fn test_property_arrays_become_fallback_declarations() {
let code = transform_import_aliases(
"import { css } from '@emotion/react'
css({ background: ['red', 'linear-gradient(red, blue)'], WebkitBoxShadow: ['none', '0 0 1px red'], margin: [4, 0], zIndex: [1, 2], color: [x, 'red'], top: ['1px'] })",
"test.tsx",
"@devup-ui/react",
&HashMap::from([("@emotion/react".to_string(), ImportAlias::NamedToNamed)]),
);
for expected in [
r#"background: "red;background:linear-gradient(red, blue)""#,
r#"WebkitBoxShadow: "none;-webkit-box-shadow:0 0 1px red""#,
r#"margin: "4px;margin:0""#,
r#"zIndex: "1;z-index:2""#,
"color: [x, 'red']",
"top: ['1px']",
] {
assert!(code.contains(expected), "{expected}\n{code}");
}
assert_eq!(kebab_property("MozAppearance"), "-moz-appearance");
assert_eq!(kebab_property("msFlex"), "ms-flex");
}

// Types are erased, so a type-only import never becomes a runtime import
#[test]
fn test_type_only_imports_stay_type_only() {
Expand Down
Loading