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_ts_styled_output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Generate React JSX-runtime calls for styled components in non-JSX source modules while preserving JSX and TSX output. No new build errors.",
"date": "2026-10-13T00:00:00.000Z"
}
7 changes: 3 additions & 4 deletions apps/landing/src/app/(detail)/docs/build-errors/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1902,15 +1902,14 @@ export function f() { const c = css; c = () => 1; return c({ color: "red" }); }

**Why:** A followed alias cannot be reassigned after the rewrite removes its declaration.

**Fix:** Keep the const alias unchanged and call it.
**Fix:** Call the imported css directly instead of creating a local runtime alias.

{/* probe: mutated-const-alias after */}

```tsx
import { css } from '@devup-ui/react'
export function f() {
const c = css
return c({ color: 'red' })
return css({ color: 'red' })
}
```

Expand Down Expand Up @@ -3926,7 +3925,7 @@ The `devup/no-runtime-read` rule reports compile-time bindings read outside supp
`<binding>` is read at runtime, where it does not exist: the build compiles it only where it is called or rendered.
```

```tsx
```tsx-error
import { css } from '@devup-ui/react'
consume(css)
```
Expand Down
7 changes: 4 additions & 3 deletions e2e/tailwind-hover-order.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ for (const singleCss of [true, false]) {
for (const fixture of cases) {
for (const [index, width] of [640, 1024].entries()) {
const label = 'props' in fixture ? 'explicit Devup guard: ' : ''
const devupColorIndex = 'props' in fixture ? 1 : 0
test(`${label}${fixture.classes}, ${width}px, singleCss=${singleCss}`, async ({
page,
}, testInfo) => {
Expand Down Expand Up @@ -191,7 +192,7 @@ for (const singleCss of [true, false]) {
const observed = []
for (const [name, classes, css, color] of [
['Tailwind', fixture.classes, oracleCss, expected[0]],
['Devup', matched?.[1], devupCss, expected[1]],
['Devup', matched?.[1], devupCss, expected[devupColorIndex]],
]) {
await page.setContent(
`<!doctype html><html data-theme="dark"><head><style>${css}</style></head><body><button id="target" class="${classes}" ${disabled ? 'disabled' : ''} style="width:160px;height:80px">probe</button></body></html>`,
Expand All @@ -207,14 +208,14 @@ for (const singleCss of [true, false]) {
} else if (!disabled) {
await expect(target).toHaveCSS(
'background-color',
colors.red[name === 'Tailwind' ? 0 : 1],
colors.red[name === 'Tailwind' ? 0 : devupColorIndex],
)
}
if (fixture.classes.includes('focus:')) {
await target.focus()
await expect(target).toHaveCSS(
'background-color',
colors.green[name === 'Tailwind' ? 0 : 1],
colors.green[name === 'Tailwind' ? 0 : devupColorIndex],
)
}
await page.mouse.down()
Expand Down
174 changes: 93 additions & 81 deletions libs/extractor/src/extractor/extract_style_from_styled.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::styled_jsx::StyledJsx;
use rustc_hash::FxHashMap;

use crate::{
Expand Down Expand Up @@ -44,6 +45,7 @@ const STYLED_FACTORY: &str =
/// only the runtime gives the component
struct Base<'a> {
name: String,
target: Option<Expression<'a>>,
styles: Option<Vec<ExtractStyleValue>>,
bound: Option<Expression<'a>>,
}
Expand All @@ -52,6 +54,7 @@ impl<'a> Base<'a> {
const fn named(name: String) -> Self {
Self {
name,
target: None,
styles: None,
bound: None,
}
Expand Down Expand Up @@ -113,10 +116,14 @@ fn tag_from_argument<'a>(
}
Some(jsx_name(argument).map_or_else(
|| Base {
target: Some(Expression::new_identifier(SPAN, STYLED_BASE, ast_builder)),
bound: Some(argument.clone_in(ast_builder.allocator())),
..Base::named(STYLED_BASE.to_string())
},
Base::named,
|name| Base {
target: Some(argument.clone_in(ast_builder.allocator())),
..Base::named(name)
},
))
}

Expand Down Expand Up @@ -185,6 +192,7 @@ pub fn extract_style_from_styled<'a>(
split_filename: Option<&str>,
imports: &FxHashMap<String, ExportVariableKind>,
attrs: &[Expression<'a>],
helper: &mut Option<StyledJsx>,
) -> (ExtractResult<'a>, Expression<'a>, Vec<(u32, String)>) {
let original_scope = expression.clone_in(ast_builder.allocator());
let mut composed_classes = Vec::new();
Expand Down Expand Up @@ -286,12 +294,13 @@ pub fn extract_style_from_styled<'a>(
));
let component = create_styled_component(
ast_builder,
&base.name,
&base,
StyledRender {
class_name: &class_name,
style_vars: &gen_styles(ast_builder, &props_styles, None),
bindings: Vec::new(),
},
helper,
);
let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, attrs));

Expand Down Expand Up @@ -363,12 +372,13 @@ pub fn extract_style_from_styled<'a>(
);
let component = create_styled_component(
ast_builder,
&base.name,
&base,
StyledRender {
class_name: &class_name,
style_vars: &gen_styles(ast_builder, &styles, None),
bindings,
},
helper,
);
let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, attrs));

Expand Down Expand Up @@ -575,8 +585,9 @@ struct StyledRender<'b, 'a> {

fn create_styled_component<'a>(
ast_builder: &AstBuilder<'a>,
tag_name: &str,
base: &Base<'a>,
render: StyledRender<'_, 'a>,
helper: &mut Option<StyledJsx>,
) -> Expression<'a> {
let StyledRender {
class_name,
Expand Down Expand Up @@ -637,92 +648,93 @@ fn create_styled_component<'a>(
None::<oxc_allocator::Box<oxc_ast::ast::FormalParameterRest<'a>>>,
ast_builder,
);
let body = Expression::new_jsx_element(
SPAN,
JSXOpeningElement::boxed(
let class_name = class_name.as_ref().map_or_else(
|| Expression::new_identifier(SPAN, "className", ast_builder),
|name| {
wrap_array_filter(
ast_builder,
&[
name.clone_in(ast_builder.allocator()),
Expression::new_identifier(SPAN, "className", ast_builder),
],
)
.unwrap_or_else(|| name.clone_in(ast_builder.allocator()))
},
);
let style = style_vars.as_ref().map_or_else(
|| Expression::new_identifier(SPAN, "style", ast_builder),
|style_vars| {
merge_object_expressions(
ast_builder,
&[
style_vars.clone_in(ast_builder.allocator()),
Expression::new_identifier(SPAN, "style", ast_builder),
],
)
.unwrap_or_else(|| style_vars.clone_in(ast_builder.allocator()))
},
);
let body = if let Some(helper) = helper {
let target = base.target.as_ref().map_or_else(
|| {
Expression::new_string_literal(
SPAN,
Str::from_in(&base.name, ast_builder.allocator()),
None,
ast_builder,
)
},
|target| target.clone_in(ast_builder.allocator()),
);
helper.render(ast_builder, target, (class_name, style))
} else {
Expression::new_jsx_element(
SPAN,
JSXElementName::new_identifier(
JSXOpeningElement::boxed(
SPAN,
Str::from_in(tag_name, ast_builder.allocator()),
ast_builder,
),
None::<oxc_allocator::Box<oxc_ast::ast::TSTypeParameterInstantiation<'a>>>,
oxc_allocator::Vec::from_iter_in(
vec![
JSXAttributeItem::new_spread_attribute(
SPAN,
Expression::new_identifier(SPAN, "rest", ast_builder),
ast_builder,
),
JSXAttributeItem::new_attribute(
SPAN,
JSXAttributeName::new_identifier(SPAN, "className", ast_builder),
Some(JSXAttributeValue::new_expression_container(
JSXElementName::new_identifier(
SPAN,
Str::from_in(&base.name, ast_builder.allocator()),
ast_builder,
),
None::<oxc_allocator::Box<oxc_ast::ast::TSTypeParameterInstantiation<'a>>>,
oxc_allocator::Vec::from_iter_in(
vec![
JSXAttributeItem::new_spread_attribute(
SPAN,
class_name
.as_ref()
.map_or_else(
|| Expression::new_identifier(SPAN, "className", ast_builder),
|name| {
wrap_array_filter(
ast_builder,
&[
name.clone_in(ast_builder.allocator()),
Expression::new_identifier(
SPAN,
"className",
ast_builder,
),
],
)
.unwrap_or_else(|| name.clone_in(ast_builder.allocator()))
},
)
.into(),
Expression::new_identifier(SPAN, "rest", ast_builder),
ast_builder,
)),
ast_builder,
),
JSXAttributeItem::new_attribute(
SPAN,
JSXAttributeName::new_identifier(SPAN, "style", ast_builder),
Some(JSXAttributeValue::new_expression_container(
),
JSXAttributeItem::new_attribute(
SPAN,
style_vars
.as_ref()
.map_or_else(
|| Expression::new_identifier(SPAN, "style", ast_builder),
|style_vars| {
merge_object_expressions(
ast_builder,
&[
style_vars.clone_in(ast_builder.allocator()),
Expression::new_identifier(
SPAN,
"style",
ast_builder,
),
],
)
.unwrap_or_else(
|| style_vars.clone_in(ast_builder.allocator()),
)
},
)
.into(),
JSXAttributeName::new_identifier(SPAN, "className", ast_builder),
Some(JSXAttributeValue::new_expression_container(
SPAN,
class_name.into(),
ast_builder,
)),
ast_builder,
)),
ast_builder,
),
],
),
JSXAttributeItem::new_attribute(
SPAN,
JSXAttributeName::new_identifier(SPAN, "style", ast_builder),
Some(JSXAttributeValue::new_expression_container(
SPAN,
style.into(),
ast_builder,
)),
ast_builder,
),
],
ast_builder,
),
ast_builder,
),
oxc_allocator::Vec::new_in(ast_builder),
None::<oxc_allocator::Box<oxc_ast::ast::JSXClosingElement<'a>>>,
ast_builder,
),
oxc_allocator::Vec::new_in(ast_builder),
None::<oxc_allocator::Box<oxc_ast::ast::JSXClosingElement<'a>>>,
ast_builder,
);
)
};
Expression::new_arrow_function_expression(
SPAN,
false,
Expand Down
1 change: 1 addition & 0 deletions libs/extractor/src/extractor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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;
pub(super) mod styled_jsx;
mod styled_record;
mod styled_record_evaluation;
#[cfg(test)]
Expand Down
Loading
Loading