Skip to content

Commit ddea431

Browse files
authored
Merge pull request #601 from dev-five-git/fix-selector-variable-issue
Fix selector variable issue
2 parents a458c89 + af5637a commit ddea431

18 files changed

Lines changed: 503 additions & 60 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"bindings/devup-ui-wasm/package.json":"Patch"},"note":"Fix selector variable issue","date":"2026-04-08T12:05:26.100136600Z"}

libs/extractor/src/extractor/extract_style_from_expression.rs

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -397,31 +397,48 @@ pub fn extract_style_from_expression<'a>(
397397
} else {
398398
match expression {
399399
Expression::UnaryExpression(un) => ExtractResult {
400+
// `name` is None only when this was reached through the `_xxx`
401+
// selector recursion (see line 324). In that case the value
402+
// cannot be statically extracted as a dynamic style because
403+
// the pseudo-selector has no CSS property slot to bind a
404+
// CSS variable to, so we return an empty result and let the
405+
// caller drop the attribute (see issue with `_hover={var}`).
400406
styles: if un.operator == UnaryOperator::Void {
401407
vec![]
402-
} else {
408+
} else if let Some(name) = name {
403409
vec![dynamic_style(
404410
ast_builder,
405-
name.unwrap(),
411+
name,
406412
expression,
407413
level,
408414
selector,
409415
)]
416+
} else {
417+
vec![]
410418
},
411419
..ExtractResult::default()
412420
},
421+
// Each variant is kept on its own line so per-line coverage
422+
// tools (tarpaulin on CI) can attribute the hit to the exact
423+
// pattern being exercised. The body is flattened to a single
424+
// `Option::map().unwrap_or_default()` chain to avoid an extra
425+
// if/else branch region — `name == None` happens only under
426+
// `_xxx={...}` pseudo-selector recursion, where no dynamic_style
427+
// can be emitted because the selector has no CSS property slot.
413428
Expression::BinaryExpression(_)
414429
| Expression::StaticMemberExpression(_)
415-
| Expression::CallExpression(_) => ExtractResult {
416-
styles: vec![dynamic_style(
417-
ast_builder,
418-
name.unwrap(),
419-
expression,
420-
level,
421-
selector,
422-
)],
423-
..ExtractResult::default()
424-
},
430+
| Expression::CallExpression(_) => name
431+
.map(|name| ExtractResult {
432+
styles: vec![dynamic_style(
433+
ast_builder,
434+
name,
435+
expression,
436+
level,
437+
selector,
438+
)],
439+
..ExtractResult::default()
440+
})
441+
.unwrap_or_default(),
425442
Expression::TSAsExpression(exp) => extract_style_from_expression(
426443
ast_builder,
427444
name,
@@ -434,6 +451,11 @@ pub fn extract_style_from_expression<'a>(
434451
extract_style_from_member_expression(ast_builder, name, mem, level, selector)
435452
}
436453
Expression::TemplateLiteral(_) => ExtractResult {
454+
// `typo == true` implies `name == Some("typography")` (set at
455+
// line 337 inside an `if let Some(name) = name` block), so the
456+
// typo branch is safe. The non-typo branch must handle the
457+
// `name.is_none()` case (pseudo-selector recursion) by
458+
// returning empty styles.
437459
styles: if typo {
438460
vec![ExtractStyleProp::Expression {
439461
expression: ast_builder.expression_template_literal(
@@ -463,22 +485,30 @@ pub fn extract_style_from_expression<'a>(
463485
),
464486
styles: vec![],
465487
}]
466-
} else {
488+
} else if let Some(name) = name {
467489
vec![dynamic_style(
468490
ast_builder,
469-
name.unwrap(),
491+
name,
470492
expression,
471493
level,
472494
selector,
473495
)]
496+
} else {
497+
vec![]
474498
},
475499
..ExtractResult::default()
476500
},
477501
Expression::Identifier(identifier) => {
502+
// When `name` is `None` we are inside a pseudo-selector
503+
// recursion (e.g. `_hover={someIdentifier}`). In that case
504+
// the identifier is a black box (it may come from another
505+
// module) and we cannot statically extract a style from it,
506+
// so we skip extraction gracefully instead of panicking. The
507+
// pseudo-selector attribute will be stripped by the visitor
508+
// like any other non-extracted style prop.
478509
if IGNORED_IDENTIFIERS.contains(&identifier.name.as_str()) {
479510
ExtractResult::default()
480-
} else {
481-
let name = name.unwrap();
511+
} else if let Some(name) = name {
482512
if typo {
483513
ExtractResult {
484514
styles: vec![ExtractStyleProp::Expression {
@@ -530,6 +560,8 @@ pub fn extract_style_from_expression<'a>(
530560
..ExtractResult::default()
531561
}
532562
}
563+
} else {
564+
ExtractResult::default()
533565
}
534566
}
535567
Expression::LogicalExpression(logical) => {

libs/extractor/src/extractor/extract_style_from_member_expression.rs

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,17 @@ pub(super) fn extract_style_from_member_expression<'a>(
6565
);
6666
}
6767
}
68+
// If `name` is None (pseudo-selector recursion) we cannot emit a
69+
// dynamic_style because there is no CSS property slot to bind to.
70+
// Fall back to an empty result in that case.
6871
return ExtractResult {
6972
props: None,
7073
styles: etc
71-
.map(|etc| {
74+
.zip(name)
75+
.map(|(etc, name)| {
7276
vec![dynamic_style(
7377
ast_builder,
74-
name.unwrap(),
78+
name,
7579
&Expression::ComputedMemberExpression(
7680
ast_builder.alloc_computed_member_expression(
7781
SPAN,
@@ -94,23 +98,27 @@ pub(super) fn extract_style_from_member_expression<'a>(
9498
let mut map = BTreeMap::new();
9599
for (idx, p) in array.elements.iter_mut().enumerate() {
96100
if let ArrayExpressionElement::SpreadElement(sp) = p {
97-
map.insert(
98-
idx.to_string(),
99-
Box::new(dynamic_style(
100-
ast_builder,
101-
name.unwrap(),
102-
&Expression::ComputedMemberExpression(
103-
ast_builder.alloc_computed_member_expression(
104-
SPAN,
105-
sp.argument.clone_in(ast_builder.allocator),
106-
mem_expression.clone_in(ast_builder.allocator),
107-
false,
101+
// Skip spread elements entirely when `name` is None — we
102+
// can't synthesize a dynamic style without a prop name.
103+
if let Some(name) = name {
104+
map.insert(
105+
idx.to_string(),
106+
Box::new(dynamic_style(
107+
ast_builder,
108+
name,
109+
&Expression::ComputedMemberExpression(
110+
ast_builder.alloc_computed_member_expression(
111+
SPAN,
112+
sp.argument.clone_in(ast_builder.allocator),
113+
mem_expression.clone_in(ast_builder.allocator),
114+
false,
115+
),
108116
),
109-
),
110-
level,
111-
&selector.clone(),
112-
)),
113-
);
117+
level,
118+
&selector.clone(),
119+
)),
120+
);
121+
}
114122
} else if let Some(p) = p.as_expression_mut() {
115123
map.insert(
116124
idx.to_string(),
@@ -162,11 +170,10 @@ pub(super) fn extract_style_from_member_expression<'a>(
162170
}
163171
}
164172

165-
match etc {
166-
None => return ExtractResult::default(),
167-
Some(etc) => ret.push(dynamic_style(
173+
match (etc, name) {
174+
(Some(etc), Some(name)) => ret.push(dynamic_style(
168175
ast_builder,
169-
name.unwrap(),
176+
name,
170177
&Expression::ComputedMemberExpression(
171178
ast_builder.alloc_computed_member_expression(
172179
SPAN,
@@ -178,6 +185,9 @@ pub(super) fn extract_style_from_member_expression<'a>(
178185
level,
179186
selector,
180187
)),
188+
// No spread fallback, or no prop name (pseudo-selector
189+
// recursion): return empty instead of panicking.
190+
_ => return ExtractResult::default(),
181191
}
182192
}
183193

@@ -205,10 +215,14 @@ pub(super) fn extract_style_from_member_expression<'a>(
205215
expression: mem_expression.clone_in(ast_builder.allocator),
206216
map,
207217
});
208-
} else if let Expression::Identifier(_) = &mut mem.object {
218+
} else if let Expression::Identifier(_) = &mut mem.object
219+
&& let Some(name) = name
220+
{
221+
// When `name` is None we are in a pseudo-selector recursion and
222+
// cannot emit a dynamic_style — skip gracefully.
209223
ret.push(dynamic_style(
210224
ast_builder,
211-
name.unwrap(),
225+
name,
212226
&Expression::ComputedMemberExpression(ast_builder.alloc_computed_member_expression(
213227
SPAN,
214228
mem.object.clone_in(ast_builder.allocator),

0 commit comments

Comments
 (0)