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
1 change: 1 addition & 0 deletions .changepacks/changepack_log_woHPUj5wH9fxS-ueBRZed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"bindings/devup-ui-wasm/package.json":"Patch"},"note":"Fix selector variable issue","date":"2026-04-08T12:05:26.100136600Z"}
64 changes: 48 additions & 16 deletions libs/extractor/src/extractor/extract_style_from_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,31 +397,48 @@ pub fn extract_style_from_expression<'a>(
} else {
match expression {
Expression::UnaryExpression(un) => ExtractResult {
// `name` is None only when this was reached through the `_xxx`
// selector recursion (see line 324). In that case the value
// cannot be statically extracted as a dynamic style because
// the pseudo-selector has no CSS property slot to bind a
// CSS variable to, so we return an empty result and let the
// caller drop the attribute (see issue with `_hover={var}`).
styles: if un.operator == UnaryOperator::Void {
vec![]
} else {
} else if let Some(name) = name {
vec![dynamic_style(
ast_builder,
name.unwrap(),
name,
expression,
level,
selector,
)]
} else {
vec![]
},
..ExtractResult::default()
},
// Each variant is kept on its own line so per-line coverage
// tools (tarpaulin on CI) can attribute the hit to the exact
// pattern being exercised. The body is flattened to a single
// `Option::map().unwrap_or_default()` chain to avoid an extra
// if/else branch region — `name == None` happens only under
// `_xxx={...}` pseudo-selector recursion, where no dynamic_style
// can be emitted because the selector has no CSS property slot.
Expression::BinaryExpression(_)
| Expression::StaticMemberExpression(_)
| Expression::CallExpression(_) => ExtractResult {
styles: vec![dynamic_style(
ast_builder,
name.unwrap(),
expression,
level,
selector,
)],
..ExtractResult::default()
},
| Expression::CallExpression(_) => name
.map(|name| ExtractResult {
styles: vec![dynamic_style(
ast_builder,
name,
expression,
level,
selector,
)],
..ExtractResult::default()
})
.unwrap_or_default(),
Expression::TSAsExpression(exp) => extract_style_from_expression(
ast_builder,
name,
Expand All @@ -434,6 +451,11 @@ pub fn extract_style_from_expression<'a>(
extract_style_from_member_expression(ast_builder, name, mem, level, selector)
}
Expression::TemplateLiteral(_) => ExtractResult {
// `typo == true` implies `name == Some("typography")` (set at
// line 337 inside an `if let Some(name) = name` block), so the
// typo branch is safe. The non-typo branch must handle the
// `name.is_none()` case (pseudo-selector recursion) by
// returning empty styles.
styles: if typo {
vec![ExtractStyleProp::Expression {
expression: ast_builder.expression_template_literal(
Expand Down Expand Up @@ -463,22 +485,30 @@ pub fn extract_style_from_expression<'a>(
),
styles: vec![],
}]
} else {
} else if let Some(name) = name {
vec![dynamic_style(
ast_builder,
name.unwrap(),
name,
expression,
level,
selector,
)]
} else {
vec![]
},
..ExtractResult::default()
},
Expression::Identifier(identifier) => {
// When `name` is `None` we are inside a pseudo-selector
// recursion (e.g. `_hover={someIdentifier}`). In that case
// the identifier is a black box (it may come from another
// module) and we cannot statically extract a style from it,
// so we skip extraction gracefully instead of panicking. The
// pseudo-selector attribute will be stripped by the visitor
// like any other non-extracted style prop.
if IGNORED_IDENTIFIERS.contains(&identifier.name.as_str()) {
ExtractResult::default()
} else {
let name = name.unwrap();
} else if let Some(name) = name {
if typo {
ExtractResult {
styles: vec![ExtractStyleProp::Expression {
Expand Down Expand Up @@ -530,6 +560,8 @@ pub fn extract_style_from_expression<'a>(
..ExtractResult::default()
}
}
} else {
ExtractResult::default()
}
}
Expression::LogicalExpression(logical) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,17 @@ pub(super) fn extract_style_from_member_expression<'a>(
);
}
}
// If `name` is None (pseudo-selector recursion) we cannot emit a
// dynamic_style because there is no CSS property slot to bind to.
// Fall back to an empty result in that case.
return ExtractResult {
props: None,
styles: etc
.map(|etc| {
.zip(name)
.map(|(etc, name)| {
vec![dynamic_style(
ast_builder,
name.unwrap(),
name,
&Expression::ComputedMemberExpression(
ast_builder.alloc_computed_member_expression(
SPAN,
Expand All @@ -94,23 +98,27 @@ pub(super) fn extract_style_from_member_expression<'a>(
let mut map = BTreeMap::new();
for (idx, p) in array.elements.iter_mut().enumerate() {
if let ArrayExpressionElement::SpreadElement(sp) = p {
map.insert(
idx.to_string(),
Box::new(dynamic_style(
ast_builder,
name.unwrap(),
&Expression::ComputedMemberExpression(
ast_builder.alloc_computed_member_expression(
SPAN,
sp.argument.clone_in(ast_builder.allocator),
mem_expression.clone_in(ast_builder.allocator),
false,
// Skip spread elements entirely when `name` is None — we
// can't synthesize a dynamic style without a prop name.
if let Some(name) = name {
map.insert(
idx.to_string(),
Box::new(dynamic_style(
ast_builder,
name,
&Expression::ComputedMemberExpression(
ast_builder.alloc_computed_member_expression(
SPAN,
sp.argument.clone_in(ast_builder.allocator),
mem_expression.clone_in(ast_builder.allocator),
false,
),
),
),
level,
&selector.clone(),
)),
);
level,
&selector.clone(),
)),
);
}
} else if let Some(p) = p.as_expression_mut() {
map.insert(
idx.to_string(),
Expand Down Expand Up @@ -162,11 +170,10 @@ pub(super) fn extract_style_from_member_expression<'a>(
}
}

match etc {
None => return ExtractResult::default(),
Some(etc) => ret.push(dynamic_style(
match (etc, name) {
(Some(etc), Some(name)) => ret.push(dynamic_style(
ast_builder,
name.unwrap(),
name,
&Expression::ComputedMemberExpression(
ast_builder.alloc_computed_member_expression(
SPAN,
Expand All @@ -178,6 +185,9 @@ pub(super) fn extract_style_from_member_expression<'a>(
level,
selector,
)),
// No spread fallback, or no prop name (pseudo-selector
// recursion): return empty instead of panicking.
_ => return ExtractResult::default(),
}
}

Expand Down Expand Up @@ -205,10 +215,14 @@ pub(super) fn extract_style_from_member_expression<'a>(
expression: mem_expression.clone_in(ast_builder.allocator),
map,
});
} else if let Expression::Identifier(_) = &mut mem.object {
} else if let Expression::Identifier(_) = &mut mem.object
&& let Some(name) = name
{
// When `name` is None we are in a pseudo-selector recursion and
// cannot emit a dynamic_style — skip gracefully.
ret.push(dynamic_style(
ast_builder,
name.unwrap(),
name,
&Expression::ComputedMemberExpression(ast_builder.alloc_computed_member_expression(
SPAN,
mem.object.clone_in(ast_builder.allocator),
Expand Down
Loading
Loading