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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"apps/landing/package.json": "Patch"
},
"note": "Respect source-order property replacement across conditional spreads in styled rule objects, and document that styled callbacks cannot return runtime class names. No new build errors; existing located Cannot compose errors remain unchanged.",
"date": "2026-10-01T00:00:00.000Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
const G = styled(motion.div)`
color: red;
`
const H = styled(forwardRef(Field))({ bg: 'red' })

Check warning on line 30 in apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx

View workflow job for this annotation

GitHub Actions / publish

In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead
```

Any base renders: a tag, a component, a member such as `motion.div`, or any other value, read once where the component is declared. `null`, numbers, booleans and `undefined` never render, so they are build errors, as is a `styled` left uncompiled (`styled('div')` never given styles, `styled.div` exported bare): its import is removed, and that code would throw at runtime.
Expand All @@ -40,6 +40,8 @@

A style argument that cannot be known at build time — a function call the build cannot run, a spread or a computed key it cannot compute — is a build error naming it, rather than a silently missing style; a spread, a selector value or a computed key the module computes (`styled.div({ ...cardStyles(), _hover: hoverStyles })`) is computed, while the other values stay CSS variables set at runtime. An object constant, declared in the file or imported, reads as if written in place: `styled.div(baseStyles)`, `styled.div({ ...baseStyles, color: 'red' })`. A single argument that is not a rule object composes as a class, as several do; one holding rules the module computes (`const base = makeStyles()`, `styled.div(base)`) applies those rules. In the object form a value may read the theme (`(p) => p.theme.brand`); any other function of the props is a build error — write it in a template (`${(p) => p.color}`), where it becomes a CSS variable.

A styled callback cannot return a class name known only at runtime (`styled.div(p => { return p.cls; })`): the build reports a located `Cannot compose` error. A condition choosing between known classes or rule objects is supported.

Inside a template, an interpolation standing where a declaration would is a mixin: `${mixin}` and `${(p) => p.active && mixin}` compose their class with the component's. A literal or constant is written into the CSS wherever it stands — a value, a CSS snippet, a selector. Any other interpolation in a selector or a property name is a build error, as are runtime values in `css`, `keyframes` and global styles, which have no element to set a CSS variable on.

`.attrs()` and `.withConfig()` chain onto any factory. Attrs are merged over the props in order, as styled-components does — an object is spread, a function is called with the props; `.withConfig()` only tunes runtime behavior, so it is dropped:
Expand Down
43 changes: 36 additions & 7 deletions libs/extractor/src/extractor/extract_style_from_styled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use crate::{
extractor::{
ExtractResult,
extract_style_from_expression::{LiteralHandling, extract_style_from_expression},
styled_record::prepare,
styled_record_evaluation::bind_render,
},
gen_class_name::{gen_class_names, merge_expression_for_class_name},
gen_style::gen_styles,
Expand Down Expand Up @@ -184,6 +186,7 @@ pub fn extract_style_from_styled<'a>(
imports: &FxHashMap<String, ExportVariableKind>,
attrs: &[Expression<'a>],
) -> (ExtractResult<'a>, Expression<'a>, Vec<(u32, String)>) {
let original_scope = expression.clone_in(ast_builder.allocator());
let mut composed_classes = Vec::new();
let mut errors = Vec::new();
if let Expression::CallExpression(call) = expression
Expand Down Expand Up @@ -277,8 +280,11 @@ pub fn extract_style_from_styled<'a>(
let component = create_styled_component(
ast_builder,
&base.name,
&class_name,
&gen_styles(ast_builder, &props_styles, None),
StyledRender {
class_name: &class_name,
style_vars: &gen_styles(ast_builder, &props_styles, None),
bindings: Vec::new(),
},
);
let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, attrs));

Expand All @@ -298,6 +304,16 @@ pub fn extract_style_from_styled<'a>(
// Case 2: styled.div({ bg: "red" }), styled("div")({ bg: "red" }),
// or styled("div", { bg: "red" })

let prepared = call.arguments[style_index]
.as_expression()
.and_then(|rules| prepare(ast_builder, rules, &original_scope));
let bindings = match prepared {
Some(prepared) => {
call.arguments[style_index] = Argument::from(prepared.rules);
prepared.bindings
}
None => Vec::new(),
};
// Extract styles from object expression
let ExtractResult {
mut styles,
Expand Down Expand Up @@ -340,8 +356,11 @@ pub fn extract_style_from_styled<'a>(
let component = create_styled_component(
ast_builder,
&base.name,
&class_name,
&gen_styles(ast_builder, &styles, None),
StyledRender {
class_name: &class_name,
style_vars: &gen_styles(ast_builder, &styles, None),
bindings,
},
);
let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, attrs));

Expand Down Expand Up @@ -540,12 +559,22 @@ fn props_arrow<'a>(ast_builder: &AstBuilder<'a>, body: Expression<'a>) -> Expres
)
}

struct StyledRender<'b, 'a> {
class_name: &'b Option<Expression<'a>>,
style_vars: &'b Option<Expression<'a>>,
bindings: Vec<(String, Expression<'a>)>,
}

fn create_styled_component<'a>(
ast_builder: &AstBuilder<'a>,
tag_name: &str,
class_name: &Option<Expression<'a>>,
style_vars: &Option<Expression<'a>>,
render: StyledRender<'_, 'a>,
) -> Expression<'a> {
let StyledRender {
class_name,
style_vars,
bindings,
} = render;
let params = FormalParameters::boxed(
SPAN,
FormalParameterKind::ArrowFormalParameters,
Expand Down Expand Up @@ -692,7 +721,7 @@ fn create_styled_component<'a>(
None::<oxc_allocator::Box<oxc_ast::ast::TSTypeParameterDeclaration<'a>>>,
params,
None::<oxc_allocator::Box<oxc_ast::ast::TSTypeAnnotation<'a>>>,
body.into(),
bind_render(ast_builder, bindings, body).into(),
ast_builder,
)
}
4 changes: 4 additions & 0 deletions libs/extractor/src/extractor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ pub(super) mod extract_style_from_jsx;
pub(super) mod extract_style_from_member_expression;
pub(super) mod extract_style_from_styled;
pub(super) mod extract_style_from_stylex;
mod styled_record;
mod styled_record_evaluation;
#[cfg(test)]
mod styled_record_tests;

/**
* type
Expand Down
Loading
Loading