diff --git a/.changepacks/changepack_log_compose_known_styles.json b/.changepacks/changepack_log_compose_known_styles.json new file mode 100644 index 000000000..6aa4c50d9 --- /dev/null +++ b/.changepacks/changepack_log_compose_known_styles.json @@ -0,0 +1,7 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch" + }, + "note": "css(a, b) composing classes whose styles the build knows (a const bound to css() in the file, or a css() result another module exports with a rule object every value of which is known) merges their styles: a later part's declaration replaces an earlier one's for the same property, selector, breakpoint and layer, also under conditions (css(base, cond && danger)) and for ||/?? parts, where the classes used to be joined and the stylesheet order picked the winner. vanilla-extract style([a, b]) passes each composed style as its own argument, so the later one wins, and a style composed again later (style([a, b, a])) is kept. Classes the build does not know (strings, props, CSS Modules) are kept as they are", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/.changepacks/changepack_log_compose_styled.json b/.changepacks/changepack_log_compose_styled.json new file mode 100644 index 000000000..4d05b2838 --- /dev/null +++ b/.changepacks/changepack_log_compose_styled.json @@ -0,0 +1,7 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch" + }, + "note": "styled(Base) extending a styled component the file binds to a const renders Base's tag directly with Base's styles composed under its own, so the extension's declarations replace Base's for the same property, selector, breakpoint and layer instead of losing to them by stylesheet order; Base's attrs apply before the extension's. attrs merge as styled-components merges them: className is joined with the caller's, style is merged and other props replace earlier ones. On an element, a className or style spread after the explicit prop now wins, as React merges props in the order they are written", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/.changepacks/changepack_log_styled_props.json b/.changepacks/changepack_log_styled_props.json new file mode 100644 index 000000000..8dfbb440e --- /dev/null +++ b/.changepacks/changepack_log_styled_props.json @@ -0,0 +1,7 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch" + }, + "note": "Styled components no longer pass every prop to the tag they render: `$` props, `theme` and the props their style functions or attrs read are kept away from a tag unless it takes them as attributes, and `shouldForwardProp` (Emotion options or styled-components withConfig) is evaluated at build time. A `shouldForwardProp` the build cannot evaluate is a build error naming the file, line and code, with the forms it accepts. An element using a styled component the file defines drops props the component neither reads nor passes on; spread props are passed as written", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/.changepacks/changepack_log_styled_targets.json b/.changepacks/changepack_log_styled_targets.json new file mode 100644 index 000000000..a51d45fe7 --- /dev/null +++ b/.changepacks/changepack_log_styled_targets.json @@ -0,0 +1,7 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch" + }, + "note": "Styled components render what their `as` prop names, defaulting to the tag or component they were defined with, and pass `forwardedAs` on as `as`, as styled-components and Emotion do; `as` used to reach the rendered tag as an attribute. `Component.withComponent(target)` on a styled component the file binds to a const builds a component rendering the same styles and attrs as `target` (a tag name or a component JSX can name), where it used to call a method the generated function does not have and throw", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/.changepacks/changepack_log_transient_props.json b/.changepacks/changepack_log_transient_props.json new file mode 100644 index 000000000..cbe2e2dd9 --- /dev/null +++ b/.changepacks/changepack_log_transient_props.json @@ -0,0 +1,9 @@ +{ + "changes": { + "apps/landing/package.json": "Patch", + "bindings/devup-ui-wasm/package.json": "Patch", + "packages/react/package.json": "Patch" + }, + "note": "Styled components now remove every runtime prop whose name starts with `$` before forwarding a rest spread to a tag or component, including props supplied by callers or attrs. Style and attrs functions can still read transient props, and all other build-time forwarding policies remain unchanged. The styled-components migration guide documents transient prop support. No new build errors", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx b/apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx index f6e92b699..801b33a05 100644 --- a/apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx +++ b/apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx @@ -34,7 +34,7 @@ Any base renders: a tag, a component, a member such as `motion.div`, or any othe A `keyframes` or `css` result held in a `const` of the same file is a build-time value, so `` css`animation: ${fadeIn} 1s;` `` and `styled.div({ ['.' + card + ':hover &']: { margin: 0 } })` work. In CSS text a class standing where a declaration would is a mixin, composed with the component's class. One imported from another module is not: its name is only known once that file is built, so declare it where it is used. -Object styles follow the libraries: several style arguments, or an array of them, merge into one rule set where a later declaration replaces an earlier one and nested rules merge; Emotion's options argument (`styled('div', { shouldForwardProp })(...)`) is accepted, and every prop is forwarded. Numbers mean pixels as they do in Emotion and styled-components — `padding: 8` is `8px`, unitless properties such as `lineHeight` stay as written — while Devup UI shorthands such as `p` keep the spacing scale. +Object styles follow the libraries: several style arguments, or an array of them, merge into one rule set where a later declaration replaces an earlier one and nested rules merge; Emotion's options argument (`styled('div', { shouldForwardProp })(...)`) is accepted, and its `shouldForwardProp` is evaluated at build time. Numbers mean pixels as they do in Emotion and styled-components — `padding: 8` is `8px`, unitless properties such as `lineHeight` stay as written — while Devup UI shorthands such as `p` keep the spacing scale. A style argument can be conditional — `cond && { ... }`, `cond ? { ... } : { ... }`, `value || { ... }` or `value ?? { ... }`, in `styled` as in `css`. Each property it sets becomes a per-property ternary over what came before (`color: cond ? 'blue' : 'red'`), so the result does not depend on the order atomic classes land in the stylesheet; a conditional class joins as `cond ? cls : ''`, and the left side of `||` / `??` joins only when it is a string, as the libraries skip other values. Classes passed beside rule objects compose in `styled` as in `css`. @@ -44,7 +44,7 @@ A styled callback cannot return a class name known only at runtime (`styled.div( 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: +`.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. In `.withConfig()`, `shouldForwardProp` is evaluated at build time; `displayName`, `componentId` and the other options are ignored and add no runtime behavior: ```tsx const Button = styled.button @@ -66,6 +66,23 @@ import * as Emotion from '@emotion/styled' const Card = Emotion.div`color: red;` ``` +## Transient props + +Props whose names start with `$` are transient, as in styled-components: style functions read them, but a styled component never passes them on to what it renders — a tag, a component or what `as` names. That includes `$` props a runtime spread brings and those attrs create, even when `shouldForwardProp` allows them: + +```tsx +const Title = styled.h1` + color: ${(p) => p.$tone}; +` + +function Heading(props) { + return +} + +// `$tone` sets the color; the <h1> gets `id` but neither `$tone` nor `$track` +const heading = <Heading $tone="teal" $track="hero" id="intro" /> +``` + ## Theme interpolation `ThemeProvider` redefines CSS variables on a `display: contents` wrapper, so it scopes the theme to its subtree without touching layout. Interpolations that read the theme are resolved at build time — no runtime lookup: @@ -128,7 +145,7 @@ Emotion's `<Global styles={...} />` behaves the same way: the `styles` prop is e | `css` prop, `jsx`, JSX runtime pragmas | compiled to `className` and `style` | | `ClassNames` | replaced by what its child renders, each `css` / `cx` call compiled to classes | | Component selectors (`${Child}`) | a marker class on the selected component | -| `.attrs()`, `.withConfig()` | attrs merged over props; `withConfig` dropped | +| `.attrs()`, `.withConfig()` | attrs merged over props; build-time `shouldForwardProp`, other options ignored | ## Limitations diff --git a/libs/extractor/src/composition.rs b/libs/extractor/src/composition.rs new file mode 100644 index 000000000..44dae55a6 --- /dev/null +++ b/libs/extractor/src/composition.rs @@ -0,0 +1,360 @@ +//! Styles composed in one place, such as `css(base, cond && danger)`: when the +//! build knows the parts, a later part's declaration replaces an earlier one's +//! for the same property, selector, breakpoint and layer. Atomic classes alone +//! cannot express that, because which of two classes wins depends on their +//! order in the stylesheet, not on the order they were composed in. + +use css::style_selector::StyleSelector; +use oxc_allocator::{CloneIn, GetAllocator}; +use oxc_ast::ast::Expression; +use oxc_ast::builder::AstBuilder; + +use crate::{ExtractStyleProp, ExtractStyleValue}; + +/// What a declaration competes on: property, selector, breakpoint and layer +#[derive(Clone, PartialEq, Eq)] +struct CascadeKey { + property: String, + selector: Option<StyleSelector>, + level: u8, + layer: Option<String>, +} + +impl CascadeKey { + fn of(value: &ExtractStyleValue) -> Self { + let (property, selector, level, layer) = match value { + ExtractStyleValue::Static(style) => ( + style.property(), + style.selector(), + style.level(), + style.layer(), + ), + ExtractStyleValue::Dynamic(style) => ( + style.property(), + style.selector(), + style.level(), + style.layer(), + ), + // A preset: the only other value a style prop holds + _ => ("typography", None, 0, None), + }; + Self { + property: property.to_string(), + selector: selector.cloned(), + level, + layer: layer.map(ToString::to_string), + } + } +} + +/// What a key resolves to +enum Choice<'a> { + Empty, + Atom(ExtractStyleValue), + Conditional { + test: Expression<'a>, + consequent: Box<Choice<'a>>, + alternate: Box<Choice<'a>>, + }, +} + +/// What a part does to a key +enum Overlay<'a> { + /// Leaves what came before + Keep, + Atom(ExtractStyleValue), + Conditional { + test: Expression<'a>, + consequent: Box<Overlay<'a>>, + alternate: Box<Overlay<'a>>, + }, +} + +/// The parts composed so far, in source order: a key a later part sets moves +/// to the end, as when merging objects +#[derive(Default)] +pub struct Composition<'a> { + entries: Vec<(CascadeKey, Choice<'a>)>, + /// Styles whose properties the build cannot pair up, kept as they are + unkeyed: Vec<ExtractStyleProp<'a>>, +} + +impl<'a> Composition<'a> { + /// A part the build knows completely + pub fn apply(&mut self, ast_builder: &AstBuilder<'a>, props: Vec<ExtractStyleProp<'a>>) { + let mut overlays: Vec<(CascadeKey, Overlay<'a>)> = Vec::new(); + for prop in props { + self.overlays(ast_builder, prop, &mut overlays); + } + for (key, overlay) in overlays { + self.overlay(ast_builder, key, overlay); + } + } + + /// A part applying only while `test` holds, or `alternate` otherwise + pub fn apply_conditional( + &mut self, + ast_builder: &AstBuilder<'a>, + test: &Expression<'a>, + consequent: Vec<ExtractStyleProp<'a>>, + alternate: Vec<ExtractStyleProp<'a>>, + ) { + let prop = ExtractStyleProp::Conditional { + condition: test.clone_in(ast_builder.allocator()), + consequent: Some(Box::new(ExtractStyleProp::StaticArray(consequent))), + alternate: Some(Box::new(ExtractStyleProp::StaticArray(alternate))), + }; + self.apply(ast_builder, vec![prop]); + } + + /// Each key a prop sets, with what it sets it to + fn overlays( + &mut self, + ast_builder: &AstBuilder<'a>, + prop: ExtractStyleProp<'a>, + overlays: &mut Vec<(CascadeKey, Overlay<'a>)>, + ) { + match prop { + ExtractStyleProp::Static(value) => { + push_overlay(overlays, CascadeKey::of(&value), Overlay::Atom(value)); + } + ExtractStyleProp::StaticArray(props) => { + for prop in props { + self.overlays(ast_builder, prop, overlays); + } + } + ExtractStyleProp::Conditional { + condition, + consequent, + alternate, + } if [&consequent, &alternate] + .into_iter() + .flatten() + .all(|side| keyed(side)) => + { + let mut sides = [Vec::new(), Vec::new()]; + for (side, prop) in sides.iter_mut().zip([consequent, alternate]) { + if let Some(prop) = prop { + self.overlays(ast_builder, *prop, side); + } + } + let [mut consequent, mut alternate] = sides; + let mut keys: Vec<CascadeKey> = Vec::new(); + for (key, _) in consequent.iter().chain(&alternate) { + if !keys.contains(key) { + keys.push(key.clone()); + } + } + for key in keys { + let consequent = take_overlay(&mut consequent, &key); + let alternate = take_overlay(&mut alternate, &key); + push_overlay( + overlays, + key, + Overlay::Conditional { + test: condition.clone_in(ast_builder.allocator()), + consequent: Box::new(consequent), + alternate: Box::new(alternate), + }, + ); + } + } + prop => self.unkeyed.push(prop), + } + } + + fn overlay(&mut self, ast_builder: &AstBuilder<'a>, key: CascadeKey, overlay: Overlay<'a>) { + let previous = self + .entries + .iter() + .position(|(existing, _)| *existing == key) + .map_or(Choice::Empty, |index| self.entries.remove(index).1); + let choice = resolve(ast_builder, overlay, previous); + self.entries.push((key, choice)); + } + + /// The composed styles, for class names and the stylesheet + #[must_use] + pub fn into_props(self) -> Vec<ExtractStyleProp<'a>> { + self.entries + .into_iter() + .filter_map(|(_, choice)| into_prop(choice)) + .chain(self.unkeyed) + .collect() + } + + /// The composed atoms when no condition chooses between them + #[must_use] + pub fn unconditional(&self) -> Option<Vec<ExtractStyleValue>> { + if !self.unkeyed.is_empty() { + return None; + } + let mut values = Vec::new(); + for (_, choice) in &self.entries { + match choice { + Choice::Atom(value) => values.push(value.clone()), + // A key is empty only under a condition + Choice::Empty | Choice::Conditional { .. } => return None, + } + } + Some(values) + } +} + +/// Styles a part of `css(...)` gives +pub enum KnownStyles<'a> { + /// The styles of a `css()` class the file binds + Known(Vec<ExtractStyleValue>), + /// A rule object + Rules(Expression<'a>), +} + +/// A side of a condition among the parts of `css(...)` +pub enum KnownSide<'a> { + Styles(Vec<KnownStyles<'a>>), + /// A class the build does not know the styles of + Class(Expression<'a>), + Empty, +} + +/// A part of `css(...)`, in source order +pub enum KnownPart<'a> { + Styles(Vec<KnownStyles<'a>>), + Conditional { + test: Expression<'a>, + consequent: Vec<KnownStyles<'a>>, + alternate: Vec<KnownStyles<'a>>, + }, + Class(Expression<'a>), +} + +/// `prop`'s styles at `order`, unless one sets its own +pub fn set_prop_order(prop: &mut ExtractStyleProp<'_>, order: u8) { + match prop { + ExtractStyleProp::Static(value) => value.set_style_order(order), + ExtractStyleProp::StaticArray(props) => { + for prop in props { + set_prop_order(prop, order); + } + } + ExtractStyleProp::Conditional { + consequent, + alternate, + .. + } => { + for side in [consequent, alternate].into_iter().flatten() { + set_prop_order(side, order); + } + } + ExtractStyleProp::Enum { map, .. } => { + for prop in map.values_mut().flatten() { + set_prop_order(prop, order); + } + } + ExtractStyleProp::MemberExpression { map, .. } => { + for prop in map.values_mut() { + set_prop_order(prop, order); + } + } + // Class names the code gives, and styles reported as errors + ExtractStyleProp::Expression { .. } | ExtractStyleProp::Unreadable { .. } => {} + } +} + +/// Whether every style `prop` holds has a key, so a condition around it can be +/// applied key by key +fn keyed(prop: &ExtractStyleProp<'_>) -> bool { + match prop { + ExtractStyleProp::Static(_) => true, + ExtractStyleProp::StaticArray(props) => props.iter().all(keyed), + ExtractStyleProp::Conditional { + consequent, + alternate, + .. + } => [consequent, alternate] + .into_iter() + .flatten() + .all(|side| keyed(side)), + _ => false, + } +} + +/// `overlay` replacing what `overlays` already sets for the key: a part +/// declaring a property twice keeps the later declaration +fn push_overlay<'a>( + overlays: &mut Vec<(CascadeKey, Overlay<'a>)>, + key: CascadeKey, + overlay: Overlay<'a>, +) { + overlays.retain(|(existing, _)| *existing != key); + overlays.push((key, overlay)); +} + +fn take_overlay<'a>( + overlays: &mut Vec<(CascadeKey, Overlay<'a>)>, + key: &CascadeKey, +) -> Overlay<'a> { + overlays + .iter() + .position(|(existing, _)| existing == key) + .map_or(Overlay::Keep, |index| overlays.remove(index).1) +} + +/// What `overlay` makes of `previous` +fn resolve<'a>( + ast_builder: &AstBuilder<'a>, + overlay: Overlay<'a>, + previous: Choice<'a>, +) -> Choice<'a> { + match overlay { + Overlay::Keep => previous, + Overlay::Atom(value) => Choice::Atom(value), + Overlay::Conditional { + test, + consequent, + alternate, + } => { + let copy = copy_choice(ast_builder, &previous); + Choice::Conditional { + test, + consequent: Box::new(resolve(ast_builder, *consequent, previous)), + alternate: Box::new(resolve(ast_builder, *alternate, copy)), + } + } + } +} + +fn copy_choice<'a>(ast_builder: &AstBuilder<'a>, choice: &Choice<'a>) -> Choice<'a> { + match choice { + Choice::Empty => Choice::Empty, + Choice::Atom(value) => Choice::Atom(value.clone()), + Choice::Conditional { + test, + consequent, + alternate, + } => Choice::Conditional { + test: test.clone_in(ast_builder.allocator()), + consequent: Box::new(copy_choice(ast_builder, consequent)), + alternate: Box::new(copy_choice(ast_builder, alternate)), + }, + } +} + +fn into_prop(choice: Choice<'_>) -> Option<ExtractStyleProp<'_>> { + match choice { + Choice::Empty => None, + Choice::Atom(value) => Some(ExtractStyleProp::Static(value)), + Choice::Conditional { + test, + consequent, + alternate, + } => { + let (consequent, alternate) = (into_prop(*consequent), into_prop(*alternate)); + (consequent.is_some() || alternate.is_some()).then(|| ExtractStyleProp::Conditional { + condition: test, + consequent: consequent.map(Box::new), + alternate: alternate.map(Box::new), + }) + } + } +} diff --git a/libs/extractor/src/extract_style/extract_dynamic_style.rs b/libs/extractor/src/extract_style/extract_dynamic_style.rs index f09b03b07..74cf36ea2 100644 --- a/libs/extractor/src/extract_style/extract_dynamic_style.rs +++ b/libs/extractor/src/extract_style/extract_dynamic_style.rs @@ -137,6 +137,10 @@ impl ExtractDynamicStyle { self.identifier.as_str() } + pub(crate) const fn identifier_mut(&mut self) -> &mut String { + &mut self.identifier + } + pub const fn style_order(&self) -> Option<u8> { self.style_order } diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs index c8ad9d20d..ef8f19f2a 100644 --- a/libs/extractor/src/extractor/extract_style_from_styled.rs +++ b/libs/extractor/src/extractor/extract_style_from_styled.rs @@ -10,10 +10,11 @@ use crate::{ ExtractResult, extract_style_from_expression::{LiteralHandling, extract_style_from_expression}, styled_record::prepare, - styled_record_evaluation::bind_render, + styled_record_evaluation::{Evaluations, bind_render, visit_style_expressions}, }, gen_class_name::{gen_class_names, merge_expression_for_class_name}, gen_style::gen_styles, + styled_reads::{Forward, Reads, withheld}, utils::{ STYLE_OBJECT, StyleArguments, build_time_error, call_with_values, merge_object_expressions, readable_code, reads_directly, style_arguments, uncomposable_error, unplaced_error, @@ -30,6 +31,7 @@ use oxc_ast::{ }, builder::AstBuilder, }; +use oxc_ast_visit::VisitMut; use oxc_span::{GetSpan, SPAN}; use oxc_syntax::operator::{BinaryOperator, LogicalOperator, UnaryOperator}; @@ -50,7 +52,377 @@ struct Base<'a> { bound: Option<Expression<'a>>, } +/// A styled component the file defines, which a component extending it +/// renders in its place: what it renders, its classes and styles, and its +/// attrs, as the extension applies them before its own +pub struct StyledDefinition<'a> { + name: String, + target: Option<Expression<'a>>, + bound: Option<Expression<'a>>, + classes: Vec<Expression<'a>>, + styles: Vec<ExtractStyleProp<'a>>, + attrs: Vec<Expression<'a>>, + /// The props its styles and attrs read + reads: Reads, + /// Its `shouldForwardProp`, as the build evaluates it + forward: Option<Forward>, + bindings: Vec<(String, Expression<'a>)>, +} + +/// Whether a styled component renders a tag, which takes only valid +/// attributes, rather than a component +fn renders_tag(name: &str, bound: Option<&Expression<'_>>) -> bool { + bound.is_none() && name.starts_with(|c: char| c.is_ascii_lowercase()) && !name.contains('.') +} + +/// `own` applying after `inherited`: a prop passes only when both pass it +fn combine_forward(inherited: Option<&Forward>, own: Option<Forward>) -> Option<Forward> { + match (inherited.cloned(), own) { + (Some(inherited), Some(own)) => Some(Forward::And(Box::new(inherited), Box::new(own))), + (inherited, own) => own.or(inherited), + } +} + +struct Rename<'b, 'a> { + builder: &'b AstBuilder<'a>, + names: FxHashMap<String, String>, +} + +impl<'a> VisitMut<'a> for Rename<'_, 'a> { + fn visit_identifier_reference( + &mut self, + identifier: &mut oxc_ast::ast::IdentifierReference<'a>, + ) { + let mut code = String::new(); + // Dynamic identifiers can hold emitted JS code. Captured names + // were excluded from authored source, so only whole tokens change. + let separator = |ch: char| !ch.is_alphanumeric() && ch != '_' && ch != '$'; + for chunk in identifier.name.split_inclusive(separator) { + let token = chunk.trim_end_matches(separator); + code.push_str(self.names.get(token).map_or(token, String::as_str)); + code.push_str(&chunk[token.len()..]); + } + identifier.name = oxc_ast::ast::Ident::from_in(code.as_str(), self.builder.allocator()); + } +} + +impl<'a> StyledDefinition<'a> { + fn visit_expressions( + &mut self, + builder: &AstBuilder<'a>, + mut visit: impl FnMut(&mut Expression<'a>), + ) { + for expression in self + .target + .iter_mut() + .chain(&mut self.bound) + .chain(&mut self.classes) + .chain(&mut self.attrs) + { + visit(expression); + } + for (_, value) in &mut self.bindings { + visit(value); + } + for style in &mut self.styles { + visit_style_expressions(builder, style, &mut visit); + } + } + + fn rebind(&self, builder: &AstBuilder<'a>, scope: &mut Expression<'a>) -> Self { + let allocator = builder.allocator(); + let mut inherited = Self { + name: self.name.clone(), + target: self.target.as_ref().map(|value| value.clone_in(allocator)), + bound: self.bound.as_ref().map(|value| value.clone_in(allocator)), + classes: clone_all(builder, &self.classes).collect(), + styles: self + .styles + .iter() + .map(|style| style.clone_in(allocator)) + .collect(), + attrs: clone_all(builder, &self.attrs).collect(), + reads: self.reads.clone(), + forward: self.forward.clone(), + bindings: self + .bindings + .iter() + .map(|(name, value)| (name.clone(), value.clone_in(allocator))) + .collect(), + }; + let mut reserved = vec![scope.clone_in(allocator)]; + inherited.visit_expressions(builder, |value| reserved.push(value.clone_in(allocator))); + reserved.extend(inherited.bindings.iter().map(|(name, _)| { + Expression::new_identifier( + SPAN, + oxc_ast::ast::Ident::from_in(name.as_str(), allocator), + builder, + ) + })); + let reservation = Expression::new_array_expression( + SPAN, + oxc_allocator::Vec::from_iter_in(reserved.into_iter().map(Into::into), builder), + builder, + ); + let mut reads = Evaluations::new(builder, readable_code(&reservation)); + let mut rename = Rename { + builder, + names: FxHashMap::default(), + }; + for (name, _) in &mut inherited.bindings { + let fresh = reads.fresh_name(); + rename.names.insert(name.clone(), fresh.clone()); + *name = fresh; + } + for (_, value) in &mut inherited.bindings { + rename.visit_expression(value); + } + for style in &mut inherited.styles { + visit_style_expressions(builder, style, &mut |value| rename.visit_expression(value)); + } + let mut reserved = vec![reservation]; + reserved.extend(inherited.bindings.iter().map(|(name, _)| { + Expression::new_identifier( + SPAN, + oxc_ast::ast::Ident::from_in(name.as_str(), allocator), + builder, + ) + })); + *scope = Expression::new_array_expression( + SPAN, + oxc_allocator::Vec::from_iter_in(reserved.into_iter().map(Into::into), builder), + builder, + ); + inherited + } +} + +impl StyledDefinition<'_> { + /// Whether an element using this component keeps passing the prop `name`: + /// what its styles read, what it passes on, and what React or the + /// component itself takes + #[must_use] + pub fn takes(&self, name: &str) -> bool { + self.reads.whole + || self.reads.names.iter().any(|read| read == name) + || matches!( + name, + "className" | "style" | "as" | "forwardedAs" | "key" | "ref" | "children" + ) + || crate::styled_reads::passes( + name, + renders_tag(&self.name, self.bound.as_ref()), + self.forward.as_ref(), + ) + } + + /// Whether an extension can render what this definition renders: a value + /// only the runtime gives is read again only when it is a binding + #[must_use] + pub fn extendable(&self) -> bool { + self.bound + .as_ref() + .is_none_or(|bound| matches!(bound, Expression::Identifier(_))) + } +} + +/// `Component.withComponent(target)`: the styles and attrs of `definition` +/// rendering `target`, a tag name or a component JSX can name; `None` for any +/// other target +pub fn with_component<'a>( + ast_builder: &AstBuilder<'a>, + definition: &StyledDefinition<'a>, + target: &Expression<'a>, + split_filename: Option<&str>, + helper: &mut Option<StyledJsx>, +) -> Option<(Expression<'a>, StyledDefinition<'a>)> { + let name = match unwrap_syntax_only(target) { + Expression::StringLiteral(literal) => literal.value.to_string(), + target => jsx_name(target)?, + }; + let allocator = ast_builder.allocator(); + let base = Base { + target: match unwrap_syntax_only(target) { + Expression::StringLiteral(_) => None, + _ => Some(target.clone_in(allocator)), + }, + ..Base::named(name) + }; + let mut scope = target.clone_in(allocator); + let definition = definition.rebind(ast_builder, &mut scope); + let mut styles: Vec<ExtractStyleProp<'a>> = definition + .styles + .iter() + .map(|style| style.clone_in(allocator)) + .collect(); + let classes: Vec<Expression<'a>> = clone_all(ast_builder, &definition.classes).collect(); + let class_name = merge_expression_for_class_name( + ast_builder, + clone_all(ast_builder, &classes).chain(gen_class_names( + ast_builder, + &mut styles, + None, + split_filename, + )), + ); + let withheld = withheld( + &definition.reads, + renders_tag(&base.name, None), + definition.forward.as_ref(), + ); + let component = create_styled_component( + ast_builder, + &base, + StyledRender { + class_name: &class_name, + style_vars: &gen_styles(ast_builder, &styles, None), + bindings: definition + .bindings + .iter() + .map(|(name, value)| (name.clone(), value.clone_in(allocator))) + .collect(), + withheld: &withheld, + }, + helper, + ); + let new_definition = base.definition( + ast_builder, + classes, + &styles, + &definition.attrs, + (definition.reads.clone(), definition.forward.clone()), + &definition.bindings, + ); + Some(( + apply_attrs(ast_builder, component, &definition.attrs), + new_definition, + )) +} + +/// What extracting a styled component gives +pub struct StyledExtraction<'a> { + pub result: ExtractResult<'a>, + pub expression: Expression<'a>, + pub errors: Vec<(u32, String)>, + pub definition: Option<StyledDefinition<'a>>, +} + +/// The styles a component renders: those it extends or its base component +/// brings, then its own, a later declaration replacing an earlier one +fn compose_styles<'a>( + ast_builder: &AstBuilder<'a>, + inherited: Option<&StyledDefinition<'a>>, + defaults: Option<Vec<ExtractStyleValue>>, + own: Vec<ExtractStyleProp<'a>>, +) -> Vec<ExtractStyleProp<'a>> { + let mut earlier: Vec<ExtractStyleProp<'a>> = inherited.map_or_else(Vec::new, |inherited| { + inherited + .styles + .iter() + .map(|style| style.clone_in(ast_builder.allocator())) + .collect() + }); + earlier.extend(defaults.into_iter().flatten().map(ExtractStyleProp::Static)); + if earlier.is_empty() { + return own; + } + let mut composition = crate::composition::Composition::default(); + composition.apply(ast_builder, earlier); + composition.apply(ast_builder, own); + composition.into_props() +} + +/// The classes and attrs a component extending `inherited` starts from +fn inherited_parts<'a>( + ast_builder: &AstBuilder<'a>, + inherited: Option<&StyledDefinition<'a>>, + attrs: &[Expression<'a>], +) -> (Vec<Expression<'a>>, Vec<Expression<'a>>) { + let allocator = ast_builder.allocator(); + let (classes, mut all_attrs) = inherited.map_or_else( + || (Vec::new(), Vec::new()), + |inherited| { + ( + inherited + .classes + .iter() + .map(|c| c.clone_in(allocator)) + .collect(), + inherited + .attrs + .iter() + .map(|a| a.clone_in(allocator)) + .collect(), + ) + }, + ); + all_attrs.extend(attrs.iter().map(|attr| attr.clone_in(allocator))); + (classes, all_attrs) +} + impl<'a> Base<'a> { + /// What `inherited` renders, in place of the component defining it + fn extending( + self, + ast_builder: &AstBuilder<'a>, + inherited: Option<&StyledDefinition<'a>>, + ) -> Self { + match inherited { + Some(inherited) => Self { + name: inherited.name.clone(), + target: inherited + .target + .as_ref() + .map(|target| target.clone_in(ast_builder.allocator())), + styles: None, + bound: inherited + .bound + .as_ref() + .map(|bound| bound.clone_in(ast_builder.allocator())), + }, + None => self, + } + } + + /// The definition of the component rendering this base + fn definition( + &self, + ast_builder: &AstBuilder<'a>, + classes: Vec<Expression<'a>>, + styles: &[ExtractStyleProp<'a>], + attrs: &[Expression<'a>], + props: (Reads, Option<Forward>), + bindings: &[(String, Expression<'a>)], + ) -> StyledDefinition<'a> { + let allocator = ast_builder.allocator(); + let (reads, forward) = props; + StyledDefinition { + name: self.name.clone(), + target: self + .target + .as_ref() + .map(|target| target.clone_in(allocator)), + bound: self.bound.as_ref().map(|bound| bound.clone_in(allocator)), + classes, + styles: styles + .iter() + .map(|style| style.clone_in(allocator)) + .collect(), + attrs: attrs.iter().map(|attr| attr.clone_in(allocator)).collect(), + reads, + forward, + bindings: bindings + .iter() + .map(|(name, value)| (name.clone(), value.clone_in(allocator))) + .collect(), + } + } + + /// The props a component rendering this base keeps away from it + fn withheld(&self, reads: &Reads, forward: Option<&Forward>) -> Vec<String> { + withheld(reads, renders_tag(&self.name, self.bound.as_ref()), forward) + } + const fn named(name: String) -> Self { Self { name, @@ -70,6 +442,29 @@ impl<'a> Base<'a> { } } +/// The component `styled(Component)` extends, as written: the argument naming +/// it in a tagged template `styled(Component)`, `styled(Component)({...})` or +/// `styled(Component, {...})` +#[must_use] +pub fn extended<'b, 'a>(expression: &'b Expression<'a>) -> Option<&'b Expression<'a>> { + let factory = match expression { + Expression::TaggedTemplateExpression(tag) => &tag.tag, + Expression::CallExpression(call) + if matches!( + unwrap_syntax_only(&call.callee), + Expression::CallExpression(_) + ) => + { + &call.callee + } + expression => expression, + }; + let Expression::CallExpression(call) = unwrap_syntax_only(factory) else { + return None; + }; + unwrap_syntax_only(call.arguments.first()?.as_expression()?).into() +} + fn extract_base_tag_and_class_name<'a>( ast_builder: &AstBuilder<'a>, input: &Expression<'a>, @@ -191,10 +586,19 @@ pub fn extract_style_from_styled<'a>( expression: &mut Expression<'a>, split_filename: Option<&str>, imports: &FxHashMap<String, ExportVariableKind>, - attrs: &[Expression<'a>], + modifiers: (&[Expression<'a>], Option<Forward>), + inherited: Option<&StyledDefinition<'a>>, helper: &mut Option<StyledJsx>, -) -> (ExtractResult<'a>, Expression<'a>, Vec<(u32, String)>) { - let original_scope = expression.clone_in(ast_builder.allocator()); +) -> StyledExtraction<'a> { + let (attrs, forward) = modifiers; + let mut original_scope = expression.clone_in(ast_builder.allocator()); + let inherited = inherited.map(|definition| definition.rebind(ast_builder, &mut original_scope)); + let inherited = inherited.as_ref(); + let forward = combine_forward(inherited.and_then(|i| i.forward.as_ref()), forward); + let mut reads = inherited.map_or_else(Reads::default, |inherited| inherited.reads.clone()); + for attr in attrs { + reads.read_in(attr); + } let mut composed_classes = Vec::new(); let mut errors = Vec::new(); if let Expression::CallExpression(call) = expression @@ -220,7 +624,8 @@ pub fn extract_style_from_styled<'a>( None => {} } } - let (result, new_expr) = if let Expression::TaggedTemplateExpression(tag) = expression + let (result, new_expr, definition) = if let Expression::TaggedTemplateExpression(tag) = + expression && let Some(mut base) = extract_base_tag_and_class_name(ast_builder, &tag.tag, imports) { // Case 1: styled.div`css` or styled("div")`css` @@ -240,13 +645,17 @@ pub fn extract_style_from_styled<'a>( ) })); if !errors.is_empty() { - return ( - ExtractResult::default(), - Expression::new_string_literal(SPAN, "", None, ast_builder), + return StyledExtraction { + result: ExtractResult::default(), + expression: Expression::new_string_literal(SPAN, "", None, ast_builder), errors, - ); + definition: None, + }; } + for interpolation in &tag.quasi.expressions { + reads.read_in(interpolation); + } let TemplateStyles { styles, statements, @@ -263,16 +672,16 @@ pub fn extract_style_from_styled<'a>( .filter(|expression| crate::css_utils::reads_unmapped_theme(expression)) .map(|expression| crate::css_utils::unmapped_theme_error("styled", expression)), ); - let mut props_styles: Vec<ExtractStyleProp<'_>> = styles + let own: Vec<ExtractStyleProp<'_>> = styles .into_iter() .map(|ex| ExtractStyleProp::Static(ex.into())) .collect(); + let defaults = base.styles.take(); + let base = base.extending(ast_builder, inherited); + let mut props_styles = compose_styles(ast_builder, inherited, defaults, own); + let (mut classes, attrs) = inherited_parts(ast_builder, inherited, attrs); - if let Some(default_class_name) = base.styles.take() { - props_styles.extend(default_class_name.into_iter().map(ExtractStyleProp::Static)); - } - - let mixins = statements.into_iter().map(|index| { + classes.extend(statements.into_iter().map(|index| { let mixin = &tag.quasi.expressions[index]; if matches!( unwrap_syntax_only(mixin), @@ -293,18 +702,15 @@ pub fn extract_style_from_styled<'a>( } else { mixin.clone_in(ast_builder.allocator()) } - }); + })); let class_name = merge_expression_for_class_name( ast_builder, - mixins - .collect::<Vec<_>>() - .into_iter() - .chain(gen_class_names( - ast_builder, - &mut props_styles, - None, - split_filename, - )), + clone_all(ast_builder, &classes).chain(gen_class_names( + ast_builder, + &mut props_styles, + None, + split_filename, + )), ); let tag = Some(Expression::new_string_literal( SPAN, @@ -312,17 +718,35 @@ pub fn extract_style_from_styled<'a>( None, ast_builder, )); + let bindings = inherited.map_or_else(Vec::new, |definition| { + definition + .bindings + .iter() + .map(|(name, value)| (name.clone(), value.clone_in(ast_builder.allocator()))) + .collect() + }); + let withheld = base.withheld(&reads, forward.as_ref()); + let definition = base.definition( + ast_builder, + classes, + &props_styles, + &attrs, + (reads, forward), + &bindings, + ); let component = create_styled_component( ast_builder, &base, StyledRender { class_name: &class_name, style_vars: &gen_styles(ast_builder, &props_styles, None), - bindings: Vec::new(), + bindings, + withheld: &withheld, }, helper, ); - let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, attrs)); + let styled_component = + base.render(ast_builder, apply_attrs(ast_builder, component, &attrs)); let result = ExtractResult { styles: props_styles, @@ -332,7 +756,7 @@ pub fn extract_style_from_styled<'a>( props: None, }; - (Some(result), Some(styled_component)) + (Some(result), Some(styled_component), Some(definition)) } else if let Expression::CallExpression(call) = expression && let Some((mut base, style_index)) = resolve_styled_call_target(ast_builder, call, imports) @@ -340,16 +764,30 @@ pub fn extract_style_from_styled<'a>( // Case 2: styled.div({ bg: "red" }), styled("div")({ bg: "red" }), // or styled("div", { bg: "red" }) + reads.read_in( + if let Argument::SpreadElement(spread) = &call.arguments[style_index] { + &spread.argument + } else { + call.arguments[style_index].to_expression() + }, + ); let prepared = call.arguments[style_index] .as_expression() .and_then(|rules| prepare(ast_builder, rules, &original_scope)); - let bindings = match prepared { + let mut bindings: Vec<_> = inherited.map_or_else(Vec::new, |definition| { + definition + .bindings + .iter() + .map(|(name, value)| (name.clone(), value.clone_in(ast_builder.allocator()))) + .collect() + }); + bindings.extend(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, @@ -377,19 +815,35 @@ pub fn extract_style_from_styled<'a>( build_time_error("styled", &code, requirement.unwrap_or(STYLE_OBJECT)), ) })); - if let Some(default_class_name) = base.styles.take() { - styles.extend(default_class_name.into_iter().map(ExtractStyleProp::Static)); + if let Some(order) = style_order { + for style in &mut styles { + crate::composition::set_prop_order(style, order); + } } + let defaults = base.styles.take(); + let base = base.extending(ast_builder, inherited); + let mut styles = compose_styles(ast_builder, inherited, defaults, styles); + let (mut classes, attrs) = inherited_parts(ast_builder, inherited, attrs); + classes.extend(composed_classes); let class_name = merge_expression_for_class_name( ast_builder, - composed_classes.into_iter().chain(gen_class_names( + clone_all(ast_builder, &classes).chain(gen_class_names( ast_builder, &mut styles, - style_order, + None, split_filename, )), ); + let withheld = base.withheld(&reads, forward.as_ref()); + let definition = base.definition( + ast_builder, + classes, + &styles, + &attrs, + (reads, forward), + &bindings, + ); let component = create_styled_component( ast_builder, &base, @@ -397,10 +851,12 @@ pub fn extract_style_from_styled<'a>( class_name: &class_name, style_vars: &gen_styles(ast_builder, &styles, None), bindings, + withheld: &withheld, }, helper, ); - let styled_component = base.render(ast_builder, apply_attrs(ast_builder, component, attrs)); + let styled_component = + base.render(ast_builder, apply_attrs(ast_builder, component, &attrs)); let result = ExtractResult { styles, @@ -410,7 +866,7 @@ pub fn extract_style_from_styled<'a>( props, }; - (Some(result), Some(styled_component)) + (Some(result), Some(styled_component), Some(definition)) } else { // Left as written it would call `styled` at runtime, which only the build runs let code = match &*expression { @@ -421,13 +877,23 @@ pub fn extract_style_from_styled<'a>( expression.span().start, build_time_error("styled", &code, STYLED_FACTORY), )); - (None, None) + (None, None, None) }; - ( - result.unwrap_or_else(ExtractResult::default), - new_expr.unwrap_or_else(|| expression.clone_in(ast_builder.allocator())), + StyledExtraction { + result: result.unwrap_or_else(ExtractResult::default), + expression: new_expr.unwrap_or_else(|| expression.clone_in(ast_builder.allocator())), errors, - ) + definition, + } +} + +fn clone_all<'s, 'a>( + ast_builder: &'s AstBuilder<'a>, + expressions: &'s [Expression<'a>], +) -> impl Iterator<Item = Expression<'a>> + 's { + expressions + .iter() + .map(|expression| expression.clone_in(ast_builder.allocator())) } /// The name the attrs wrapper binds props to, chosen not to shadow what the @@ -436,15 +902,16 @@ const ATTRS_PROPS: &str = "__devupProps"; /// Strip styled-components' `.attrs()` / `.withConfig()` off a styled factory /// such as `styled.div.attrs(a).withConfig(c)`, returning the attrs in the -/// order they apply. `withConfig` only tunes runtime behavior, so it is dropped. +/// order they apply, and the `withConfig` options in the order they apply pub fn take_styled_modifiers<'a>( ast_builder: &AstBuilder<'a>, factory: &mut Expression<'a>, is_styled: impl Fn(&str) -> bool, -) -> Vec<Expression<'a>> { +) -> (Vec<Expression<'a>>, Vec<Expression<'a>>) { let mut attrs = Vec::new(); + let mut configs = Vec::new(); if !is_modified_styled(factory, is_styled) { - return attrs; + return (attrs, configs); } while let Expression::CallExpression(call) = unwrap_syntax_only_mut(factory) && let CallExpression { @@ -454,18 +921,62 @@ pub fn take_styled_modifiers<'a>( && matches!(member.property.name.as_str(), "attrs" | "withConfig") { let placeholder = || Expression::new_null_literal(SPAN, ast_builder); - if member.property.name == "attrs" - && let Some(argument) = arguments[0].as_expression_mut() - { - attrs.push(std::mem::replace(argument, placeholder())); + if let Some(argument) = arguments[0].as_expression_mut() { + let argument = std::mem::replace(argument, placeholder()); + if member.property.name == "attrs" { + attrs.push(argument); + } else { + configs.push(argument); + } } let object = std::mem::replace(&mut member.object, placeholder()); *factory = object; } attrs.reverse(); - attrs + configs.reverse(); + (attrs, configs) } +/// The `shouldForwardProp` the options objects give, the later applying after +/// the earlier, with the offset and code of one the build cannot evaluate +pub fn read_forward(options: &[&Expression<'_>]) -> (Option<Forward>, Option<(u32, String)>) { + let mut forward = None; + for options in options { + let Expression::ObjectExpression(object) = unwrap_syntax_only(options) else { + continue; + }; + for property in &object.properties { + let ObjectPropertyKind::ObjectProperty(property) = property else { + continue; + }; + if property.computed + || property.key.static_name().as_deref() != Some("shouldForwardProp") + { + continue; + } + match Forward::read(&property.value) { + Some(own) => forward = combine_forward(forward.as_ref(), Some(own)), + None => { + return ( + forward, + Some(( + property.value.span().start, + build_time_error( + "styled", + &readable_code(&property.value), + SHOULD_FORWARD_PROP, + ), + )), + ); + } + } + } + } + (forward, None) +} + +const SHOULD_FORWARD_PROP: &str = "`shouldForwardProp` must be a function of the prop name that compares it with strings, `[...].includes(prop)`, `prop.startsWith(...)` or `isPropValid(prop)`, joined by `!`, `&&` and `||`"; + fn is_modified_styled(expression: &Expression<'_>, is_styled: impl Fn(&str) -> bool) -> bool { let mut expression = unwrap_syntax_only(expression); let mut modified = false; @@ -509,44 +1020,215 @@ fn apply_attrs<'a>( if attrs.is_empty() { return component; } - let props = || Expression::new_identifier(SPAN, ATTRS_PROPS, ast_builder); - let mut merged = props(); + let merged = if attrs.iter().all(sets_plain_props) { + spread_attrs(ast_builder, attrs) + } else { + merge_attrs(ast_builder, attrs) + }; + props_arrow( + ast_builder, + wrap_direct_call(ast_builder, &component, &[merged]), + ) +} + +/// The binding each attrs step reads the props merged so far through +const ATTRS_CONTEXT: &str = "__devupContext"; +/// The binding each attrs step reads what its attrs give through +const ATTRS_VALUE: &str = "__devupAttrs"; + +/// Whether `attr` is an object setting props other than `className` and +/// `style` by name, which spreading over the props merges as attrs do +fn sets_plain_props(attr: &Expression<'_>) -> bool { + let Expression::ObjectExpression(object) = unwrap_syntax_only(attr) else { + return false; + }; + object.properties.iter().all(|property| { + matches!(property, ObjectPropertyKind::ObjectProperty(property) + if !property.computed + && property + .key + .static_name() + .is_some_and(|name| name != "className" && name != "style")) + }) +} + +/// The props with each attrs spread over them in order +fn spread_attrs<'a>(ast_builder: &AstBuilder<'a>, attrs: &[Expression<'a>]) -> Expression<'a> { + let mut merged = identifier(ast_builder, ATTRS_PROPS); for attr in attrs { - let attr = attr.clone_in(ast_builder.allocator()); - if matches!(unwrap_syntax_only(&attr), Expression::ObjectExpression(_)) { - merged = spread_objects(ast_builder, merged, attr); - continue; - } - let called = wrap_direct_call(ast_builder, &attr, &[props()]); - let resolved = if matches!( - unwrap_syntax_only(&attr), - Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) - ) { - called - } else { - let is_function = Expression::new_binary_expression( + merged = spread_objects(ast_builder, merged, attr.clone_in(ast_builder.allocator())); + } + merged +} + +/// The props merged as styled-components merges attrs: each attrs, or what +/// calling it with the props merged so far gives, joins its `className` to +/// theirs, merges its `style` over theirs and replaces their other props; the +/// caller's `className` comes last +fn merge_attrs<'a>(ast_builder: &AstBuilder<'a>, attrs: &[Expression<'a>]) -> Expression<'a> { + let mut merged = with_property( + ast_builder, + identifier(ast_builder, ATTRS_PROPS), + "className", + identifier(ast_builder, "undefined"), + ); + for attr in attrs { + let resolved = spread_objects( + ast_builder, + Expression::new_object_expression( SPAN, - Expression::new_unary_expression( - SPAN, - UnaryOperator::Typeof, - attr.clone_in(ast_builder.allocator()), - ast_builder, - ), - BinaryOperator::StrictEquality, - Expression::new_string_literal(SPAN, "function", None, ast_builder), + oxc_allocator::Vec::new_in(ast_builder), ast_builder, - ); - Expression::new_conditional_expression(SPAN, is_function, called, attr, ast_builder) - }; - let step = props_arrow(ast_builder, spread_objects(ast_builder, props(), resolved)); + ), + resolve_attrs(ast_builder, attr.clone_in(ast_builder.allocator())), + ); + let merge = call_with_values( + ast_builder, + vec![(ATTRS_VALUE.to_string(), resolved)], + merge_attrs_value(ast_builder), + ); + let step = named_arrow(ast_builder, ATTRS_CONTEXT, merge); merged = wrap_direct_call(ast_builder, &step, &[merged]); } - props_arrow( + let caller_class = joined_class( + ast_builder, + member(ast_builder, ATTRS_CONTEXT, "className"), + member(ast_builder, ATTRS_PROPS, "className"), + ); + let last = named_arrow( + ast_builder, + ATTRS_CONTEXT, + with_property( + ast_builder, + identifier(ast_builder, ATTRS_CONTEXT), + "className", + caller_class, + ), + ); + wrap_direct_call(ast_builder, &last, &[merged]) +} + +/// What `attr` gives: itself when it is an object, or what calling it with +/// the props merged so far gives when it is a function +fn resolve_attrs<'a>(ast_builder: &AstBuilder<'a>, attr: Expression<'a>) -> Expression<'a> { + if matches!(unwrap_syntax_only(&attr), Expression::ObjectExpression(_)) { + return attr; + } + let called = wrap_direct_call( + ast_builder, + &attr, + &[identifier(ast_builder, ATTRS_CONTEXT)], + ); + if matches!( + unwrap_syntax_only(&attr), + Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) + ) { + return called; + } + let is_function = Expression::new_binary_expression( + SPAN, + Expression::new_unary_expression( + SPAN, + UnaryOperator::Typeof, + attr.clone_in(ast_builder.allocator()), + ast_builder, + ), + BinaryOperator::StrictEquality, + Expression::new_string_literal(SPAN, "function", None, ast_builder), + ast_builder, + ); + Expression::new_conditional_expression(SPAN, is_function, called, attr, ast_builder) +} + +/// `{ ...context, ...attrs, className, style }` with the classes joined and +/// the styles merged +fn merge_attrs_value<'a>(ast_builder: &AstBuilder<'a>) -> Expression<'a> { + let merged = spread_objects( + ast_builder, + identifier(ast_builder, ATTRS_CONTEXT), + identifier(ast_builder, ATTRS_VALUE), + ); + let class_name = joined_class( + ast_builder, + member(ast_builder, ATTRS_CONTEXT, "className"), + member(ast_builder, ATTRS_VALUE, "className"), + ); + let style = spread_objects( + ast_builder, + member(ast_builder, ATTRS_CONTEXT, "style"), + member(ast_builder, ATTRS_VALUE, "style"), + ); + let merged = with_property(ast_builder, merged, "className", class_name); + with_property(ast_builder, merged, "style", style) +} + +fn identifier<'a>(ast_builder: &AstBuilder<'a>, name: &'static str) -> Expression<'a> { + Expression::new_identifier(SPAN, name, ast_builder) +} + +fn member<'a>( + ast_builder: &AstBuilder<'a>, + object: &'static str, + name: &'static str, +) -> Expression<'a> { + Expression::StaticMemberExpression(oxc_ast::ast::StaticMemberExpression::boxed( + SPAN, + identifier(ast_builder, object), + oxc_ast::ast::IdentifierName::new(SPAN, name, ast_builder), + false, + ast_builder, + )) +} + +/// `[first, second].filter(Boolean).join(" ") || undefined` +fn joined_class<'a>( + ast_builder: &AstBuilder<'a>, + first: Expression<'a>, + second: Expression<'a>, +) -> Expression<'a> { + let joined = wrap_array_filter(ast_builder, &[first, second]) + .unwrap_or_else(|| identifier(ast_builder, "undefined")); + Expression::new_logical_expression( + SPAN, + joined, + LogicalOperator::Or, + identifier(ast_builder, "undefined"), ast_builder, - wrap_direct_call(ast_builder, &component, &[merged]), ) } +/// `object` with `name` set to `value`: an object spreading it, then setting +/// the property +fn with_property<'a>( + ast_builder: &AstBuilder<'a>, + object: Expression<'a>, + name: &'static str, + value: Expression<'a>, +) -> Expression<'a> { + let mut properties = match object { + Expression::ObjectExpression(object) => object.unbox().properties, + object => oxc_allocator::Vec::from_array_in( + [ObjectPropertyKind::new_spread_property( + SPAN, + object, + ast_builder, + )], + ast_builder, + ), + }; + properties.push(ObjectPropertyKind::new_object_property( + SPAN, + oxc_ast::ast::PropertyKind::Init, + PropertyKey::new_static_identifier(SPAN, name, ast_builder), + value, + false, + false, + false, + ast_builder, + )); + Expression::new_object_expression(SPAN, properties, ast_builder) +} + fn spread_objects<'a>( ast_builder: &AstBuilder<'a>, first: Expression<'a>, @@ -567,10 +1249,19 @@ fn spread_objects<'a>( } fn props_arrow<'a>(ast_builder: &AstBuilder<'a>, body: Expression<'a>) -> Expression<'a> { + named_arrow(ast_builder, ATTRS_PROPS, body) +} + +/// `(name) => body` +fn named_arrow<'a>( + ast_builder: &AstBuilder<'a>, + name: &'static str, + body: Expression<'a>, +) -> Expression<'a> { let parameter = FormalParameter::new( SPAN, oxc_allocator::Vec::new_in(ast_builder), - BindingPattern::new_binding_identifier(SPAN, ATTRS_PROPS, ast_builder), + BindingPattern::new_binding_identifier(SPAN, name, ast_builder), None::<oxc_allocator::Box<oxc_ast::ast::TSTypeAnnotation<'a>>>, None::<oxc_allocator::Box<Expression<'a>>>, false, @@ -597,10 +1288,87 @@ fn props_arrow<'a>(ast_builder: &AstBuilder<'a>, body: Expression<'a>) -> Expres ) } +/// `rest` without the props in `withheld`, as `(({ a: _0, ...p }) => p)(rest)` +fn without_props<'a>(ast_builder: &AstBuilder<'a>, withheld: &[String]) -> Expression<'a> { + let rest = Expression::new_identifier(SPAN, "rest", ast_builder); + if withheld.is_empty() { + return rest; + } + let properties = withheld.iter().enumerate().map(|(index, name)| { + BindingProperty::new( + SPAN, + PropertyKey::StringLiteral(oxc_ast::ast::StringLiteral::boxed( + SPAN, + Str::from_in(name.as_str(), ast_builder.allocator()), + None, + ast_builder, + )), + BindingPattern::new_binding_identifier( + SPAN, + Str::from_in( + format!("__devupOmit{index}").as_str(), + ast_builder.allocator(), + ), + ast_builder, + ), + false, + false, + ast_builder, + ) + }); + let pattern = BindingPattern::new_object_pattern( + SPAN, + oxc_allocator::Vec::from_iter_in(properties, ast_builder), + Some(BindingRestElement::boxed( + SPAN, + BindingPattern::new_binding_identifier(SPAN, "__devupDom", ast_builder), + ast_builder, + )), + ast_builder, + ); + let parameter = FormalParameter::new( + SPAN, + oxc_allocator::Vec::new_in(ast_builder), + pattern, + None::<oxc_allocator::Box<oxc_ast::ast::TSTypeAnnotation<'a>>>, + None::<oxc_allocator::Box<Expression<'a>>>, + false, + None, + false, + false, + ast_builder, + ); + let arrow = Expression::new_arrow_function_expression( + SPAN, + false, + None::<oxc_allocator::Box<oxc_ast::ast::TSTypeParameterDeclaration<'a>>>, + FormalParameters::boxed( + SPAN, + FormalParameterKind::ArrowFormalParameters, + oxc_allocator::Vec::from_iter_in([parameter], ast_builder), + None::<oxc_allocator::Box<oxc_ast::ast::FormalParameterRest<'a>>>, + ast_builder, + ), + None::<oxc_allocator::Box<oxc_ast::ast::TSTypeAnnotation<'a>>>, + Expression::new_identifier(SPAN, "__devupDom", ast_builder).into(), + ast_builder, + ); + wrap_direct_call( + ast_builder, + &Expression::new_parenthesized_expression(SPAN, arrow, ast_builder), + &[rest], + ) +} + +/// The binding a styled component renders through: what `as` names, or the +/// tag or component it was defined with +const RENDERED: &str = "DevupAs"; + struct StyledRender<'b, 'a> { class_name: &'b Option<Expression<'a>>, style_vars: &'b Option<Expression<'a>>, bindings: Vec<(String, Expression<'a>)>, + withheld: &'b [String], } fn create_styled_component<'a>( @@ -613,6 +1381,7 @@ fn create_styled_component<'a>( class_name, style_vars, bindings, + withheld, } = render; let params = FormalParameters::boxed( SPAN, @@ -645,6 +1414,49 @@ fn create_styled_component<'a>( false, ast_builder, ), + BindingProperty::new( + SPAN, + PropertyKey::new_static_identifier(SPAN, "as", ast_builder), + BindingPattern::new_assignment_pattern( + SPAN, + BindingPattern::new_binding_identifier( + SPAN, + RENDERED, + ast_builder, + ), + 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()), + ), + ast_builder, + ), + false, + false, + ast_builder, + ), + BindingProperty::new( + SPAN, + PropertyKey::new_static_identifier( + SPAN, + "forwardedAs", + ast_builder, + ), + BindingPattern::new_binding_identifier( + SPAN, + "forwardedAs", + ast_builder, + ), + true, + false, + ast_builder, + ), ], ast_builder, ), @@ -695,34 +1507,37 @@ fn create_styled_component<'a>( }, ); 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)) + helper.render( + ast_builder, + identifier(ast_builder, RENDERED), + (class_name, style, identifier(ast_builder, "forwardedAs")), + super::styled_transient::filter(ast_builder, without_props(ast_builder, withheld)), + ) } else { Expression::new_jsx_element( SPAN, JSXOpeningElement::boxed( SPAN, - JSXElementName::new_identifier( - SPAN, - Str::from_in(&base.name, ast_builder.allocator()), - ast_builder, - ), + JSXElementName::new_identifier(SPAN, RENDERED, 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), + super::styled_transient::filter( + ast_builder, + without_props(ast_builder, withheld), + ), + ast_builder, + ), + JSXAttributeItem::new_attribute( + SPAN, + JSXAttributeName::new_identifier(SPAN, "as", ast_builder), + Some(JSXAttributeValue::new_expression_container( + SPAN, + identifier(ast_builder, "forwardedAs").into(), + ast_builder, + )), ast_builder, ), JSXAttributeItem::new_attribute( diff --git a/libs/extractor/src/extractor/mod.rs b/libs/extractor/src/extractor/mod.rs index d95289c0d..09b9c1b27 100644 --- a/libs/extractor/src/extractor/mod.rs +++ b/libs/extractor/src/extractor/mod.rs @@ -14,6 +14,7 @@ mod styled_record; mod styled_record_evaluation; #[cfg(test)] mod styled_record_tests; +mod styled_transient; /** * type diff --git a/libs/extractor/src/extractor/styled_jsx.rs b/libs/extractor/src/extractor/styled_jsx.rs index 75af546ef..1616e8013 100644 --- a/libs/extractor/src/extractor/styled_jsx.rs +++ b/libs/extractor/src/extractor/styled_jsx.rs @@ -59,16 +59,13 @@ impl StyledJsx { &mut self, ast: &AstBuilder<'a>, target: Expression<'a>, - props: (Expression<'a>, Expression<'a>), + props: (Expression<'a>, Expression<'a>, Expression<'a>), + rest: Expression<'a>, ) -> Expression<'a> { self.used = true; - let mut properties = Vec::with_capacity_in(3, ast); - properties.push(ObjectPropertyKind::new_spread_property( - SPAN, - Expression::new_identifier(SPAN, "rest", ast), - ast, - )); - for (key, value) in [("className", props.0), ("style", props.1)] { + let mut properties = Vec::with_capacity_in(4, ast); + properties.push(ObjectPropertyKind::new_spread_property(SPAN, rest, ast)); + for (key, value) in [("className", props.0), ("style", props.1), ("as", props.2)] { properties.push(ObjectPropertyKind::new_object_property( SPAN, PropertyKind::Init, diff --git a/libs/extractor/src/extractor/styled_jsx_tests.rs b/libs/extractor/src/extractor/styled_jsx_tests.rs index 024c57303..cf5ad2889 100644 --- a/libs/extractor/src/extractor/styled_jsx_tests.rs +++ b/libs/extractor/src/extractor/styled_jsx_tests.rs @@ -22,7 +22,9 @@ fn commonjs_binding_resolves_factory_when_renderer_is_used() { ( Expression::new_identifier(SPAN, "className", &ast), Expression::new_identifier(SPAN, "style", &ast), + Expression::new_string_literal(SPAN, "nav", None, &ast), ), + Expression::new_identifier(SPAN, "rest", &ast), ); // When: the real binding and renderer execute against a CommonJS factory export. program.body.push( @@ -46,7 +48,7 @@ fn commonjs_binding_resolves_factory_when_renderer_is_used() { .as_string() .unwrap_or_else(|| panic!("CommonJS renderer JSON result must be a string")) .to_std_string_escaped(), - r#"{"target":"article","props":{"children":"child","className":"caller","style":{"color":"olive"}}}"#, + r#"{"target":"article","props":{"children":"child","className":"caller","style":{"color":"olive"},"as":"nav"}}"#, ); } diff --git a/libs/extractor/src/extractor/styled_record_evaluation.rs b/libs/extractor/src/extractor/styled_record_evaluation.rs index 6a2ac15c8..6dc227fcf 100644 --- a/libs/extractor/src/extractor/styled_record_evaluation.rs +++ b/libs/extractor/src/extractor/styled_record_evaluation.rs @@ -2,7 +2,7 @@ use oxc_allocator::{CloneIn, FromIn, GetAllocator}; use oxc_ast::{ast::Expression, builder::AstBuilder}; use oxc_span::{GetSpan, SPAN}; -use crate::utils::{call_with_values, is_same_expression}; +use crate::utils::is_same_expression; /// Ordered reads shared by the class and variable expressions of one render. pub(super) struct Evaluations<'b, 'a> { @@ -23,13 +23,7 @@ impl<'b, 'a> Evaluations<'b, 'a> { } pub fn capture(&mut self, value: &Expression<'a>, guard: &Expression<'a>) -> Expression<'a> { - let name = loop { - let name = format!("__devupRecord{}", self.next); - self.next += 1; - if !self.source.contains(&name) { - break name; - } - }; + let name = self.fresh_name(); let value = self.choose(guard, value, &self.undefined()); self.bindings.push((name.clone(), value)); Expression::new_identifier( @@ -39,6 +33,16 @@ impl<'b, 'a> Evaluations<'b, 'a> { ) } + pub fn fresh_name(&mut self) -> String { + loop { + let name = format!("__devupRecord{}", self.next); + self.next += 1; + if !self.source.contains(&name) { + return name; + } + } + } + pub fn boolean(&self, value: bool) -> Expression<'a> { Expression::new_boolean_literal(SPAN, value, self.builder) } @@ -73,76 +77,161 @@ impl<'b, 'a> Evaluations<'b, 'a> { } } -pub(super) fn bind_render<'a>( +fn visit_style_value_expression<'a>( builder: &AstBuilder<'a>, - bindings: Vec<(String, Expression<'a>)>, - mut body: Expression<'a>, -) -> Expression<'a> { - // Dependent arguments must be inside the scope of the preceding read. - for binding in bindings.into_iter().rev() { - body = call_with_values(builder, vec![binding], body); + style: &mut crate::ExtractStyleValue, + visit: &mut impl FnMut(&mut Expression<'a>), +) { + use crate::ExtractStyleValue; + match style { + ExtractStyleValue::Dynamic(value) => { + let mut expression = Expression::new_identifier( + SPAN, + oxc_ast::ast::Ident::from_in(value.identifier(), builder.allocator()), + builder, + ); + visit(&mut expression); + *value.identifier_mut() = crate::utils::readable_code(&expression); + } + ExtractStyleValue::Static(_) + | ExtractStyleValue::Typography(_) + | ExtractStyleValue::Css(_) + | ExtractStyleValue::Import(_) + | ExtractStyleValue::FontFace(_) + | ExtractStyleValue::Keyframes(_) => {} } - body } -#[cfg(test)] -mod tests { - use boa_engine::{Context, Source}; - use oxc_allocator::Allocator; - use oxc_ast::ast::Statement; - use oxc_parser::Parser; - use oxc_span::SourceType; - use rstest::rstest; - - use super::*; - use crate::utils::readable_code; - - #[rstest] - #[case::equal_true(true, true)] - #[case::equal_false(false, false)] - #[case::different_true(true, false)] - #[case::different_false(false, true)] - fn boolean_choice_when_test_is_captured( - #[case] yes: bool, - #[case] no: bool, - #[values(false, true)] on: bool, - ) { - let allocator = Allocator::default(); - let builder = AstBuilder::new(&allocator); - let source = "(trace.push('test'), on);"; - let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse(); - assert_eq!(parsed.diagnostics.len(), 0); - let Statement::ExpressionStatement(statement) = &parsed.program.body[0] else { - panic!("test fixture must be an expression"); - }; - let mut reads = Evaluations::new(&builder, source.to_string()); - let test = reads.capture(&statement.expression, &reads.boolean(true)); - let choice = reads.choose(&test, &reads.boolean(yes), &reads.boolean(no)); - match &choice { - Expression::BooleanLiteral(value) => { - assert_eq!(yes, no); - assert_eq!(value.value, yes); +pub(super) fn visit_style_expressions<'a>( + builder: &AstBuilder<'a>, + style: &mut crate::ExtractStyleProp<'a>, + visit: &mut impl FnMut(&mut Expression<'a>), +) { + use crate::ExtractStyleProp; + match style { + ExtractStyleProp::Static(value) => visit_style_value_expression(builder, value, visit), + ExtractStyleProp::Unreadable { .. } => {} + ExtractStyleProp::StaticArray(styles) => { + for style in styles { + visit_style_expressions(builder, style, visit); + } + } + ExtractStyleProp::Conditional { + condition, + consequent, + alternate, + } => { + visit(condition); + for style in consequent.iter_mut().chain(alternate.iter_mut()) { + visit_style_expressions(builder, style, visit); + } + } + ExtractStyleProp::Enum { condition, map } => { + visit(condition); + for style in map.values_mut().flatten() { + visit_style_expressions(builder, style, visit); + } + } + ExtractStyleProp::Expression { expression, styles } => { + visit(expression); + for style in styles { + visit_style_value_expression(builder, style, visit); } - Expression::ConditionalExpression(_) => assert_ne!(yes, no), - _ => panic!("Boolean choice must be constant or conditional"), } - let render = bind_render(&builder, reads.bindings, choice); - let script = format!( - "const trace = []; const on = {on}; const result = {}; JSON.stringify([result, trace]);", - readable_code(&render) + ExtractStyleProp::MemberExpression { expression, map } => { + visit(expression); + for style in map.values_mut() { + visit_style_expressions(builder, style, visit); + } + } + } +} + +pub(super) fn bind_render<'a>( + builder: &AstBuilder<'a>, + bindings: Vec<(String, Expression<'a>)>, + body: Expression<'a>, +) -> Expression<'a> { + use oxc_allocator::Vec as ArenaVec; + use oxc_ast::ast::{ + ArrowFunctionBody, BindingPattern, FormalParameterKind, FormalParameters, Statement, + VariableDeclaration, VariableDeclarationKind, VariableDeclarator, + }; + if bindings.is_empty() { + return body; + } + // Sibling declarations preserve dependent read order without one scope per capture. + let mut statements = ArenaVec::with_capacity_in(bindings.len() + 1, builder); + for (name, value) in bindings { + let name = oxc_ast::ast::Ident::from_in(name.as_str(), builder.allocator()); + // Sequence evaluation prevents declaration-name inference for anonymous definitions. + let value = Expression::new_sequence_expression( + SPAN, + ArenaVec::from_array_in( + [ + Expression::new_numeric_literal( + SPAN, + 0.0, + None, + oxc_syntax::number::NumberBase::Decimal, + builder, + ), + value, + ], + builder, + ), + builder, ); - let mut context = Context::default(); - let result = match context.eval(Source::from_bytes(script.as_bytes())) { - Ok(result) => result, - Err(error) => panic!("Boolean choice evaluation failed: {error}"), - }; - let result = match result.as_string() { - Some(result) => result.to_std_string_escaped(), - None => panic!("Boolean choice must return a JSON string"), - }; - assert_eq!( - result, - format!(r#"[{},["test"]]"#, if on { yes } else { no }) + let declaration = VariableDeclarator::new( + SPAN, + BindingPattern::new_binding_identifier(SPAN, name, builder), + None, + Some(value), + false, + builder, ); + statements.push(Statement::VariableDeclaration(VariableDeclaration::boxed( + SPAN, + VariableDeclarationKind::Const, + ArenaVec::from_array_in([declaration], builder), + false, + builder, + ))); } + statements.push(Statement::new_return_statement(SPAN, Some(body), builder)); + let arrow = Expression::new_arrow_function_expression( + SPAN, + false, + None, + FormalParameters::boxed( + SPAN, + FormalParameterKind::ArrowFormalParameters, + ArenaVec::new_in(builder), + None, + builder, + ), + None, + ArrowFunctionBody::new_function_body(SPAN, ArenaVec::new_in(builder), statements, builder), + builder, + ); + Expression::new_call_expression( + SPAN, + Expression::new_parenthesized_expression(SPAN, arrow, builder), + None, + ArenaVec::new_in(builder), + false, + builder, + ) } + +#[cfg(test)] +#[path = "styled_record_evaluation_visitor_tests.rs"] +mod visitor_tests; + +#[cfg(test)] +#[path = "styled_record_evaluation_depth_tests.rs"] +mod depth_tests; + +#[cfg(test)] +#[path = "styled_record_evaluation_tests.rs"] +mod tests; diff --git a/libs/extractor/src/extractor/styled_record_evaluation_depth_tests.rs b/libs/extractor/src/extractor/styled_record_evaluation_depth_tests.rs new file mode 100644 index 000000000..77d53d175 --- /dev/null +++ b/libs/extractor/src/extractor/styled_record_evaluation_depth_tests.rs @@ -0,0 +1,185 @@ +use boa_engine::{Context, Source}; +use oxc_allocator::{Allocator, CloneIn}; +use oxc_ast::{ast::Statement, builder::AstBuilder}; +use oxc_parser::Parser; +use oxc_span::SourceType; +use rstest::rstest; + +use super::{Evaluations, bind_render}; +use crate::utils::{call_with_values, readable_code}; + +#[rstest] +fn ordered_captures_when_many_guarded_reads_depend_on_prior_bindings( + #[values(false, true)] outer: bool, + #[values(false, true)] inner: bool, +) { + // Given: shallow authored expressions, reserved names and dependent guarded reads. + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let mut reads = Evaluations::new(&builder, "__devupRecord0 __devupRecord1".to_string()); + let parse = |source: &str| { + let parsed = Parser::new(&allocator, source, SourceType::mjs()).parse(); + assert_eq!(parsed.diagnostics.len(), 0); + let Statement::ExpressionStatement(statement) = &parsed.program.body[0] else { + panic!("capture fixture must be an expression"); + }; + statement.expression.clone_in(&allocator) + }; + let outer_read = reads.capture( + &parse("(trace.push('outer'), outer);"), + &reads.boolean(true), + ); + let inner_read = reads.capture(&parse("(trace.push('inner'), inner);"), &outer_read); + let guard = reads.choose(&outer_read, &inner_read, &reads.boolean(false)); + let mut previous = "-1".to_string(); + let mut values = Vec::new(); + for index in 0..32 { + let value = reads.capture( + &parse(&format!("(trace.push('leaf:{index}'), {previous} + 1);")), + &guard, + ); + previous = readable_code(&value); + values.push(previous.clone()); + } + let body = parse(&format!( + "JSON.stringify([__devupRecord0,__devupRecord1,[{}].filter(v=>v!==undefined),trace]);", + values.join(",") + )); + + // When: parse and execute production binding emission at the default test stack. + let render = bind_render(&builder, reads.bindings, body); + let script = format!( + "const trace=[];const outer={outer},inner={inner},__devupRecord0='authored',__devupRecord1='collision';{};", + readable_code(&render) + ); + let mut context = Context::default(); + let result = context + .eval(Source::from_bytes(script.as_bytes())) + .unwrap_or_else(|error| panic!("dependent capture execution failed: {error}")); + let result = result + .as_string() + .unwrap_or_else(|| panic!("dependent capture observation must be JSON")) + .to_std_string_escaped(); + + // Then: every selected read runs once in order; absent branches never read leaves. + let mut trace = vec!["outer".to_string()]; + let mut expected_values = Vec::new(); + if outer { + trace.push("inner".to_string()); + if inner { + for index in 0..32 { + trace.push(format!("leaf:{index}")); + expected_values.push(index); + } + } + } + let expected = serde_json::to_string(&("authored", "collision", expected_values, trace)) + .unwrap_or_else(|error| panic!("capture observation serialization failed: {error}")); + assert_eq!(result, expected); +} + +#[rstest] +#[case::class("class {static {trace.push(this.name)}}", "")] +#[case::function("function(){trace.push(arguments.callee.name)}", "controller();")] +fn anonymous_names_when_prepared_controller_is_bound( + #[case] initializer: &str, + #[case] observe: &str, + #[values(false, true)] bounded: bool, +) { + // Given: the authored styled conditional spread reaches typed preparation. + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let source = format!("styled.div({{color:'orange',...({initializer}?{{color:'red'}}:{{}})}});"); + let parsed = Parser::new(&allocator, &source, SourceType::tsx()).parse(); + assert_eq!(parsed.diagnostics.len(), 0); + let Statement::ExpressionStatement(statement) = &parsed.program.body[0] else { + panic!("styled fixture must be an expression"); + }; + let oxc_ast::ast::Expression::CallExpression(call) = &statement.expression else { + panic!("styled fixture must be a call"); + }; + let rules = call.arguments[0].to_expression(); + let prepared = super::super::styled_record::prepare(&builder, rules, &statement.expression) + .unwrap_or_else(|| panic!("anonymous controller must require preparation")); + assert_eq!(prepared.bindings.len(), 1); + let body_source = format!( + "((record,controller)=>{{{observe}return JSON.stringify([record.color,controller.name,trace]);}})({},{});", + readable_code(&prepared.rules), + prepared.bindings[0].0 + ); + let body = Parser::new(&allocator, &body_source, SourceType::tsx()).parse(); + assert_eq!(body.diagnostics.len(), 0); + let Statement::ExpressionStatement(body) = &body.program.body[0] else { + panic!("observation must be an expression"); + }; + + // When: execute either the old argument seam or the production bounded binder. + let body = body.expression.clone_in(&allocator); + let render = if bounded { + bind_render(&builder, prepared.bindings, body) + } else { + call_with_values(&builder, prepared.bindings, body) + }; + let script = format!("const trace=[];{};", readable_code(&render)); + let mut context = Context::default(); + let result = context + .eval(Source::from_bytes(script.as_bytes())) + .unwrap_or_else(|error| panic!("prepared anonymous controller execution failed: {error}")); + let result = result + .as_string() + .unwrap_or_else(|| panic!("anonymous controller observation must be JSON")) + .to_std_string_escaped(); + + // Then: later color wins and static/self-name observers see the original empty name. + assert_eq!(result, r#"["red","",[""]]"#); +} + +#[rstest] +#[case::class("class {static {trace.push(this.name)}}", "")] +#[case::function("function(){trace.push(arguments.callee.name)}", "value();")] +fn anonymous_names_when_capture_initializer_is_bound( + #[case] initializer: &str, + #[case] observe: &str, + #[values(false, true)] bounded: bool, +) { + // Given: an anonymous expression captured under the production literal-true guard. + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let source = format!("({initializer});"); + let parsed = Parser::new(&allocator, &source, SourceType::tsx()).parse(); + assert_eq!(parsed.diagnostics.len(), 0); + let Statement::ExpressionStatement(statement) = &parsed.program.body[0] else { + panic!("anonymous fixture must be an expression"); + }; + let mut reads = Evaluations::new(&builder, source.clone()); + let value = reads.capture(&statement.expression, &reads.boolean(true)); + let body_source = format!( + "(value=>{{{observe}return JSON.stringify([value.name,trace]);}})({});", + readable_code(&value) + ); + let body = Parser::new(&allocator, &body_source, SourceType::tsx()).parse(); + assert_eq!(body.diagnostics.len(), 0); + let Statement::ExpressionStatement(body) = &body.program.body[0] else { + panic!("observation must be an expression"); + }; + + // When: execute either the old argument seam or the production bounded binder. + let body = body.expression.clone_in(&allocator); + let render = if bounded { + bind_render(&builder, reads.bindings, body) + } else { + call_with_values(&builder, reads.bindings, body) + }; + let script = format!("const trace=[];{};", readable_code(&render)); + let mut context = Context::default(); + let result = context + .eval(Source::from_bytes(script.as_bytes())) + .unwrap_or_else(|error| panic!("anonymous capture execution failed: {error}")); + let result = result + .as_string() + .unwrap_or_else(|| panic!("anonymous capture observation must be JSON")) + .to_std_string_escaped(); + + // Then: both the value's name and its authored observer retain argument semantics. + assert_eq!(result, r#"["",[""]]"#); +} diff --git a/libs/extractor/src/extractor/styled_record_evaluation_tests.rs b/libs/extractor/src/extractor/styled_record_evaluation_tests.rs new file mode 100644 index 000000000..f6c326ec6 --- /dev/null +++ b/libs/extractor/src/extractor/styled_record_evaluation_tests.rs @@ -0,0 +1,58 @@ +use boa_engine::{Context, Source}; +use oxc_allocator::Allocator; +use oxc_ast::ast::Statement; +use oxc_parser::Parser; +use oxc_span::SourceType; +use rstest::rstest; + +use super::*; +use crate::utils::readable_code; + +#[rstest] +#[case::equal_true(true, true)] +#[case::equal_false(false, false)] +#[case::different_true(true, false)] +#[case::different_false(false, true)] +fn boolean_choice_when_test_is_captured( + #[case] yes: bool, + #[case] no: bool, + #[values(false, true)] on: bool, +) { + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let source = "(trace.push('test'), on);"; + let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse(); + assert_eq!(parsed.diagnostics.len(), 0); + let Statement::ExpressionStatement(statement) = &parsed.program.body[0] else { + panic!("test fixture must be an expression"); + }; + let mut reads = Evaluations::new(&builder, source.to_string()); + let test = reads.capture(&statement.expression, &reads.boolean(true)); + let choice = reads.choose(&test, &reads.boolean(yes), &reads.boolean(no)); + match &choice { + Expression::BooleanLiteral(value) => { + assert_eq!(yes, no); + assert_eq!(value.value, yes); + } + Expression::ConditionalExpression(_) => assert_ne!(yes, no), + _ => panic!("Boolean choice must be constant or conditional"), + } + let render = bind_render(&builder, reads.bindings, choice); + let script = format!( + "const trace = []; const on = {on}; const result = {}; JSON.stringify([result, trace]);", + readable_code(&render) + ); + let mut context = Context::default(); + let result = match context.eval(Source::from_bytes(script.as_bytes())) { + Ok(result) => result, + Err(error) => panic!("Boolean choice evaluation failed: {error}"), + }; + let result = match result.as_string() { + Some(result) => result.to_std_string_escaped(), + None => panic!("Boolean choice must return a JSON string"), + }; + assert_eq!( + result, + format!(r#"[{},["test"]]"#, if on { yes } else { no }) + ); +} diff --git a/libs/extractor/src/extractor/styled_record_evaluation_visitor_tests.rs b/libs/extractor/src/extractor/styled_record_evaluation_visitor_tests.rs new file mode 100644 index 000000000..8cc7b8dcf --- /dev/null +++ b/libs/extractor/src/extractor/styled_record_evaluation_visitor_tests.rs @@ -0,0 +1,301 @@ +use std::collections::BTreeMap; + +use oxc_allocator::{Allocator, FromIn, GetAllocator}; +use oxc_ast::{ast::Expression, builder::AstBuilder}; +use oxc_span::SPAN; +use rstest::rstest; + +use super::visit_style_expressions; +use crate::{ + ExtractStyleProp, ExtractStyleValue, + extract_style::{ + extract_dynamic_style::ExtractDynamicStyle, extract_static_style::ExtractStaticStyle, + }, + utils::readable_code, +}; + +#[derive(Clone, Copy)] +enum Container { + Conditional, + StaticArray, + Enum, + Expression, + MemberExpression, +} + +fn identifier<'a>(builder: &AstBuilder<'a>, name: &str) -> Expression<'a> { + Expression::new_identifier( + SPAN, + oxc_ast::ast::Ident::from_in(name, builder.allocator()), + builder, + ) +} + +fn payload<'a>(builder: &AstBuilder<'a>, name: &str) -> ExtractStyleProp<'a> { + ExtractStyleProp::Expression { + expression: identifier(builder, name), + styles: vec![ + ExtractStyleValue::Dynamic(ExtractDynamicStyle::new("color", 2, name, None)), + ExtractStyleValue::Dynamic(ExtractDynamicStyle::new( + "background", + 4, + &format!("{name} + {name}.color !important"), + None, + )), + ExtractStyleValue::Static(ExtractStaticStyle::new( + "--authored", + "__devupRecord0", + 3, + None, + )), + ], + } +} + +fn assert_identifier(expression: &Expression<'_>, expected: &str) { + let Expression::Identifier(identifier) = expression else { + panic!("visitor must preserve the identifier expression"); + }; + assert_eq!(identifier.name.as_str(), expected); + assert_eq!(readable_code(expression), expected); +} + +fn assert_payload(style: &ExtractStyleProp<'_>, expected: &str) { + let ExtractStyleProp::Expression { expression, styles } = style else { + panic!("fixture payload must remain an Expression"); + }; + assert_identifier(expression, expected); + assert_eq!(styles.len(), 3); + let ExtractStyleValue::Dynamic(simple) = &styles[0] else { + panic!("first payload must remain dynamic"); + }; + assert_eq!(simple.identifier(), expected); + assert_eq!( + simple, + &ExtractDynamicStyle::new("color", 2, expected, None) + ); + let ExtractStyleValue::Dynamic(code) = &styles[1] else { + panic!("second payload must remain dynamic code"); + }; + assert_eq!(code.identifier(), format!("{expected} + {expected}.color")); + assert_eq!( + code, + &ExtractDynamicStyle::new( + "background", + 4, + &format!("{expected} + {expected}.color !important"), + None, + ) + ); + assert_eq!( + styles[2], + ExtractStyleValue::Static(ExtractStaticStyle::new( + "--authored", + "__devupRecord0", + 3, + None, + )) + ); +} + +#[test] +#[should_panic(expected = "visitor must preserve the identifier expression")] +fn identifier_guard_rejects_when_expression_is_null() { + // Given: an AST expression with the wrong shape. + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let expression = Expression::new_null_literal(SPAN, &builder); + // When/Then: the existing identifier assertion rejects it at its shape guard. + assert_identifier(&expression, "__devupRecord0"); +} + +#[test] +#[should_panic(expected = "fixture payload must remain an Expression")] +fn payload_guard_rejects_when_container_is_an_array() { + // Given: a non-Expression container. + let style = ExtractStyleProp::StaticArray(vec![]); + // When/Then: the existing payload assertion rejects the container shape. + assert_payload(&style, "__devupRecord0"); +} + +#[test] +#[should_panic(expected = "first payload must remain dynamic")] +fn payload_guard_rejects_when_first_declaration_is_static() { + // Given: a valid payload with only its first declaration's shape changed. + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let mut style = payload(&builder, "__devupRecord0"); + if let ExtractStyleProp::Expression { styles, .. } = &mut style { + styles[0] = styles[2].clone(); + } + // When/Then: earlier identifier/length checks pass, then the first guard rejects. + assert_payload(&style, "__devupRecord0"); +} + +#[test] +#[should_panic(expected = "second payload must remain dynamic code")] +fn payload_guard_rejects_when_second_declaration_is_static() { + // Given: a valid payload with only its second declaration's shape changed. + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let mut style = payload(&builder, "__devupRecord0"); + if let ExtractStyleProp::Expression { styles, .. } = &mut style { + styles[1] = styles[2].clone(); + } + // When/Then: the first declaration checks pass, then the second guard rejects. + assert_payload(&style, "__devupRecord0"); +} + +#[rstest] +#[case::conditional(Container::Conditional)] +#[case::static_array(Container::StaticArray)] +#[case::enum_map(Container::Enum)] +#[case::expression(Container::Expression)] +#[case::member_map(Container::MemberExpression)] +fn payloads_are_rewritten_when_style_expressions_are_visited(#[case] container: Container) { + // Given: two distinct nested captures, including serialized dynamic code. + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let first = payload(&builder, "__devupRecord0"); + let second = payload(&builder, "__devupRecord1"); + let control = identifier(&builder, "__devupRecord2"); + let mut style = match container { + Container::Conditional => ExtractStyleProp::Conditional { + condition: control, + consequent: Some(Box::new(first)), + alternate: Some(Box::new(second)), + }, + Container::StaticArray => ExtractStyleProp::StaticArray(vec![first, second]), + Container::Enum => ExtractStyleProp::Enum { + condition: control, + map: BTreeMap::from([ + ("__devupRecord0".to_string(), vec![first, second]), + ("empty".to_string(), vec![]), + ]), + }, + Container::Expression => first, + Container::MemberExpression => ExtractStyleProp::MemberExpression { + expression: control, + map: BTreeMap::from([ + ("__devupRecord0".to_string(), Box::new(first)), + ("second".to_string(), Box::new(second)), + ]), + }, + }; + + // When: the callback rewrites actual AST identifier references/code payloads. + visit_style_expressions(&builder, &mut style, &mut |expression| { + let Expression::Identifier(value) = expression else { + panic!("fixture visitor receives identifier expressions"); + }; + let renamed = value.name.replace("__devupRecord", "__devupInherited"); + value.name = oxc_ast::ast::Ident::from_in(renamed.as_str(), builder.allocator()); + }); + + // Then: every branch/map/array entry and AST control was rewritten. + match &style { + ExtractStyleProp::Conditional { + condition, + consequent, + alternate, + } => { + assert_identifier(condition, "__devupInherited2"); + let (Some(consequent), Some(alternate)) = (consequent, alternate) else { + panic!("both conditional branches must remain present"); + }; + assert_payload(consequent, "__devupInherited0"); + assert_payload(alternate, "__devupInherited1"); + } + ExtractStyleProp::StaticArray(styles) => { + assert_eq!(styles.len(), 2); + assert_payload(&styles[0], "__devupInherited0"); + assert_payload(&styles[1], "__devupInherited1"); + } + ExtractStyleProp::Enum { condition, map } => { + assert_identifier(condition, "__devupInherited2"); + assert_eq!(map.len(), 2); + assert_eq!(map["empty"].len(), 0); + assert_payload(&map["__devupRecord0"][0], "__devupInherited0"); + assert_payload(&map["__devupRecord0"][1], "__devupInherited1"); + } + ExtractStyleProp::Expression { .. } => assert_payload(&style, "__devupInherited0"), + ExtractStyleProp::MemberExpression { expression, map } => { + assert_identifier(expression, "__devupInherited2"); + assert_eq!(map.len(), 2); + assert_payload(&map["__devupRecord0"], "__devupInherited0"); + assert_payload(&map["second"], "__devupInherited1"); + } + ExtractStyleProp::Static(_) | ExtractStyleProp::Unreadable { .. } => { + panic!("container must remain unchanged"); + } + } +} + +#[test] +fn metadata_is_unchanged_when_style_expressions_are_visited() { + // Given: static declarations and unreadable diagnostics are not expressions. + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let static_value = ExtractStyleValue::Static(ExtractStaticStyle::new( + "--authored", + "__devupRecord0", + 3, + None, + )); + let mut style = ExtractStyleProp::StaticArray(vec![ + ExtractStyleProp::Static(static_value.clone()), + ExtractStyleProp::Unreadable { + offset: 17, + code: "__devupRecord0".to_string(), + prop: true, + requirement: Some("a valid selector"), + }, + ExtractStyleProp::Static(ExtractStyleValue::Dynamic(ExtractDynamicStyle::new( + "color", + 2, + "__devupRecord0", + None, + ))), + ]); + + // When: only the dynamic declaration is visited. + visit_style_expressions(&builder, &mut style, &mut |expression| { + assert_identifier(expression, "__devupRecord0"); + *expression = identifier(&builder, "__devupInherited0"); + }); + + // Then: metadata stays byte-identical and the dynamic declaration changes. + let ExtractStyleProp::StaticArray(styles) = style else { + panic!("array fixture"); + }; + let ExtractStyleProp::Static(value) = &styles[0] else { + panic!("static fixture"); + }; + assert_eq!(value, &static_value); + let ExtractStyleProp::Unreadable { + offset, + code, + prop, + requirement, + } = &styles[1] + else { + panic!("unreadable fixture"); + }; + assert_eq!( + (*offset, code.as_str(), *prop), + (17, "__devupRecord0", true) + ); + assert_eq!(*requirement, Some("a valid selector")); + let ExtractStyleProp::Static(value) = &styles[2] else { + panic!("dynamic fixture"); + }; + assert_eq!( + value, + &ExtractStyleValue::Dynamic(ExtractDynamicStyle::new( + "color", + 2, + "__devupInherited0", + None, + )) + ); +} diff --git a/libs/extractor/src/extractor/styled_record_tests.rs b/libs/extractor/src/extractor/styled_record_tests.rs index 4f999f15d..2a46c72a0 100644 --- a/libs/extractor/src/extractor/styled_record_tests.rs +++ b/libs/extractor/src/extractor/styled_record_tests.rs @@ -9,6 +9,206 @@ use rstest::rstest; use super::{styled_record::prepare, styled_record_evaluation::bind_render}; use crate::utils::readable_code; +#[rstest] +#[case::prepared_base("const Ext=styled(Base)({'p':1});", "", "4px")] +#[case::two_levels( + "const Mid=styled(Base)({'p':1});const Ext=styled(Mid)({'p':2});", + "", + "8px" +)] +#[case::tagged_extension("const Ext=styled(Base)`padding:4px`;", "", "4px")] +#[case::retargeted_extension( + "const Mid=Base.withComponent('section');const Ext=styled(Mid)({'p':1});", + "", + "4px" +)] +#[case::capture_collisions( + "const Mid=styled(Base)({backgroundColor:'white',...((trace.push('own'),own)?{backgroundColor:'black'}:{})});const Ext=styled(Mid)(__devupRecord0,{p:1,...((trace.push('authored'),__devupRecord1)?{p:2}:{})});", + ",own,__devupRecord0,__devupRecord1", + "8px" +)] +#[serial_test::serial] +fn inherited_captures_when_native_extension_renders( + #[case] extension: &str, + #[case] parameters: &str, + #[case] padding: &str, + #[values(false, true)] on: bool, + #[values(false, true)] authored: bool, + #[values(false, true)] single_css: bool, +) { + // Given: independent inherited, own and authored controllers. + reset_class_map(); + reset_file_map(); + let code = format!( + "import {{styled}} from '@devup-ui/react';const trace=[];function make(on{parameters}){{const Base=styled.div({{color:'red',...((trace.push('base'),on)?{{color:'blue'}}:{{}})}});{extension}return Ext({{id:'extension',className:'caller'}});}}const rendered=make({on},{},'authored',{authored});", + !on + ); + let output = match crate::extract( + "test.tsx", + &code, + crate::ExtractOption { + single_css, + ..crate::ExtractOption::default() + }, + ) { + Ok(output) => output, + Err(error) => panic!("extension fixture must extract: {error}"), + }; + let mut declarations = Vec::new(); + for style in &output.styles { + let crate::ExtractStyleValue::Static(value) = style else { + panic!("fixture declarations must be static"); + }; + let Some(crate::extract_style::style_property::StyleProperty::ClassName(name)) = + style.extract(if single_css { None } else { Some("test.tsx") }) + else { + panic!("fixture declaration must have a class"); + }; + declarations.push(format!( + "['{name}','{}','{}']", + value.property(), + value.value() + )); + } + // When: execute the emitted native render, not a generated-name assertion. + let rendered = evaluate_native_render( + &output.code, + &format!( + "const names=rendered.props.className.split(' ');const styles=[{}].filter(([name])=>names.includes(name)).map(([,prop,value])=>[prop,value]).sort();JSON.stringify([rendered.type,rendered.props.id,styles,names.filter(name=>name==='authored'||name==='caller'),trace]);", + declarations.join(",") + ), + ); + // Then: only the selected declarations survive, with ordered single reads. + let color = if on { "blue" } else { "red" }; + let target = if extension.contains("withComponent") { + "section" + } else { + "div" + }; + let expected = if parameters.is_empty() { + format!( + r#"["{target}","extension",[["color","{color}"],["padding","{padding}"]],["caller"],["base"]]"# + ) + } else { + let background = if on { "white" } else { "black" }; + let padding = if authored { padding } else { "4px" }; + format!( + r#"["div","extension",[["background-color","{background}"],["color","{color}"],["padding","{padding}"]],["authored","caller"],["base","own","authored"]]"# + ) + }; + assert_eq!(rendered, expected); +} + +#[rstest] +#[serial_test::serial] +fn inherited_captures_when_dynamic_native_base_renders( + #[values(false, true)] on: bool, + #[values(false, true)] single_css: bool, +) { + // Given: a lowercase runtime target binding and an inherited captured guard. + reset_class_map(); + reset_file_map(); + let code = format!( + "import {{styled}} from '@devup-ui/react';const trace=[];function make(on,target){{const Base=styled(target)({{color:'red',...((trace.push('base'),on)?{{color:'blue'}}:{{}})}});const Ext=styled(Base)({{p:1}});return Ext({{id:'extension',className:'caller'}});}}const rendered=make({on},'div');" + ); + let output = match crate::extract( + "test.tsx", + &code, + crate::ExtractOption { + single_css, + ..crate::ExtractOption::default() + }, + ) { + Ok(output) => output, + Err(error) => panic!("dynamic base extension must extract: {error}"), + }; + let mut declarations = Vec::new(); + for style in &output.styles { + let crate::ExtractStyleValue::Static(value) = style else { + panic!("fixture declarations must be static"); + }; + let Some(crate::extract_style::style_property::StyleProperty::ClassName(name)) = + style.extract(if single_css { None } else { Some("test.tsx") }) + else { + panic!("fixture declaration must have a class"); + }; + declarations.push(format!( + "['{name}','{}','{}']", + value.property(), + value.value() + )); + } + // When: execute the emitted extension with its actual runtime tag value. + let rendered = evaluate_native_render( + &output.code, + &format!( + "const names=rendered.props.className.split(' ');const styles=[{}].filter(([name])=>names.includes(name)).map(([,prop,value])=>[prop,value]).sort();JSON.stringify([rendered.type,rendered.props.id,styles,names.filter(name=>name==='caller'),trace]);", + declarations.join(",") + ), + ); + // Then: the tag, selected color, extension padding and single read survive. + let color = if on { "blue" } else { "red" }; + assert_eq!( + rendered, + format!( + r#"["div","extension",[["color","{color}"],["padding","4px"]],["caller"],["base"]]"# + ) + ); +} + +#[rstest] +#[serial_test::serial] +fn inherited_dependent_reads_when_attrs_and_later_styles_compose( + #[values(false, true)] outer: bool, + #[values(false, true)] inner: bool, + #[values(false, true)] single_css: bool, +) { + // Given: nested captured guards, a dynamic leaf and an authored attrs name. + reset_class_map(); + reset_file_map(); + let code = format!( + "import {{styled}} from '@devup-ui/react';const trace=[];function make(outer,inner,__devupRecord0){{const Base=styled.div.attrs({{title:__devupRecord0,id:'base'}})({{color:'red',backgroundColor:'white',...((trace.push('outer'),outer)?{{...((trace.push('inner'),inner)?{{backgroundColor:(trace.push('leaf'),'gold')}}:{{}})}}:{{}})}});const Ext=styled(Base).attrs({{id:'extension'}})({{color:'orange'}});return Ext({{}});}}const rendered=make({outer},{inner},'authored-title');" + ); + let output = match crate::extract( + "test.tsx", + &code, + crate::ExtractOption { + single_css, + ..crate::ExtractOption::default() + }, + ) { + Ok(output) => output, + Err(error) => panic!("dependent extension must extract: {error}"), + }; + let mut colors = Vec::new(); + for style in &output.styles { + if let crate::ExtractStyleValue::Static(value) = style + && value.property() == "color" + && let Some(crate::extract_style::style_property::StyleProperty::ClassName(name)) = + style.extract(if single_css { None } else { Some("test.tsx") }) + { + colors.push(format!("['{name}','{}']", value.value())); + } + } + // When: render the flattened extension through the existing native harness. + let rendered = evaluate_native_render( + &output.code, + &format!( + "const names=rendered.props.className.split(' ');JSON.stringify([rendered.props.id,rendered.props.title,[{}].filter(([name])=>names.includes(name)).map(([,value])=>value),Object.values(rendered.props.style||{{}}).filter(value=>value!=null),trace]);", + colors.join(",") + ), + ); + // Then: guarded reads stay single and ordered, attrs and later color win. + let expected = match (outer, inner) { + (true, true) => { + r#"["extension","authored-title",["orange"],["gold"],["outer","inner","leaf"]]"# + } + (true, false) => r#"["extension","authored-title",["orange"],[],["outer","inner"]]"#, + (false, _) => r#"["extension","authored-title",["orange"],[],["outer"]]"#, + }; + assert_eq!(rendered, expected); +} + fn evaluate_record(rules: &str, setup: &str) -> String { let allocator = Allocator::default(); let builder = AstBuilder::new(&allocator); @@ -287,7 +487,7 @@ fn original_record_when_unaffected_or_unreadable(#[case] rules: &str) { assert_eq!(readable_code(&statement.expression), original); } -/// Evaluate the emitted render scope, lowering only its empty native JSX element. +/// Evaluate the emitted render scope, lowering its empty native or bound JSX element. fn evaluate_native_render(code: &str, observation: &str) -> String { use oxc_ast::ast::{ ImportDeclaration, JSXAttributeItem, JSXAttributeName, JSXAttributeValue, JSXElement, @@ -306,10 +506,17 @@ fn evaluate_native_render(code: &str, observation: &str) -> String { fn visit_jsx_element(&mut self, element: &JSXElement<'a>) { assert_eq!(element.children.len(), 0); - let JSXElementName::Identifier(tag) = &element.opening_element.name else { - panic!("fixture must render a native element"); + let tag = match &element.opening_element.name { + JSXElementName::Identifier(tag) => { + assert_eq!(tag.name, "div"); + "'div'" + } + JSXElementName::IdentifierReference(tag) => { + assert!(["DevupStyled", "DevupAs"].contains(&tag.name.as_str())); + tag.name.as_str() + } + _ => panic!("fixture must render a native element"), }; - assert_eq!(tag.name, "div"); let props: Vec<_> = element .opening_element .attributes @@ -336,7 +543,7 @@ fn evaluate_native_render(code: &str, observation: &str) -> String { .collect(); self.0.push(( element.span, - format!("({{type:'div',props:{{{}}}}})", props.join(",")), + format!("({{type:{tag},props:{{{}}}}})", props.join(",")), )); } } @@ -369,6 +576,113 @@ fn evaluate_native_render(code: &str, observation: &str) -> String { } } +#[rstest] +#[serial_test::serial] +fn captures_and_filter_survive_when_prepared_base_retargets_then_extends( + #[values("test.tsx", "test.ts")] filename: &str, + #[values(false, true)] on: bool, + #[values(false, true)] single_css: bool, +) { + // Given: conditional captures, prop-reading attrs and an inherited filter. + reset_class_map(); + reset_file_map(); + let source = format!( + "import {{styled}} from '@devup-ui/react';const trace=[];const on={on};const Base=styled.div.attrs(p=>({{'data-tone':p.tone}})).withConfig({{shouldForwardProp:p=>p!=='tone'&&p!=='blocked'}})({{color:'red',...((trace.push('base'),on)?{{color:'blue'}}:{{}})}});const Mid=Base.withComponent('section');const Card=styled(Mid)({{p:1}});" + ); + let output = match crate::extract( + filename, + &source, + crate::ExtractOption { + single_css, + ..crate::ExtractOption::default() + }, + ) { + Ok(output) => output, + Err(error) => panic!("prepared filter extraction failed: {error}"), + }; + let color_classes: Vec<_> = output + .styles + .iter() + .filter_map(|style| match style { + crate::ExtractStyleValue::Static(value) + if value.property() == "color" + && value.value() == if on { "blue" } else { "red" } => + { + match style.extract(if single_css { None } else { Some(filename) }) { + Some(crate::extract_style::style_property::StyleProperty::ClassName(name)) => { + Some(name) + } + _ => panic!("selected color must have a class"), + } + } + _ => None, + }) + .collect(); + assert_ne!(color_classes.len(), 0); + let winning = serde_json::to_string(&color_classes) + .unwrap_or_else(|error| panic!("class fixture serialization failed: {error}")); + // When: the emitted extension renders with distinct input, selected target and forwarded target. + let observation = format!( + "const __devupJsx0=(type,props)=>({{type,props}});const rendered=Card({{tone:'olive',blocked:9,id:'card',ref:7,children:'child',className:'caller',as:'article',forwardedAs:'nav'}});JSON.stringify([rendered.type,rendered.props.as,rendered.props.id,rendered.props.ref,rendered.props.children,rendered.props.className.includes('caller'),{winning}.some(c=>rendered.props.className.split(' ').includes(c)),rendered.props['data-tone'],'tone' in rendered.props,'blocked' in rendered.props,trace]);" + ); + let result = evaluate_native_render(&output.code, &observation); + // Then: captures run once, attrs reads survive, filtered props disappear and identity/ordinary props remain. + assert_eq!( + result, + r#"["article","nav","card",7,"child",true,true,"olive",false,false,["base"]]"# + ); +} + +#[rstest] +#[serial_test::serial] +fn transient_inputs_survive_when_prepared_typescript_renderer_forwards_rest( + #[values(false, true)] single_css: bool, +) { + // Given: inherited captures, attrs producing unknown transient keys and a transient style input. + reset_class_map(); + reset_file_map(); + let source = "import {styled} from '@devup-ui/react';const trace=[];const Base=styled.div.attrs(p=>({...p.extra,'data-tone':p.$tone})).withConfig({shouldForwardProp:p=>p!=='blocked'})({color:'red',...((trace.push('base'),true)?{color:'blue'}:{})});const Mid=Base.withComponent('section');const Card=styled(Mid)`background: ${p=>p.$tone};`;"; + let output = crate::extract( + "test.ts", + source, + crate::ExtractOption { + single_css, + ..crate::ExtractOption::default() + }, + ) + .unwrap_or_else(|error| panic!("transient TS extraction failed: {error}")); + let winning: Vec<_> = output + .styles + .iter() + .filter_map(|style| match style { + crate::ExtractStyleValue::Static(value) + if value.property() == "color" && value.value() == "blue" => + { + match style.extract(if single_css { None } else { Some("test.ts") }) { + Some(crate::extract_style::style_property::StyleProperty::ClassName(name)) => { + Some(name) + } + _ => panic!("selected color must have a class"), + } + } + _ => None, + }) + .collect(); + assert_ne!(winning.len(), 0); + let winning = serde_json::to_string(&winning) + .unwrap_or_else(|error| panic!("class fixture serialization failed: {error}")); + // When: the emitted non-JSX renderer receives keys absent from the source and renders once. + let observation = format!( + "const __devupJsx0=(type,props)=>({{type,props}});const input={{$runtimeOnly:'secret',$tone:'orchid',extra:{{$fromAttrs:'secret',title:'attrs-title'}},blocked:9,id:'card',ref:7,children:'child',className:'caller',style:{{opacity:.25}},as:'article',forwardedAs:'nav'}};const before=JSON.stringify(input);const rendered=Card(input);JSON.stringify([Object.keys(rendered.props).filter(k=>k.startsWith('$')),Object.values(rendered.props.style).includes('orchid'),rendered.type,rendered.props.as,rendered.props.id,rendered.props.ref,rendered.props.children,rendered.props.className.includes('caller'),{winning}.some(c=>rendered.props.className.split(' ').includes(c)),rendered.props['data-tone'],rendered.props.title,rendered.props.style.opacity,'blocked' in rendered.props,before===JSON.stringify(input),trace]);" + ); + let result = evaluate_native_render(&output.code, &observation); + // Then: forwarding alone is filtered; original reads, classes, captures and caller props survive. + assert_eq!( + result, + r#"[[],true,"article","nav","card",7,"child",true,true,"orchid","attrs-title",0.25,false,true,["base"]]"# + ); +} + #[rstest] #[serial_test::serial] fn template_key_precedence_when_public_component_renders( diff --git a/libs/extractor/src/extractor/styled_transient.rs b/libs/extractor/src/extractor/styled_transient.rs new file mode 100644 index 000000000..8136603bd --- /dev/null +++ b/libs/extractor/src/extractor/styled_transient.rs @@ -0,0 +1,100 @@ +use oxc_allocator::Vec; +use oxc_ast::{ + ast::{ + BindingPattern, Expression, FormalParameter, FormalParameterKind, FormalParameters, + IdentifierName, StaticMemberExpression, + }, + builder::AstBuilder, +}; +use oxc_span::SPAN; +use oxc_syntax::{number::NumberBase, operator::BinaryOperator}; + +/// Filter only the forwarded copy, leaving styled reads on the original rest object. +pub(super) fn filter<'a>(builder: &AstBuilder<'a>, props: Expression<'a>) -> Expression<'a> { + let key = Expression::new_computed_member_expression( + SPAN, + Expression::new_identifier(SPAN, "k", builder), + Expression::new_numeric_literal(SPAN, 0.0, None, NumberBase::Decimal, builder), + false, + builder, + ); + let body = Expression::new_binary_expression( + SPAN, + key, + BinaryOperator::StrictInequality, + Expression::new_string_literal(SPAN, "$", None, builder), + builder, + ); + let pattern = BindingPattern::new_array_pattern( + SPAN, + Vec::from_array_in( + [Some(BindingPattern::new_binding_identifier( + SPAN, "k", builder, + ))], + builder, + ), + None, + builder, + ); + let parameter = FormalParameter::new( + SPAN, + Vec::new_in(builder), + pattern, + None::<oxc_allocator::Box<oxc_ast::ast::TSTypeAnnotation<'a>>>, + None::<oxc_allocator::Box<Expression<'a>>>, + false, + None, + false, + false, + builder, + ); + let params = FormalParameters::boxed( + SPAN, + FormalParameterKind::ArrowFormalParameters, + Vec::from_array_in([parameter], builder), + None::<oxc_allocator::Box<oxc_ast::ast::FormalParameterRest<'a>>>, + builder, + ); + let predicate = Expression::new_arrow_function_expression( + SPAN, + false, + None::<oxc_allocator::Box<oxc_ast::ast::TSTypeParameterDeclaration<'a>>>, + params, + None::<oxc_allocator::Box<oxc_ast::ast::TSTypeAnnotation<'a>>>, + body.into(), + builder, + ); + let object = || Expression::new_identifier(SPAN, "Object", builder); + let entries = call(builder, member(builder, object(), "entries"), props); + let filtered = call(builder, member(builder, entries, "filter"), predicate); + call(builder, member(builder, object(), "fromEntries"), filtered) +} + +fn member<'a>( + builder: &AstBuilder<'a>, + object: Expression<'a>, + name: &'static str, +) -> Expression<'a> { + Expression::StaticMemberExpression(StaticMemberExpression::boxed( + SPAN, + object, + IdentifierName::new(SPAN, name, builder), + false, + builder, + )) +} + +fn call<'a>( + builder: &AstBuilder<'a>, + callee: Expression<'a>, + argument: Expression<'a>, +) -> Expression<'a> { + Expression::new_call_expression( + SPAN, + callee, + None::<oxc_allocator::Box<oxc_ast::ast::TSTypeParameterInstantiation<'a>>>, + Vec::from_array_in([argument.into()], builder), + false, + builder, + ) +} diff --git a/libs/extractor/src/imported_constants.rs b/libs/extractor/src/imported_constants.rs index a600d1deb..82ad48295 100644 --- a/libs/extractor/src/imported_constants.rs +++ b/libs/extractor/src/imported_constants.rs @@ -21,8 +21,13 @@ use oxc_syntax::operator::BinaryOperator; use oxc_syntax::symbol::SymbolId; use rustc_hash::{FxHashMap, FxHashSet}; +use crate::composition::{Composition, set_prop_order}; +use crate::extractor::ExtractResult; +use crate::extractor::extract_style_from_expression::{ + LiteralHandling, extract_style_from_expression, +}; use crate::stylex::StylexFunction; -use crate::{ExtractOption, ModuleResolver}; +use crate::{ExtractOption, ExtractStyleValue, ModuleResolver}; #[derive(Clone, Debug)] enum Constant { @@ -44,8 +49,9 @@ enum Constant { /// The class a `StyleX` theme applies Theme(String), /// What another style API gives: a class, a component or a keyframes - /// name, never rules - Style, + /// name, never rules; for a `css()` class, the styles behind it when + /// they are known + Style(Option<Rc<Vec<ExtractStyleValue>>>), /// An object or array code changes, or a value read from one Changed(Rc<Change>), } @@ -154,6 +160,8 @@ pub(crate) struct Inlined { pub dependencies: BTreeSet<String>, pub stylex_vars: FxHashMap<String, FxHashMap<String, String>>, pub stylex_themes: FxHashMap<String, String>, + /// The styles behind imported `css()` classes + pub css_styles: FxHashMap<String, Vec<ExtractStyleValue>>, pub unknown: Unknown, pub changed: Changed, } @@ -471,6 +479,11 @@ pub(crate) fn inline_constants<'a>( Constant::Theme(class) => { inlined.stylex_themes.insert(name.clone(), class.clone()); } + Constant::Style(Some(styles)) => { + inlined + .css_styles + .insert(name.clone(), styles.as_ref().clone()); + } _ => {} } for symbol in bindings.get(name.as_str()).into_iter().flatten() { @@ -1032,6 +1045,55 @@ impl<'p, 'a> ModuleScope<'p, 'a> { }) } + /// The styles behind `css(rules)`, the package's own `css` given one + /// rule object every value of which is known, as the module's class names + /// do not tell them + fn css_styles( + &mut self, + modules: &mut Modules<'_>, + call: &oxc_ast::ast::CallExpression<'_>, + ) -> Option<Rc<Vec<ExtractStyleValue>>> { + let Expression::Identifier(callee) = &call.callee else { + return None; + }; + let (source, Imported::Named(export)) = self.imports.get(callee.name.as_str())? else { + return None; + }; + if export != "css" || !source.starts_with(modules.option.package.as_str()) { + return None; + } + let [argument] = call.arguments.as_slice() else { + return None; + }; + let rules = self.evaluate(modules, argument.as_expression()?)?; + let allocator = Allocator::default(); + let builder = AstBuilder::new(&allocator); + let mut rules = match constant_literal(&builder, &rules, true)? { + rules @ Expression::ObjectExpression(_) => rules, + _ => return None, + }; + let ExtractResult { + mut styles, + style_order, + .. + } = extract_style_from_expression( + &builder, + None, + &mut rules, + 0, + &None, + LiteralHandling::ExpandResponsiveThemeToken, + ); + if let Some(order) = style_order { + for prop in &mut styles { + set_prop_order(prop, order); + } + } + let mut composition = Composition::default(); + composition.apply(&builder, styles); + composition.unconditional().map(Rc::new) + } + fn is_style_import(&self, option: &ExtractOption, name: &str) -> bool { self.style_imports.contains(name) || self.style_names.contains(name) @@ -1465,13 +1527,15 @@ impl<'p, 'a> ModuleScope<'p, 'a> { } fold_math(callee.property.name.as_str(), &arguments) } - callee if self.is_style_api(modules, callee) => Some(Constant::Style), + callee if self.is_style_api(modules, callee) => { + Some(Constant::Style(self.css_styles(modules, call))) + } _ => self.evaluate_stylex(modules, call), }, Expression::TaggedTemplateExpression(tagged) if self.is_style_api(modules, &tagged.tag) => { - Some(Constant::Style) + Some(Constant::Style(None)) } Expression::TSAsExpression(inner) => self.evaluate(modules, &inner.expression), Expression::TSSatisfiesExpression(inner) => self.evaluate(modules, &inner.expression), @@ -1837,55 +1901,7 @@ impl<'a> Inline<'_, 'a> { } fn literal(&self, constant: &Constant) -> Option<Expression<'a>> { - let builder = self.ast_builder; - match constant { - Constant::String(value) => Some(Expression::new_string_literal( - SPAN, - Str::from_in(value.as_str(), builder.allocator()), - None, - builder, - )), - Constant::Number(value) => Some(Expression::new_numeric_literal( - SPAN, - *value, - None, - NumberBase::Decimal, - builder, - )), - Constant::Null => Some(Expression::new_null_literal(SPAN, builder)), - Constant::Bool(value) => Some(Expression::new_boolean_literal(SPAN, *value, builder)), - Constant::Record(entries) if self.objects => { - let mut properties = oxc_allocator::Vec::with_capacity_in(entries.len(), builder); - for (key, value) in entries.iter() { - properties.push(ObjectPropertyKind::new_object_property( - SPAN, - oxc_ast::ast::PropertyKind::Init, - oxc_ast::ast::PropertyKey::StringLiteral( - oxc_ast::ast::StringLiteral::boxed( - SPAN, - Str::from_in(key.as_str(), builder.allocator()), - None, - builder, - ), - ), - self.literal(value)?, - false, - false, - false, - builder, - )); - } - Some(Expression::new_object_expression(SPAN, properties, builder)) - } - Constant::Array(values) if self.objects => { - let mut elements = oxc_allocator::Vec::with_capacity_in(values.len(), builder); - for value in values.iter() { - elements.push(self.literal(value)?.into()); - } - Some(Expression::new_array_expression(SPAN, elements, builder)) - } - _ => None, - } + constant_literal(self.ast_builder, constant, self.objects) } fn reading_objects<T>(&mut self, objects: bool, visit: impl FnOnce(&mut Self) -> T) -> T { @@ -1904,6 +1920,60 @@ impl<'a> Inline<'_, 'a> { } } +/// `constant` written as a literal, objects and arrays too when `objects` +fn constant_literal<'a>( + builder: &AstBuilder<'a>, + constant: &Constant, + objects: bool, +) -> Option<Expression<'a>> { + match constant { + Constant::String(value) => Some(Expression::new_string_literal( + SPAN, + Str::from_in(value.as_str(), builder.allocator()), + None, + builder, + )), + Constant::Number(value) => Some(Expression::new_numeric_literal( + SPAN, + *value, + None, + NumberBase::Decimal, + builder, + )), + Constant::Null => Some(Expression::new_null_literal(SPAN, builder)), + Constant::Bool(value) => Some(Expression::new_boolean_literal(SPAN, *value, builder)), + Constant::Record(entries) if objects => { + let mut properties = oxc_allocator::Vec::with_capacity_in(entries.len(), builder); + for (key, value) in entries.iter() { + properties.push(ObjectPropertyKind::new_object_property( + SPAN, + oxc_ast::ast::PropertyKind::Init, + oxc_ast::ast::PropertyKey::StringLiteral(oxc_ast::ast::StringLiteral::boxed( + SPAN, + Str::from_in(key.as_str(), builder.allocator()), + None, + builder, + )), + constant_literal(builder, value, objects)?, + false, + false, + false, + builder, + )); + } + Some(Expression::new_object_expression(SPAN, properties, builder)) + } + Constant::Array(values) if objects => { + let mut elements = oxc_allocator::Vec::with_capacity_in(values.len(), builder); + for value in values.iter() { + elements.push(constant_literal(builder, value, objects)?.into()); + } + Some(Expression::new_array_expression(SPAN, elements, builder)) + } + _ => None, + } +} + impl<'a> VisitMut<'a> for Inline<'_, 'a> { fn visit_expression(&mut self, expression: &mut Expression<'a>) { if self.styles { diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 732ede591..065aba011 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -2,6 +2,7 @@ mod as_visit; mod build_time_values; mod class_arguments; mod component; +mod composition; mod css_utils; mod emotion_namespace; pub mod extract_style; @@ -13,10 +14,12 @@ mod imported_constants; mod module_loader; mod mutations; mod prop_modify_utils; +mod prop_valid; #[cfg(test)] mod responsive_selector_tests; mod source_map; mod style_values; +mod styled_reads; #[cfg(test)] mod styled_source_tests; mod stylex; @@ -510,6 +513,7 @@ fn extract_source( if global { None } else { Some(bucket) }, ); visitor.import_stylex(inlined.stylex_vars, inlined.stylex_themes); + visitor.import_css(inlined.css_styles); visitor.unknown_bindings(&inlined.unknown); visitor.changed_bindings(inlined.changed.clone()); visitor.visit_program(&mut program); @@ -843,6 +847,8 @@ pub fn has_devup_ui(filename: &str, code: &str, package: &str) -> bool { #[cfg(test)] #[allow(clippy::expect_used, clippy::unwrap_used)] mod tests { + mod styled_transient_tests; + mod emotion_namespace_tests { include!("emotion_namespace_tests.rs"); } @@ -14751,6 +14757,449 @@ const Button = styled.button({ bg: 'red' }) )); } + /// `code` extracted with readable class names + fn readable_code(code: &str) -> String { + reset_class_map(); + reset_file_map(); + css::debug::set_debug(true); + let output = extract( + "test.tsx", + code, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: true, + import_main_css: false, + import_aliases: HashMap::new(), + }, + ); + css::debug::set_debug(false); + match output { + Ok(output) => output.code, + Err(error) => error.to_string(), + } + } + + // A later part of `css(...)` replaces an earlier part's declaration of the + // same property, selector, breakpoint and layer, whatever the stylesheet + // order of their classes + #[rstest] + #[case( + "css(yellow, azure)", + r#""color-0-red-_a__c_hover-255 background-0-black--255 color-0-azure--255""# + )] + #[case( + "css(azure, yellow)", + r#""background-0-black--255 color-0-red-_a__c_hover-255 color-0-yellow--255""# + )] + #[case( + "css({ color: 'green' }, yellow)", + r#""color-0-red-_a__c_hover-255 color-0-yellow--255""# + )] + #[case( + "css(yellow, { color: 'green' })", + r#""color-0-red-_a__c_hover-255 color-0-green--255""# + )] + #[case( + "css([yellow, azure])", + r#""color-0-red-_a__c_hover-255 background-0-black--255 color-0-azure--255""# + )] + #[case( + "css(...[yellow, azure])", + r#""color-0-red-_a__c_hover-255 background-0-black--255 color-0-azure--255""# + )] + #[case( + "css(yellow, null, undefined, false, azure)", + r#""color-0-red-_a__c_hover-255 background-0-black--255 color-0-azure--255""# + )] + #[case( + "css(yellow, on && azure)", + r#"`color-0-red-_a__c_hover-255 ${on ? "background-0-black--255" : ""} ${on ? "color-0-azure--255" : "color-0-yellow--255"}`"# + )] + #[case( + "css(on ? yellow : azure)", + r#"`${on ? "color-0-red-_a__c_hover-255" : ""} ${on ? "color-0-yellow--255" : "color-0-azure--255"} ${on ? "" : "background-0-black--255"}`"# + )] + #[case( + "css(yellow, on ? { color: 'pink' } : null)", + r#"`color-0-red-_a__c_hover-255 ${on ? "color-0-pink--255" : "color-0-yellow--255"}`"# + )] + #[case( + "css(yellow, { color: on ? 'pink' : 'teal' })", + r#"`color-0-red-_a__c_hover-255 ${on ? "color-0-pink--255" : "color-0-teal--255"}`"# + )] + #[case( + "css(yellow, ext, azure)", + r"`color-0-red-_a__c_hover-255 background-0-black--255 color-0-azure--255 ${ext}`" + )] + #[case( + "css(yellow, on ? 'plain' : azure)", + r#"`${on ? "plain" : ""} ${`color-0-red-_a__c_hover-255 ${on ? "" : "background-0-black--255"} ${on ? "color-0-yellow--255" : "color-0-azure--255"}`}`"# + )] + #[case( + "css(azure, wide)", + r#""background-0-black--255 color-0-a1--255 color-1-b1--255""# + )] + #[case( + "css(wide, { color: [null, 'x2'] })", + r#""color-0-a1--255 color-1-x2--255""# + )] + #[case( + "css(yellow, ordered)", + r#""color-0-red-_a__c_hover-255 color-0-navy--3""# + )] + #[case("css(heading, body)", r#""typo-body""#)] + #[case( + "css(chained, { color: 'gold' })", + r#""color-0-red-_a__c_hover-255 background-0-black--255 color-0-gold--255""# + )] + #[case( + "css(yellow, { color: { a: 'red', b: 'blue' }[size] })", + r"`color-0-red-_a__c_hover-255 color-0-yellow--255 ${{" + )] + #[case( + "css(yellow, on ? { color: { a: 'red' }[size] } : null)", + r#"`color-0-red-_a__c_hover-255 color-0-yellow--255 ${on ? { "a": "color-0-red--255" }[size] || "" : ""}`"# + )] + #[case( + "css(yellow, on || azure)", + r#"`${on ? typeof on === "string" ? on : "" : ""} ${`color-0-red-_a__c_hover-255 ${on ? "" : "background-0-black--255"} ${on ? "color-0-yellow--255" : "color-0-azure--255"}`}`"# + )] + #[case( + "css(yellow, ext ?? azure)", + r#"`${ext != null ? typeof ext === "string" ? ext : "" : ""} ${`color-0-red-_a__c_hover-255 ${ext != null ? "" : "background-0-black--255"} ${ext != null ? "color-0-yellow--255" : "color-0-azure--255"}`}`"# + )] + #[case( + "css(yellow, null ?? azure)", + r#""color-0-red-_a__c_hover-255 background-0-black--255 color-0-azure--255""# + )] + #[case( + "css(yellow, false ?? azure)", + r#""color-0-red-_a__c_hover-255 color-0-yellow--255""# + )] + #[case( + "css(azure || yellow)", + r#""background-0-black--255 color-0-azure--255""# + )] + #[case( + "css(yellow, on || 'plain')", + r#"`color-0-red-_a__c_hover-255 color-0-yellow--255 ${on ? typeof on === "string" ? on : "" : "plain"}`"# + )] + #[case( + "css(yellow, { color: on ? 'a' : 'b', m: [1, 2], styleOrder: 2 })", + r#"`color-0-red-_a__c_hover-255 margin-0-4px--2 margin-1-8px--2 ${on ? "color-0-a--2" : "color-0-b--2"}`"# + )] + #[case( + "css(yellow, { color: { a: 'x' }[size], styleOrder: 2 })", + r#"`color-0-red-_a__c_hover-255 color-0-yellow--255 ${{ "a": "color-0-x--2" }[size] || ""}`"# + )] + #[case( + "css(yellow, on ? { color: other ? 'a' : 'b' } : null)", + r#"`color-0-red-_a__c_hover-255 ${on ? other ? "color-0-a--255" : "color-0-b--255" : "color-0-yellow--255"}`"# + )] + #[case( + "css(yellow, fade)", + r"`color-0-red-_a__c_hover-255 color-0-yellow--255 ${fade}`" + )] + #[case( + "css(yellow, { color: ['x', 'y'][idx], styleOrder: 2 })", + "`color-0-red-_a__c_hover-255 color-0-yellow--255 ${{\n\t\"0\": \"color-0-x--2\",\n\t\"1\": \"color-0-y--2\"\n}[idx] || \"\"}`" + )] + #[case( + "css(yellow, { color: 'pink' } || azure)", + r#""color-0-red-_a__c_hover-255 color-0-pink--255""# + )] + #[case( + "css(yellow, undefined ?? azure)", + r#""color-0-red-_a__c_hover-255 background-0-black--255 color-0-azure--255""# + )] + #[case( + "css(yellow, false ?? azure)", + r#""color-0-red-_a__c_hover-255 color-0-yellow--255""# + )] + #[serial] + fn test_css_composes_known_classes(#[case] call: &str, #[case] expected: &str) { + let code = readable_code(&format!( + "import {{css, keyframes}} from '@devup-ui/core' +const yellow = css({{ color: 'yellow', _hover: {{ color: 'red' }} }}) +const azure = css({{ color: 'azure', bg: 'black' }}) +const wide = css({{ color: ['a1', 'b1'] }}) +const ordered = css({{ color: 'navy', styleOrder: 3 }}) +const heading = css({{ typography: 'heading' }}) +const body = css({{ typography: 'body' }}) +const chained = css(yellow, azure) +const fade = keyframes({{ from: {{ opacity: 0 }} }}) +export const result = {call} +" + )); + assert!( + code.contains(&format!("export const result = {expected}")), + "{code}" + ); + } + + #[test] + #[serial] + fn test_styled_extension_composes_base() { + let code = readable_code( + "import {styled} from '@devup-ui/core' +const Base = styled.button`color: red; background: white; &:hover { color: red; }` +const Ext = styled(Base)`color: blue; &:hover { color: blue; }` +const Obj = styled.div({ color: 'red', p: 2 }) +const ObjExt = styled(Obj)({ color: 'blue' }) +const Twice = styled(ObjExt, { m: 1 }) +const Runtime = styled(make())({ color: 'red' }) +const FromRuntime = styled(Runtime)({ color: 'blue' }) +let Changing = styled.div({ color: 'red' }) +const FromChanging = styled(Changing)({ color: 'blue' }) +const Ordered = styled.div({ color: 'red', styleOrder: 3 })", + ); + for expected in [ + "const Ext = ({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"color-0-blue-_a__c_hover-255 color-0-blue--255 background-0-white--255\", className]", + "const ObjExt = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"color-0-blue--255 padding-0-8px--255\", className]", + "const Twice = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"margin-0-4px--255 color-0-blue--255 padding-0-8px--255\", className]", + "const FromRuntime = ({ style, className, as: DevupAs = Runtime, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))}", + "const FromChanging = ({ style, className, as: DevupAs = Changing, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))}", + "const Ordered = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"color-0-red--3\", className]", + ] { + assert!(code.contains(expected), "{expected}\n{code}"); + } + } + + // `withComponent` renders the same styles as another tag or component, and + // `as` changes what a styled component renders while `forwardedAs` passes on + #[test] + #[serial] + fn test_styled_with_component_and_as() { + let code = readable_code( + "import {styled} from '@devup-ui/core' +const Section = styled.section.attrs({ role: 'region' })({ color: 'red' }) +export const Aside = Section.withComponent('aside') +export const Linked = Section.withComponent(Link) +export const Nested = Section.withComponent(motion.div) +export const Again = Aside.withComponent('nav') +export const Kept = Section.withComponent(make()) +export const Other = other.withComponent('aside') +export const a = <Section as=\"a\" forwardedAs=\"b\" />", + ); + for expected in [ + "export const Aside = (__devupProps) => (({ style, className, as: DevupAs = \"aside\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"color-0-red--255\", className]", + "as: DevupAs = Link,", + "as: DevupAs = motion.div,", + "export const Again = (__devupProps) => (({ style, className, as: DevupAs = \"nav\",", + "export const Kept = Section.withComponent(make());", + "export const Other = other.withComponent(\"aside\");", + "...{ role: \"region\" }", + "<Section as=\"a\" forwardedAs=\"b\" />", + ] { + assert!(code.contains(expected), "{expected}\n{code}"); + } + } + + // A styled component keeps the props its styles read, `$` props and `theme` + // away from the tag, follows `shouldForwardProp` as the build evaluates it, + // and an element using it drops what it neither reads nor passes on + #[test] + #[serial] + fn test_styled_prop_forwarding() { + let code = readable_code( + "import {styled} from '@devup-ui/core' +import isPropValid from '@emotion/is-prop-valid' +const A = styled('h1', { shouldForwardProp: (prop) => prop !== 'tone' })({ color: 'red' }) +const B = styled.div`color: ${(p) => p.$c}; background: ${(p) => p.tone};` +const C = styled.div.withConfig({ shouldForwardProp: (prop, valid) => valid(prop) && !['x', 'y'].includes(prop) })({ color: 'red' }) +const D = styled('p', { shouldForwardProp: (prop) => isPropValid(prop) || prop.startsWith('data-') })({ color: 'red' }) +const E = styled('p', { shouldForwardProp: () => true })({ color: 'red' }) +const F = styled(Link)`color: ${(p) => p.$c};` +const G = styled('p', { shouldForwardProp: function (prop) { return prop === 'id' } })({ color: 'red' }) +const H = styled.div`color: ${function (p) { return p.c }};` +const I = styled.div`color: ${({ tone, ...more }) => tone};` +const J = styled.div`color: ${(p) => p[key]}; margin: ${([a]) => a}; padding: ${() => 1};` +const K = styled(A, { shouldForwardProp: (prop) => 'id' === prop || prop === 'title' })({ color: 'blue' }) +const L = styled.div.withConfig(config)`color: ${(p) => { return p.level }}; margin: ${({ [k]: v }) => v};` +const M = styled('p', { shouldForwardProp: (prop) => { return prop === 'id' } })({ color: 'red' }) +export const a = <A tone=\"loud\" invalidThing=\"x\" data-ok=\"yes\" /> +export const b = <B tone=\"x\" $c=\"red\" theme={{}} aria-label=\"ok\" junk=\"j\" {...rest} /> +export const j = <J junk=\"j\" /> +export const f = <F junk=\"j\" xlink:href=\"#a\" /> +export const k = <K title=\"t\" tone=\"x\" />", + ); + for expected in [ + "{...Object.fromEntries(Object.entries((({ \"tone\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))}", + "{...Object.fromEntries(Object.entries((({ \"$c\": __devupOmit0, \"tone\": __devupOmit1, \"theme\": __devupOmit2, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))}", + "{...Object.fromEntries(Object.entries((({ \"x\": __devupOmit0, \"y\": __devupOmit1, \"theme\": __devupOmit2, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))}", + "{...Object.fromEntries(Object.entries((({ \"$c\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))}", + "export const a = <A invalidThing=\"x\" data-ok=\"yes\" />", + "export const b = <B tone=\"x\" $c=\"red\" aria-label=\"ok\" {...rest} />", + "export const j = <J junk=\"j\" />", + "export const f = <F junk=\"j\" xlink:href=\"#a\" />", + "export const k = <K title=\"t\" />", + "{...Object.fromEntries(Object.entries((({ \"level\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))}", + ] { + assert!(code.contains(expected), "{expected}\n{code}"); + } + let error = readable_code( + "import {styled} from '@devup-ui/core' +const A = styled('h1', { shouldForwardProp: (prop) => allowed.has(prop) })({ color: 'red' }) +const B = styled('h1', { shouldForwardProp: (prop) => prop === other })({ color: 'red' }) +const C = styled('h1', { shouldForwardProp: (prop) => prop > 'a' })({ color: 'red' }) +const D = styled('h1', { shouldForwardProp: (prop) => prop ?? 'a' })({ color: 'red' }) +const E = styled('h1', { shouldForwardProp: (prop) => { const x = 1; return x } })({ color: 'red' }) +const F = styled('h1', { shouldForwardProp: check })({ color: 'red' }) +const G = styled('h1', { shouldForwardProp: ({ a }) => a })({ color: 'red' }) +const H = styled('h1', { shouldForwardProp: (prop) => [x].includes(prop) })({ color: 'red' }) +const I = styled('h1', { shouldForwardProp: (prop) => list.includes(prop) })({ color: 'red' }) +const J = styled('h1', { shouldForwardProp: (prop) => prop.endsWith('a') })({ color: 'red' }) +const K = styled('h1', { [k]: 1, ...o, shouldForwardProp: (prop) => valid(prop, 1) })({ color: 'red' }) +const L = styled('h1', { shouldForwardProp: (prop) => prop === 'a' ?? prop === 'b' })({ color: 'red' }) +const M = styled('h1', { shouldForwardProp: (prop) => x === y })({ color: 'red' }) +const N = styled('h1', { shouldForwardProp: (prop) => { prop === 'a' } })({ color: 'red' })", + ); + assert!( + error.contains("`shouldForwardProp` must be a function"), + "{error}" + ); + assert!(error.contains("allowed.has(prop)"), "{error}"); + } + + #[test] + #[serial] + fn test_styled_attrs_merge_in_order() { + let code = readable_code( + "import {styled} from '@devup-ui/core' +const Base = styled.input.attrs({ type: 'text', title: 'base' })({ color: 'red' }) +const Ext = styled(Base).attrs({ type: 'password' })({ color: 'blue' }) +const X = styled.div.attrs({ className: 'from-attrs', style: { color: 'green' } })({ color: 'red' }) +const F = styled.div.attrs((p) => ({ id: p.id }))({ color: 'red' }) +const V = styled.div.attrs(extra)({ color: 'red' })", + ); + for expected in [ + "...{\n\t\t...__devupProps,\n\t\t...{\n\t\t\ttype: \"text\",\n\t\t\ttitle: \"base\"\n\t\t}\n\t},\n\t...{ type: \"password\" }", + "className: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(\" \") || undefined", + "className: [__devupContext.className, __devupProps.className].filter(Boolean).join(\" \") || undefined", + "...__devupContext.style,\n\t\t...__devupAttrs.style", + "((p) => ({ id: p.id }))(__devupContext)", + "typeof extra === \"function\" ? extra(__devupContext) : extra", + ] { + assert!(code.contains(expected), "{expected}\n{code}"); + } + } + + #[rstest] + #[case( + "<Box className=\"direct\" style={{ opacity: 1 }} {...{ className: 'spread', style: { opacity: 2 } }} />", + "className=\"spread\" style={{ opacity: 2 }}" + )] + #[case( + "<Box {...{ className: 'spread', style: { opacity: 2 } }} className=\"direct\" style={{ opacity: 1 }} />", + "className=\"direct\" style={{ opacity: 1 }}" + )] + #[case( + "<Box className=\"direct\" style={{ opacity: 1 }} {...rest} />", + "className={(\"className\" in Object(rest) ? rest.className : \"direct\") || \"\"} style={\"style\" in Object(rest) ? rest.style : { opacity: 1 }}" + )] + #[case( + "<Box className=\"direct\" {...{ title: 'x' }} {...{ ...rest, className: 'last' }} />", + "className=\"last\"" + )] + #[case( + "<Box className=\"direct\" {...{ className: 'first', ...rest }} />", + "className={{\n\tclassName: \"first\",\n\t...rest\n}.className || \"\"}" + )] + #[case( + "<Box className=<i /> color=\"red\" />", + "<div className=\"color-0-red--255\" />" + )] + #[case( + "<Box {...rest} {...more} />", + "className={(\"className\" in Object(more) ? more.className : rest?.className) || \"\"}" + )] + #[serial] + fn test_jsx_props_written_later_win(#[case] element: &str, #[case] expected: &str) { + let code = readable_code(&format!( + "import {{Box}} from '@devup-ui/core' +export const a = {element}" + )); + assert!(code.contains(expected), "{expected}\n{code}"); + } + + #[test] + #[serial] + fn test_css_composing_reports_runtime_values() { + let code = readable_code( + "import {css} from '@devup-ui/core' +const base = css({ color: 'red' }) +export const a = css(base, { color: tone }) +export const b = css(base, { [key]: 'x', styleOrder: 2 }) +export const c = css(base, getStyles()) +export const d = css(base, { positioning: side, styleOrder: 2 })", + ); + assert!(!code.contains("test.tsx:6:"), "{code}"); + assert!(code.contains("`css()` cannot use `tone`"), "{code}"); + assert!( + code.contains("Cannot compose `\"color-0-red--255\", getStyles()`"), + "{code}" + ); + } + + // The styles of a `css()` class another module exports compose as well + #[test] + #[serial] + fn test_css_composes_imported_classes() { + reset_class_map(); + reset_file_map(); + let modules: &[(&str, &str)] = &[( + "/src/styles.ts", + "import { css, keyframes } from '@devup-ui/react'; +import * as Devup from '@devup-ui/react'; +const brand = 'teal'; +export const base = css({ color: brand, _hover: { color: 'red' } }); +export const danger = css({ color: 'crimson' }); +export const twice = css({ color: 'a' }, { m: 1 }); +export const text = css`color: blue;`; +export const runtime = css({ color: globalThis.tone }); +export const listed = css([{ color: 'b' }]); +export const spaced = Devup.css({ color: 'c' }); +export const fade = keyframes({ from: { opacity: 0 } }); +export const called = Devup({ color: 'e' }); +export const ordered = css({ color: 'f', m: 2, styleOrder: 2 });", + )]; + let resolver = memory_resolver(modules); + css::debug::set_debug(true); + let output = extract_with_modules( + "/src/App.tsx", + "import { css } from '@devup-ui/react'; +import { base, danger, twice, text, runtime, listed, spaced, fade, called, ordered } from './styles'; +export const a = css(base, danger); +export const b = css(danger, base, { m: 1 }); +export const c = css(twice, text, runtime, listed, spaced, fade); +export const d = css(ordered, danger, called);", + ExtractOption { + import_aliases: HashMap::from([( + "@emotion/css".to_string(), + ImportAlias::NamedToNamed, + )]), + ..ExtractOption::default() + }, + false, + &resolver, + ) + .unwrap(); + css::debug::set_debug(false); + for expected in [ + r#"export const a = "color-0-red-_a__c_hover-255-a color-0-crimson--255-a";"#, + r#"export const b = "color-0-red-_a__c_hover-255-a color-0-teal--255-a margin-0-4px--255-a";"#, + "export const c = `${twice} ${text} ${runtime} ${listed} ${spaced} ${fade}`;", + "export const d = `margin-0-8px--2-a color-0-crimson--255-a ${called}`;", + ] { + assert!( + output.code.contains(expected), + "{expected} in {}", + output.code + ); + } + } + // Each Tailwind class becomes the classes of its styles; every other class, // and every class that runs into an interpolation, stays as written #[rstest] diff --git a/libs/extractor/src/prop_modify_utils.rs b/libs/extractor/src/prop_modify_utils.rs index 4b2a8c588..16f0d7daf 100644 --- a/libs/extractor/src/prop_modify_utils.rs +++ b/libs/extractor/src/prop_modify_utils.rs @@ -62,20 +62,13 @@ fn resolve_class_name_expression<'a>( class_name_prop: &Option<Expression<'a>>, styles: &mut [ExtractStyleProp<'a>], style_order: Option<u8>, - spread_props: &[Expression<'a>], filename: Option<&str>, conditional_branch: Option<(Expression<'a>, &mut [ExtractStyleProp<'a>], Option<u8>)>, ) -> (Option<Expression<'a>>, Vec<ExtractStyleValue>) { if let Some((condition, alt_styles, alt_style_order)) = conditional_branch { // Conditional styleOrder: generate className for both branches - let (con_expr, con_tailwind) = get_class_name_expression( - ast_builder, - class_name_prop, - styles, - style_order, - spread_props, - filename, - ); + let (con_expr, con_tailwind) = + get_class_name_expression(ast_builder, class_name_prop, styles, style_order, filename); let alt_class_name_prop = class_name_prop .as_ref() .map(|c| c.clone_in(ast_builder.allocator())); @@ -84,7 +77,6 @@ fn resolve_class_name_expression<'a>( &alt_class_name_prop, alt_styles, alt_style_order, - spread_props, filename, ); @@ -95,14 +87,7 @@ fn resolve_class_name_expression<'a>( all_tailwind.extend(alt_tailwind); (combined_expr, all_tailwind) } else { - get_class_name_expression( - ast_builder, - class_name_prop, - styles, - style_order, - spread_props, - filename, - ) + get_class_name_expression(ast_builder, class_name_prop, styles, style_order, filename) } } @@ -121,38 +106,44 @@ pub fn modify_prop_object<'a>( filename: Option<&str>, conditional_branch: Option<(Expression<'a>, &mut [ExtractStyleProp<'a>], Option<u8>)>, ) -> Vec<ExtractStyleValue> { - let mut class_name_prop = None; - let mut style_prop = None; - let mut spread_props = vec![]; - for idx in (0..props.len()).rev() { - let prop = props.remove(idx); + let mut class_names = Vec::new(); + let mut style_values = Vec::new(); + let written = std::mem::replace(props, oxc_allocator::Vec::new_in(ast_builder)); + for prop in written { match prop { - ObjectPropertyKind::ObjectProperty(attr) => { - if let Some(name) = get_str_by_property_key(&attr.key) { - if name == "className" { - class_name_prop = Some(attr.value.clone_in(ast_builder.allocator())); - } else if name == "style" { - style_prop = Some(attr.value.clone_in(ast_builder.allocator())); - } else { - props.insert(idx, ObjectPropertyKind::ObjectProperty(attr)); - } + ObjectPropertyKind::ObjectProperty(attr) + if matches!( + get_str_by_property_key(&attr.key).as_deref(), + Some("className" | "style") + ) => + { + let value = Written::Prop(attr.value.clone_in(ast_builder.allocator())); + if get_str_by_property_key(&attr.key).as_deref() == Some("className") { + class_names.push(value); } else { - props.insert(idx, ObjectPropertyKind::ObjectProperty(attr)); + style_values.push(value); } } ObjectPropertyKind::SpreadProperty(spread) => { - spread_props.push(spread.argument.clone_in(ast_builder.allocator())); - props.insert(idx, ObjectPropertyKind::SpreadProperty(spread)); + class_names.push(Written::Spread( + spread.argument.clone_in(ast_builder.allocator()), + )); + style_values.push(Written::Spread( + spread.argument.clone_in(ast_builder.allocator()), + )); + props.push(ObjectPropertyKind::SpreadProperty(spread)); } + prop @ ObjectPropertyKind::ObjectProperty(_) => props.push(prop), } } + let class_name_prop = last_written(ast_builder, &class_names, "className"); + let style_prop = last_written(ast_builder, &style_values, "style"); let (class_name_expr, tailwind_styles) = resolve_class_name_expression( ast_builder, &class_name_prop, styles, style_order, - &spread_props, filename, conditional_branch, ); @@ -169,14 +160,8 @@ pub fn modify_prop_object<'a>( ast_builder, )); } - if let Some(ex) = get_style_expression( - ast_builder, - &style_prop, - styles, - &style_vars, - &spread_props, - filename, - ) { + if let Some(ex) = get_style_expression(ast_builder, &style_prop, styles, &style_vars, filename) + { props.push(ObjectPropertyKind::new_object_property( SPAN, PropertyKind::Init, @@ -212,54 +197,54 @@ pub fn modify_props<'a>( filename: Option<&str>, conditional_branch: Option<(Expression<'a>, &mut [ExtractStyleProp<'a>], Option<u8>)>, ) -> Vec<ExtractStyleValue> { - let mut class_name_prop = None; - let mut style_prop = None; - let mut spread_props = vec![]; - for idx in (0..props.len()).rev() { - let prop = props.remove(idx); + let mut class_names = Vec::new(); + let mut style_values = Vec::new(); + let written = std::mem::replace(props, oxc_allocator::Vec::new_in(ast_builder)); + for prop in written { match prop { - JSXAttributeItem::Attribute(attr) => { - if let Identifier(ident) = &attr.name - && let Some(value) = &attr.value - && (ident.name == "className" || ident.name == "style") - { - let mut res = None; - - if let JSXAttributeValue::ExpressionContainer(container) = &value { - res = container - .expression - .as_expression() - .map(|expression| expression.clone_in(ast_builder.allocator())); - } else if let JSXAttributeValue::StringLiteral(literal) = &value { - res = Some(Expression::new_string_literal( - SPAN, - literal.value, - None, - ast_builder, - )); - } - let name = ident.name.as_str(); - if name == "className" { - class_name_prop = res; - } else if name == "style" { - style_prop = res; - } + JSXAttributeItem::Attribute(attr) + if matches!(&attr.name, Identifier(ident) + if ident.name == "className" || ident.name == "style") + && attr.value.is_some() => + { + let value = match &attr.value { + Some(JSXAttributeValue::ExpressionContainer(container)) => container + .expression + .as_expression() + .map(|expression| expression.clone_in(ast_builder.allocator())), + Some(JSXAttributeValue::StringLiteral(literal)) => Some( + Expression::new_string_literal(SPAN, literal.value, None, ast_builder), + ), + _ => None, + }; + let Some(value) = value else { + continue; + }; + if matches!(&attr.name, Identifier(ident) if ident.name == "className") { + class_names.push(Written::Prop(value)); } else { - props.insert(idx, JSXAttributeItem::Attribute(attr)); + style_values.push(Written::Prop(value)); } } JSXAttributeItem::SpreadAttribute(spread) => { - spread_props.push(spread.argument.clone_in(ast_builder.allocator())); - props.insert(idx, JSXAttributeItem::SpreadAttribute(spread)); + class_names.push(Written::Spread( + spread.argument.clone_in(ast_builder.allocator()), + )); + style_values.push(Written::Spread( + spread.argument.clone_in(ast_builder.allocator()), + )); + props.push(JSXAttributeItem::SpreadAttribute(spread)); } + prop @ JSXAttributeItem::Attribute(_) => props.push(prop), } } + let class_name_prop = last_written(ast_builder, &class_names, "className"); + let style_prop = last_written(ast_builder, &style_values, "style"); let (class_name_expr, tailwind_styles) = resolve_class_name_expression( ast_builder, &class_name_prop, styles, style_order, - &spread_props, filename, conditional_branch, ); @@ -275,14 +260,8 @@ pub fn modify_props<'a>( ast_builder, )); } - if let Some(ex) = get_style_expression( - ast_builder, - &style_prop, - styles, - &style_vars, - &spread_props, - filename, - ) { + if let Some(ex) = get_style_expression(ast_builder, &style_prop, styles, &style_vars, filename) + { props.push(JSXAttributeItem::new_attribute( SPAN, JSXAttributeName::new_identifier(SPAN, "style", ast_builder), @@ -310,7 +289,6 @@ pub fn get_class_name_expression<'a>( class_name_prop: &Option<Expression<'a>>, styles: &mut [ExtractStyleProp<'a>], style_order: Option<u8>, - spread_props: &[Expression<'a>], filename: Option<&str>, ) -> (Option<Expression<'a>>, Vec<ExtractStyleValue>) { let mut tailwind = TailwindClassName { @@ -330,32 +308,13 @@ pub fn get_class_name_expression<'a>( .map(|class_name| convert_class_name(ast_builder, class_name)); // Merge class names: [className] + [devup-ui component styles] - let mut class_expressions = Vec::with_capacity(2 + spread_props.len()); + let mut class_expressions = Vec::with_capacity(2); if let Some(class_name) = class_name_to_use { class_expressions.push(class_name); } if let Some(class_name) = gen_class_names(ast_builder, styles, style_order, filename) { class_expressions.push(class_name); } - if class_name_prop.is_none() { - class_expressions.extend( - spread_props - .iter() - .filter(|ex| may_hold(ex, "className")) - .map(|ex| { - convert_class_name( - ast_builder, - &Expression::StaticMemberExpression(StaticMemberExpression::boxed( - SPAN, - ex.clone_in(ast_builder.allocator()), - IdentifierName::new(SPAN, "className", ast_builder), - true, - ast_builder, - )), - ) - }), - ); - } let expression = merge_string_expressions(ast_builder, &class_expressions); (expression, tailwind.into_styles()) @@ -762,6 +721,107 @@ fn template_raw(cooked: &str) -> String { raw } +/// `className` or `style` written as a prop, or spread with other props +pub(crate) enum Written<'a> { + Prop(Expression<'a>), + Spread(Expression<'a>), +} + +/// The value `key` ends up with as the props are written in order: a later +/// prop or spread holding `key` replaces an earlier one. Whether a spread holds +/// it only the runtime tells, unless it is an object literal. +fn last_written<'a>( + ast_builder: &AstBuilder<'a>, + written: &[Written<'a>], + key: &'static str, +) -> Option<Expression<'a>> { + let mut value: Option<Expression<'a>> = None; + for written in written { + let spread = match written { + Written::Prop(prop) => { + value = Some(prop.clone_in(ast_builder.allocator())); + continue; + } + Written::Spread(spread) if may_hold(spread, key) => spread, + Written::Spread(_) => continue, + }; + let member = |optional| { + Expression::StaticMemberExpression(StaticMemberExpression::boxed( + SPAN, + spread.clone_in(ast_builder.allocator()), + IdentifierName::new(SPAN, key, ast_builder), + optional, + ast_builder, + )) + }; + if let Some(written) = written_value(spread, key) { + value = Some(written.clone_in(ast_builder.allocator())); + continue; + } + value = Some(match value { + Some(earlier) if !names(spread, key) => { + let holds = Expression::new_binary_expression( + SPAN, + Expression::new_string_literal(SPAN, key, None, ast_builder), + oxc_syntax::operator::BinaryOperator::In, + crate::utils::wrap_direct_call( + ast_builder, + &Expression::new_identifier(SPAN, "Object", ast_builder), + &[spread.clone_in(ast_builder.allocator())], + ), + ast_builder, + ); + Expression::new_conditional_expression( + SPAN, + holds, + member(false), + earlier, + ast_builder, + ) + } + _ => member(!names(spread, key)), + }); + } + value +} + +/// Whether the object literal `spread` names `key`, so it always holds it +fn names(spread: &Expression<'_>, key: &str) -> bool { + let Expression::ObjectExpression(object) = crate::utils::unwrap_syntax_only(spread) else { + return false; + }; + object.properties.iter().any(|property| { + matches!(property, ObjectPropertyKind::ObjectProperty(property) + if !property.computed + && get_str_by_property_key(&property.key).as_deref() == Some(key)) + }) +} + +/// What the object literal `spread` sets `key` to, when it writes it by name +/// after anything that may also hold it +fn written_value<'b, 'a>(spread: &'b Expression<'a>, key: &str) -> Option<&'b Expression<'a>> { + let Expression::ObjectExpression(object) = crate::utils::unwrap_syntax_only(spread) else { + return None; + }; + object + .properties + .iter() + .rev() + .find_map(|property| match property { + ObjectPropertyKind::ObjectProperty(property) + if !property.computed + && get_str_by_property_key(&property.key).as_deref() != Some(key) => + { + None + } + ObjectPropertyKind::ObjectProperty(property) if !property.computed => { + Some(Some(&property.value)) + } + _ => Some(None), + }) + .flatten() +} + /// Whether the object `spread` gives may hold `key`: an object literal holds /// only the keys it writes fn may_hold(spread: &Expression<'_>, key: &str) -> bool { @@ -781,10 +841,9 @@ pub fn get_style_expression<'a>( style_prop: &Option<Expression<'a>>, styles: &[ExtractStyleProp<'a>], style_vars: &Option<Expression<'a>>, - spread_props: &[Expression<'a>], filename: Option<&str>, ) -> Option<Expression<'a>> { - let mut style_expressions = Vec::with_capacity(3 + spread_props.len()); + let mut style_expressions = Vec::with_capacity(3); if let Some(style) = gen_styles(ast_builder, styles, filename) { style_expressions.push(style); } @@ -797,20 +856,6 @@ pub fn get_style_expression<'a>( if let Some(style_prop) = style_prop.clone_in(ast_builder.allocator()) { style_expressions.push(style_prop); } - if style_prop.is_none() { - style_expressions.extend(spread_props.iter().filter(|ex| may_hold(ex, "style")).map( - |ex| { - Expression::StaticMemberExpression(StaticMemberExpression::boxed( - SPAN, - ex.clone_in(ast_builder.allocator()), - IdentifierName::new(SPAN, "style", ast_builder), - true, - ast_builder, - )) - }, - )); - } - merge_object_expressions(ast_builder, &style_expressions) } diff --git a/libs/extractor/src/prop_valid.rs b/libs/extractor/src/prop_valid.rs new file mode 100644 index 000000000..82031372f --- /dev/null +++ b/libs/extractor/src/prop_valid.rs @@ -0,0 +1,437 @@ +//! The props React passes to an element as attributes, as +//! `@emotion/is-prop-valid` 1.4 lists them: a styled component keeps passing +//! these to the tag it renders even when its styles read them + +use phf::phf_set; + +static VALID_PROPS: phf::Set<&str> = phf_set! { + "children", + "dangerouslySetInnerHTML", + "key", + "ref", + "autoFocus", + "defaultValue", + "defaultChecked", + "innerHTML", + "suppressContentEditableWarning", + "suppressHydrationWarning", + "valueLink", + "abbr", + "accept", + "acceptCharset", + "accessKey", + "action", + "allow", + "allowUserMedia", + "allowPaymentRequest", + "allowFullScreen", + "allowTransparency", + "alt", + "async", + "autoComplete", + "autoPlay", + "capture", + "cellPadding", + "cellSpacing", + "challenge", + "charSet", + "checked", + "cite", + "classID", + "className", + "cols", + "colSpan", + "content", + "contentEditable", + "contextMenu", + "controls", + "controlsList", + "coords", + "crossOrigin", + "data", + "dateTime", + "decoding", + "default", + "defer", + "dir", + "disabled", + "disablePictureInPicture", + "disableRemotePlayback", + "download", + "draggable", + "encType", + "enterKeyHint", + "fetchpriority", + "fetchPriority", + "form", + "formAction", + "formEncType", + "formMethod", + "formNoValidate", + "formTarget", + "frameBorder", + "headers", + "height", + "hidden", + "high", + "href", + "hrefLang", + "htmlFor", + "httpEquiv", + "id", + "inputMode", + "integrity", + "is", + "keyParams", + "keyType", + "kind", + "label", + "lang", + "list", + "loading", + "loop", + "low", + "marginHeight", + "marginWidth", + "max", + "maxLength", + "media", + "mediaGroup", + "method", + "min", + "minLength", + "multiple", + "muted", + "name", + "nonce", + "noValidate", + "open", + "optimum", + "pattern", + "placeholder", + "playsInline", + "popover", + "popoverTarget", + "popoverTargetAction", + "poster", + "preload", + "profile", + "radioGroup", + "readOnly", + "referrerPolicy", + "rel", + "required", + "reversed", + "role", + "rows", + "rowSpan", + "sandbox", + "scope", + "scoped", + "scrolling", + "seamless", + "selected", + "shape", + "size", + "sizes", + "slot", + "span", + "spellCheck", + "src", + "srcDoc", + "srcLang", + "srcSet", + "start", + "step", + "style", + "summary", + "tabIndex", + "target", + "title", + "translate", + "type", + "useMap", + "value", + "width", + "wmode", + "wrap", + "about", + "datatype", + "inlist", + "prefix", + "property", + "resource", + "typeof", + "vocab", + "autoCapitalize", + "autoCorrect", + "autoSave", + "color", + "incremental", + "fallback", + "inert", + "itemProp", + "itemScope", + "itemType", + "itemID", + "itemRef", + "on", + "option", + "results", + "security", + "unselectable", + "accentHeight", + "accumulate", + "additive", + "alignmentBaseline", + "allowReorder", + "alphabetic", + "amplitude", + "arabicForm", + "ascent", + "attributeName", + "attributeType", + "autoReverse", + "azimuth", + "baseFrequency", + "baselineShift", + "baseProfile", + "bbox", + "begin", + "bias", + "by", + "calcMode", + "capHeight", + "clip", + "clipPathUnits", + "clipPath", + "clipRule", + "colorInterpolation", + "colorInterpolationFilters", + "colorProfile", + "colorRendering", + "contentScriptType", + "contentStyleType", + "cursor", + "cx", + "cy", + "d", + "decelerate", + "descent", + "diffuseConstant", + "direction", + "display", + "divisor", + "dominantBaseline", + "dur", + "dx", + "dy", + "edgeMode", + "elevation", + "enableBackground", + "end", + "exponent", + "externalResourcesRequired", + "fill", + "fillOpacity", + "fillRule", + "filter", + "filterRes", + "filterUnits", + "floodColor", + "floodOpacity", + "focusable", + "fontFamily", + "fontSize", + "fontSizeAdjust", + "fontStretch", + "fontStyle", + "fontVariant", + "fontWeight", + "format", + "from", + "fr", + "fx", + "fy", + "g1", + "g2", + "glyphName", + "glyphOrientationHorizontal", + "glyphOrientationVertical", + "glyphRef", + "gradientTransform", + "gradientUnits", + "hanging", + "horizAdvX", + "horizOriginX", + "ideographic", + "imageRendering", + "in", + "in2", + "intercept", + "k", + "k1", + "k2", + "k3", + "k4", + "kernelMatrix", + "kernelUnitLength", + "kerning", + "keyPoints", + "keySplines", + "keyTimes", + "lengthAdjust", + "letterSpacing", + "lightingColor", + "limitingConeAngle", + "local", + "markerEnd", + "markerMid", + "markerStart", + "markerHeight", + "markerUnits", + "markerWidth", + "mask", + "maskContentUnits", + "maskUnits", + "mathematical", + "mode", + "numOctaves", + "offset", + "opacity", + "operator", + "order", + "orient", + "orientation", + "origin", + "overflow", + "overlinePosition", + "overlineThickness", + "panose1", + "paintOrder", + "pathLength", + "patternContentUnits", + "patternTransform", + "patternUnits", + "pointerEvents", + "points", + "pointsAtX", + "pointsAtY", + "pointsAtZ", + "preserveAlpha", + "preserveAspectRatio", + "primitiveUnits", + "r", + "radius", + "refX", + "refY", + "renderingIntent", + "repeatCount", + "repeatDur", + "requiredExtensions", + "requiredFeatures", + "restart", + "result", + "rotate", + "rx", + "ry", + "scale", + "seed", + "shapeRendering", + "slope", + "spacing", + "specularConstant", + "specularExponent", + "speed", + "spreadMethod", + "startOffset", + "stdDeviation", + "stemh", + "stemv", + "stitchTiles", + "stopColor", + "stopOpacity", + "strikethroughPosition", + "strikethroughThickness", + "string", + "stroke", + "strokeDasharray", + "strokeDashoffset", + "strokeLinecap", + "strokeLinejoin", + "strokeMiterlimit", + "strokeOpacity", + "strokeWidth", + "surfaceScale", + "systemLanguage", + "tableValues", + "targetX", + "targetY", + "textAnchor", + "textDecoration", + "textRendering", + "textLength", + "to", + "transform", + "u1", + "u2", + "underlinePosition", + "underlineThickness", + "unicode", + "unicodeBidi", + "unicodeRange", + "unitsPerEm", + "vAlphabetic", + "vHanging", + "vIdeographic", + "vMathematical", + "values", + "vectorEffect", + "version", + "vertAdvY", + "vertOriginX", + "vertOriginY", + "viewBox", + "viewTarget", + "visibility", + "widths", + "wordSpacing", + "writingMode", + "x", + "xHeight", + "x1", + "x2", + "xChannelSelector", + "xlinkActuate", + "xlinkArcrole", + "xlinkHref", + "xlinkRole", + "xlinkShow", + "xlinkTitle", + "xlinkType", + "xmlBase", + "xmlns", + "xmlnsXlink", + "xmlLang", + "xmlSpace", + "y", + "y1", + "y2", + "yChannelSelector", + "z", + "zoomAndPan", + "for", + "class", + "autofocus", +}; + +/// Whether React passes `name` to an element as an attribute +pub fn is_prop_valid(name: &str) -> bool { + VALID_PROPS.contains(name) + || name.starts_with("data-") + || name.starts_with("aria-") + || name.starts_with("x-") + || name + .strip_prefix("on") + .is_some_and(|event| event.starts_with(|c: char| c.is_ascii_uppercase())) +} diff --git a/libs/extractor/src/snapshots/extractor__tests__compiled_bindings_never_dangle.snap b/libs/extractor/src/snapshots/extractor__tests__compiled_bindings_never_dangle.snap index 45662cd31..70a702afc 100644 --- a/libs/extractor/src/snapshots/extractor__tests__compiled_bindings_never_dangle.snap +++ b/libs/extractor/src/snapshots/extractor__tests__compiled_bindings_never_dangle.snap @@ -5,7 +5,7 @@ expression: outputs [ ( "import { css, styled, Box } from '@devup-ui/react';\nconst myCss = css;\nconst again = myCss;\nconst S = styled;\nconst B = Box;\nexport const a = again({ color: 'red' });\nexport const s = S.div({ color: 'blue' });\nexport const b = <B p={1} />;", - "export const a = \"a-a\";\nexport const s = ({ style, className, ...rest }) => <div {...rest} className={[\"a-b\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const b = <div className=\"a-c\" />;", + "export const a = \"a-a\";\nexport const s = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-b\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const b = <div className=\"a-c\" />;", ), ( "import { css } from '@devup-ui/react';\nexport function f(css) { return css; }\nexport const a = css({ color: 'red' });", diff --git a/libs/extractor/src/snapshots/extractor__tests__compose_rules_the_module_computes.snap b/libs/extractor/src/snapshots/extractor__tests__compose_rules_the_module_computes.snap index 5325a6ce2..6c5c65ddc 100644 --- a/libs/extractor/src/snapshots/extractor__tests__compose_rules_the_module_computes.snap +++ b/libs/extractor/src/snapshots/extractor__tests__compose_rules_the_module_computes.snap @@ -64,6 +64,16 @@ ToBTreeSet { layer: None, }, ), + Static( + ExtractStaticStyle { + property: "padding", + value: "4px", + level: 0, + selector: None, + style_order: None, + layer: None, + }, + ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as Devup from \"@devup-ui/react\";\nimport * as tokens from \"./styles\";\nimport { card, rules } from \"./styles\";\nimport { emotionClass } from \"./emotion\";\nconst make = (n: number) => ({ m: n });\nconst local = make(2);\nconst computed = make(3);\nconst name = String(\"named\");\nconst shared = {\n\trules: make(4),\n\tfixed: { p: 5 }\n};\nexport const a = \"a-d\";\nexport const b = `a-e ${card}`;\nexport const c = `named x ${`y`}`;\nexport const d = ({ style, className, ...rest }) => <div {...rest} className={[\"a-d\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const e = ({ style, className, ...rest }) => <span {...rest} className={[\"a-e\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const f = (__devupProps) => (({ style, className, ...rest }) => <p {...rest} className={[\"a-f\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"note\" }\n});\nexport const g = (on) => `a-d a-a ${on ? \"\" : card} ${on ? typeof on === \"string\" ? on : \"\" : \"\"}`;\nexport const h = \"a-b\";\nexport const i = \"a-d\";\nexport const k = `k ${tokens.card}`;\nexport const l = `a-c ${emotionClass}`;\nexport const n = <Devup.Layout.Box {...{ \"m\": 2 }} />;\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as Devup from \"@devup-ui/react\";\nimport * as tokens from \"./styles\";\nimport { card, rules } from \"./styles\";\nimport { emotionClass } from \"./emotion\";\nconst make = (n: number) => ({ m: n });\nconst local = make(2);\nconst computed = make(3);\nconst name = String(\"named\");\nconst shared = {\n\trules: make(4),\n\tfixed: { p: 5 }\n};\nexport const a = \"a-e\";\nexport const b = \"a-a a-f\";\nexport const c = `named x ${`y`}`;\nexport const d = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-e\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const e = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-f\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const f = (__devupProps) => (({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-g\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"note\" }\n});\nexport const g = (on) => `a-e a-b ${on ? typeof on === \"string\" ? on : \"\" : \"\"}`;\nexport const h = \"a-c\";\nexport const i = \"a-e\";\nexport const k = `k ${tokens.card}`;\nexport const l = `a-d ${emotionClass}`;\nexport const n = <Devup.Layout.Box {...{ \"m\": 2 }} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__composed_css_keeps_composed_classes.snap b/libs/extractor/src/snapshots/extractor__tests__composed_css_keeps_composed_classes.snap index 12974320c..a6019f8f9 100644 --- a/libs/extractor/src/snapshots/extractor__tests__composed_css_keeps_composed_classes.snap +++ b/libs/extractor/src/snapshots/extractor__tests__composed_css_keeps_composed_classes.snap @@ -35,5 +35,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst base = \"a\";\nexport const a = \"a b\";\nexport const b = \"a extra c\";\nexport const c = \"a\";\nexport const d = cond ? \"a\" : \"\";\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst base = \"a\";\nexport const a = \"a b\";\nexport const b = \"extra a c\";\nexport const c = \"a\";\nexport const d = cond ? \"a\" : \"\";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__compute_what_elements_and_styled_objects_take.snap b/libs/extractor/src/snapshots/extractor__tests__compute_what_elements_and_styled_objects_take.snap index 0288b22d1..e59dd6ea2 100644 --- a/libs/extractor/src/snapshots/extractor__tests__compute_what_elements_and_styled_objects_take.snap +++ b/libs/extractor/src/snapshots/extractor__tests__compute_what_elements_and_styled_objects_take.snap @@ -99,5 +99,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as Devup from \"@devup-ui/react\";\nimport { PRIMARY } from \"./tokens\";\nconst make = (n: number) => ({ m: n });\nconst KEY = [\"co\", \"lor\"].join(\"\");\nfunction darken(amount: number, color: string) {\n\treturn color === PRIMARY ? \"#112233\" : color;\n}\nconst card = make(2);\nconst hover = make(3);\nconst parts = [make(4), { p: 1 }];\nexport const a = <div onClick={() => 1} className=\"a-c\" />;\nexport const b = <div className=\"a-d\" />;\nexport const c = <div className=\"a-e\" />;\nexport const d = <div className=\"a-a a-c\" />;\nexport const e = ({ style, className, ...rest }) => <div {...rest} className={[\"a-f a-e a-g\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const f = \"a-h a-a\";\nexport const g = \"a-i\";\nexport const h = <div {...card} />;\nfunction local() {\n\treturn { p: 7 };\n}\nexport const i = <div className=\"a-j\" />;\nexport const j = (register) => ((__devupSpread0) => <div {...__devupSpread0} className={__devupSpread0?.className || \"\"} style={__devupSpread0?.style} />)(register(\"email\"));\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as Devup from \"@devup-ui/react\";\nimport { PRIMARY } from \"./tokens\";\nconst make = (n: number) => ({ m: n });\nconst KEY = [\"co\", \"lor\"].join(\"\");\nfunction darken(amount: number, color: string) {\n\treturn color === PRIMARY ? \"#112233\" : color;\n}\nconst card = make(2);\nconst hover = make(3);\nconst parts = [make(4), { p: 1 }];\nexport const a = <div onClick={() => 1} className=\"a-c\" />;\nexport const b = <div className=\"a-d\" />;\nexport const c = <div className=\"a-e\" />;\nexport const d = <div className=\"a-a a-c\" />;\nexport const e = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-f a-e a-g\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const f = \"a-h a-a\";\nexport const g = \"a-i\";\nexport const h = <div {...card} />;\nfunction local() {\n\treturn { p: 7 };\n}\nexport const i = <div className=\"a-j\" />;\nexport const j = (register) => ((__devupSpread0) => <div {...__devupSpread0} className={__devupSpread0?.className || \"\"} style={__devupSpread0?.style} />)(register(\"email\"));\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__conditional_composition.snap b/libs/extractor/src/snapshots/extractor__tests__conditional_composition.snap index d60be1a7c..0630f2cba 100644 --- a/libs/extractor/src/snapshots/extractor__tests__conditional_composition.snap +++ b/libs/extractor/src/snapshots/extractor__tests__conditional_composition.snap @@ -117,5 +117,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nexport const a = `a ${cond ? \"b\" : \"c\"} ${cond ? \"d\" : \"\"}`;\nexport const b = `${flag ? \"e\" : \"f\"} ${flag ? \"\" : \"g\"}`;\nexport const c = `c ${cond ? other : \"\"} ${flag ? \"x\" : \"\"}`;\nexport const d = `i ${cond ? \"h\" : \"\"}`;\nexport const e = `${flag ? other : \"\"} ${flag ? \"c\" : \"b\"}`;\nexport const h = `${cond ? typeof cond === \"string\" ? cond : \"\" : \"\"} ${cond ? \"c\" : \"b\"}`;\nexport const i = \"c\";\nexport const j = `${value != null ? typeof value === \"string\" ? value : \"\" : \"\"} ${value != null ? \"c\" : \"b\"}`;\nexport const K = ({ style, className, ...rest }) => <div {...rest} className={[`${base} ${`j ${cond ? \"b\" : \"\"}`}`, className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nexport const a = `a ${cond ? \"b\" : \"c\"} ${cond ? \"d\" : \"\"}`;\nexport const b = `${flag ? \"e\" : \"f\"} ${flag ? \"\" : \"g\"}`;\nexport const c = `c ${cond ? other : \"\"} ${flag ? \"x\" : \"\"}`;\nexport const d = `i ${cond ? \"h\" : \"\"}`;\nexport const e = `${flag ? other : \"\"} ${flag ? \"c\" : \"b\"}`;\nexport const h = `${cond ? typeof cond === \"string\" ? cond : \"\" : \"\"} ${cond ? \"c\" : \"b\"}`;\nexport const i = \"c\";\nexport const j = `${value != null ? typeof value === \"string\" ? value : \"\" : \"\"} ${value != null ? \"c\" : \"b\"}`;\nexport const K = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[`${base} ${`j ${cond ? \"b\" : \"\"}`}`, className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__constant_objects_arrays_and_enums.snap b/libs/extractor/src/snapshots/extractor__tests__constant_objects_arrays_and_enums.snap index 5ca82d42b..2e697926b 100644 --- a/libs/extractor/src/snapshots/extractor__tests__constant_objects_arrays_and_enums.snap +++ b/libs/extractor/src/snapshots/extractor__tests__constant_objects_arrays_and_enums.snap @@ -305,6 +305,6 @@ Ok( }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as Devup from \"@devup-ui/react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { space, Size, Local, focus } from \"./tokens\";\nconst base = {\n\tp: 4,\n\tcolor: \"red\",\n\tcolor: \"blue\"\n};\nconst hover = { opacity: .5 };\nconst merged = {\n\t...base,\n\tm: 1\n};\nconst partial = {\n\t...unknown,\n\tm: 1\n};\nconst proto = {\n\t__proto__: null,\n\ta: 1\n};\nconst getter = { get a() {\n\treturn 1;\n} };\nexport const withGetter = <div className=\"a-a\" style={{ \"--a\": getter.a }} />;\nconst NONE = null;\nconst MEDIA = \"@media (min-width: 1px)\";\nconst keyed = { [MEDIA]: { color: \"green\" } };\nconst widths = [\n\t10,\n\tnull,\n\t20\n];\nconst more = [...widths, 30];\nconst bad = [...unknown];\nenum Level {\n\tLow = 1,\n\tHigh,\n\tName = \"n\",\n\tComputed = Math.max(1, 2),\n\tLast = 9\n}\ndeclare enum Ambient {\n\tX = 1\n}\nconst sx = { color: \"red\" };\nexport const a = \"a-b a-c\";\nexport const b = \"a-b a-d a-e a-f\";\nexport const c = \"a-b a-c a-g\";\nexport const d = ({ style, className, ...rest }) => <div {...rest} className={[\"a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const e = ({ style, className, ...rest }) => <div {...rest} className={[\"a-b a-c a-e\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const f = (Comp, handler) => <Comp onClick={handler} className=\"a-c a-e a-h a-i a-j a-k a-l a-m a-n a-o a-p a-q a-r\" {...base} />;\nexport const g = <div className=\"a-e\" />;\nexport const h = \"a-b a-c\";\nexport const i = ({ style, className, ...rest }) => <div {...rest} className={[\"a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const j = (__devupProps) => (({ style, className, ...rest }) => <div {...rest} className={[\"a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"button\" }\n});\nexport const k = { \"x\": \"a-s\" };\n;\nexport const l = \"a-t\";\nexport const m = \"a-u a-v a-y a-z\";\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as Devup from \"@devup-ui/react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { space, Size, Local, focus } from \"./tokens\";\nconst base = {\n\tp: 4,\n\tcolor: \"red\",\n\tcolor: \"blue\"\n};\nconst hover = { opacity: .5 };\nconst merged = {\n\t...base,\n\tm: 1\n};\nconst partial = {\n\t...unknown,\n\tm: 1\n};\nconst proto = {\n\t__proto__: null,\n\ta: 1\n};\nconst getter = { get a() {\n\treturn 1;\n} };\nexport const withGetter = <div className=\"a-a\" style={{ \"--a\": getter.a }} />;\nconst NONE = null;\nconst MEDIA = \"@media (min-width: 1px)\";\nconst keyed = { [MEDIA]: { color: \"green\" } };\nconst widths = [\n\t10,\n\tnull,\n\t20\n];\nconst more = [...widths, 30];\nconst bad = [...unknown];\nenum Level {\n\tLow = 1,\n\tHigh,\n\tName = \"n\",\n\tComputed = Math.max(1, 2),\n\tLast = 9\n}\ndeclare enum Ambient {\n\tX = 1\n}\nconst sx = { color: \"red\" };\nexport const a = \"a-b a-c\";\nexport const b = \"a-b a-d a-e a-f\";\nexport const c = \"a-b a-c a-g\";\nexport const d = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const e = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-b a-c a-e\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const f = (Comp, handler) => <Comp onClick={handler} className=\"a-c a-e a-h a-i a-j a-k a-l a-m a-n a-o a-p a-q a-r\" {...base} />;\nexport const g = <div className=\"a-e\" />;\nexport const h = \"a-b a-c\";\nexport const i = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const j = (__devupProps) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-b a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"button\" }\n});\nexport const k = { \"x\": \"a-s\" };\n;\nexport const l = \"a-t\";\nexport const m = \"a-u a-v a-y a-z\";\n", }, ) diff --git a/libs/extractor/src/snapshots/extractor__tests__create_global_style_collapses_to_a_null_component.snap b/libs/extractor/src/snapshots/extractor__tests__create_global_style_collapses_to_a_null_component.snap index 20f438589..53e91efdd 100644 --- a/libs/extractor/src/snapshots/extractor__tests__create_global_style_collapses_to_a_null_component.snap +++ b/libs/extractor/src/snapshots/extractor__tests__create_global_style_collapses_to_a_null_component.snap @@ -38,5 +38,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst GlobalStyle = () => null;\nconst FromObject = () => null;\nconst S = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const App = () => <><GlobalStyle /><FromObject /><S /></>;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst GlobalStyle = () => null;\nconst FromObject = () => null;\nconst S = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const App = () => <><GlobalStyle /><FromObject /><S /></>;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__dynamic_as_elements.snap b/libs/extractor/src/snapshots/extractor__tests__dynamic_as_elements.snap index 9643beb44..2a72f6b7e 100644 --- a/libs/extractor/src/snapshots/extractor__tests__dynamic_as_elements.snap +++ b/libs/extractor/src/snapshots/extractor__tests__dynamic_as_elements.snap @@ -73,7 +73,7 @@ expression: outputs ), ( "<Box as={['div', 'a'][i]} {...register('x')} p={1} />", - "((DevupAs0, __devupSpread0) => <DevupAs0 {...__devupSpread0} className={`a-a ${__devupSpread0?.className || \"\"}`} style={__devupSpread0?.style} />)([\"div\", \"a\"][i] || \"div\", register(\"x\"));", + "((DevupAs0, __devupSpread0) => <DevupAs0 {...__devupSpread0} className={`${__devupSpread0?.className || \"\"} a-a`} style={__devupSpread0?.style} />)([\"div\", \"a\"][i] || \"div\", register(\"x\"));", ), ( "<Text as={b ? 'h1' : undefined} />", diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles-2.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles-2.snap index c5b7c96d9..2a58322f7 100644 --- a/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles-2.snap @@ -45,5 +45,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nexport const G = (__devupProps) => (({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ type: \"button\" }\n});\nexport const H = ({ style, className, ...rest }) => <Base {...rest} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const I = ({ style, className, ...rest }) => <div {...rest} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const j = \"d\";\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nexport const G = (__devupProps) => (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ type: \"button\" }\n});\nexport const H = ({ style, className, as: DevupAs = Base, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const I = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const j = \"d\";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles.snap index 6bc7e57e1..433a1b687 100644 --- a/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles.snap +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_and_styled_components_object_styles.snap @@ -166,5 +166,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { Global } from \"@devup-ui/react/compat\";\nexport const a = \"a b c d\";\nexport const B = ({ style, className, ...rest }) => <div {...rest} className={[\"e\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const C = ({ style, className, ...rest }) => <span {...rest} className={[\"f\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const D = ({ style, className, ...rest }) => <div {...rest} className={[\"f g h i\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const E = ({ style, className, ...rest }) => <div {...rest} className={[`j ${cond ? \"k\" : \"\"}`, className].filter(Boolean).join(\" \")} style={style} />;\nexport const F = ({ style, className, ...rest }) => <p {...rest} className={[\"l\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const spin = \"m\";\nexport const App = () => <><Global /><Other styles={{ top: 1 }} /><Global /></>;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { Global } from \"@devup-ui/react/compat\";\nexport const a = \"a b c d\";\nexport const B = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"x\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"e\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const C = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"f\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const D = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"f g h i\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const E = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[`j ${cond ? \"k\" : \"\"}`, className].filter(Boolean).join(\" \")} style={style} />;\nexport const F = ({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"l\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const spin = \"m\";\nexport const App = () => <><Global /><Other styles={{ top: 1 }} /><Global /></>;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__emotion_global_component_and_import_surface.snap b/libs/extractor/src/snapshots/extractor__tests__emotion_global_component_and_import_surface.snap index 180ee6b42..223433cad 100644 --- a/libs/extractor/src/snapshots/extractor__tests__emotion_global_component_and_import_surface.snap +++ b/libs/extractor/src/snapshots/extractor__tests__emotion_global_component_and_import_surface.snap @@ -32,5 +32,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { Global, ThemeProvider, useTheme } from \"@devup-ui/react/compat\";\nimport { ClassNames } from \"@emotion/react\";\nconst S = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const App = () => <><Global /><S /></>;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { Global, ThemeProvider, useTheme } from \"@devup-ui/react/compat\";\nimport { ClassNames } from \"@emotion/react\";\nconst S = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const App = () => <><Global /><S /></>;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__evaluate_build_time_values.snap b/libs/extractor/src/snapshots/extractor__tests__evaluate_build_time_values.snap index 290d709ff..68b4845ca 100644 --- a/libs/extractor/src/snapshots/extractor__tests__evaluate_build_time_values.snap +++ b/libs/extractor/src/snapshots/extractor__tests__evaluate_build_time_values.snap @@ -229,5 +229,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { PRIMARY, SPACE } from \"./color\";\nimport { card } from \"./styles\";\nimport { BEFORE } from \"./fails\";\nexport const SIZE = 4;\nexport default function twice(n: number) {\n\treturn n * 2;\n}\nenum Level {\n\tLow = 1,\n\tHigh = twice(2)\n}\nfunction darken(amount: number, color: string): string {\n\treturn color === PRIMARY ? `darker(${amount})` : color;\n}\nconst LIGHT = darken(.2, PRIMARY);\nconst color = darken(.3, PRIMARY);\nconst make = (size: number) => ({\n\tp: size,\n\t_hover: { color: LIGHT }\n});\nconst label = (text: string) => text.toUpperCase().padStart(4, \"-\");\nexport const a = \"a-n a-o a-p a-q a-e a-r\";\nexport const b = \"a-s\";\n;\nconst styles = { \"base\": \"a-t\" };\nexport const c = <div {...{ className: \"a-t\" }} className={`a-a ${{ className: \"a-t\" }?.className || \"\"}`} style={{ \"--a\": darken(.4,`#336699`) }} />;\nexport const d = \"a-u a-v a-w a-x a-y a-i\";\nexport const e = (dark: boolean) => `a-_ ${dark ? \"a-t\" : \"a-j\"} ${dark ? \"a-z\" : \"\"}`;\nexport const f = `a-aa ${card}`;\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { PRIMARY, SPACE } from \"./color\";\nimport { card } from \"./styles\";\nimport { BEFORE } from \"./fails\";\nexport const SIZE = 4;\nexport default function twice(n: number) {\n\treturn n * 2;\n}\nenum Level {\n\tLow = 1,\n\tHigh = twice(2)\n}\nfunction darken(amount: number, color: string): string {\n\treturn color === PRIMARY ? `darker(${amount})` : color;\n}\nconst LIGHT = darken(.2, PRIMARY);\nconst color = darken(.3, PRIMARY);\nconst make = (size: number) => ({\n\tp: size,\n\t_hover: { color: LIGHT }\n});\nconst label = (text: string) => text.toUpperCase().padStart(4, \"-\");\nexport const a = \"a-n a-o a-p a-q a-e a-r\";\nexport const b = \"a-s\";\n;\nconst styles = { \"base\": \"a-t\" };\nexport const c = <div {...{ className: \"a-t\" }} className=\"a-t a-a\" style={{ \"--a\": darken(.4,`#336699`) }} />;\nexport const d = \"a-u a-v a-w a-x a-y a-i\";\nexport const e = (dark: boolean) => `a-_ ${dark ? \"a-t\" : \"a-j\"} ${dark ? \"a-z\" : \"\"}`;\nexport const f = \"a-aa\";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__every_runtime_value_an_element_holds.snap b/libs/extractor/src/snapshots/extractor__tests__every_runtime_value_an_element_holds.snap index 0be1eadf6..7add116c6 100644 --- a/libs/extractor/src/snapshots/extractor__tests__every_runtime_value_an_element_holds.snap +++ b/libs/extractor/src/snapshots/extractor__tests__every_runtime_value_an_element_holds.snap @@ -87,5 +87,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nclass A {\n\t#w = 1;\n\trender(f) {\n\t\treturn ((__devupSpread0) => <div {...this.#w} {...__devupSpread0} className={`a-a ${__devupSpread0?.className || \"\"} ${this.#w?.className || \"\"}`} style={{\n\t\t\t...{ \"--a\": this.#w },\n\t\t\t...__devupSpread0?.style,\n\t\t\t...this.#w?.style\n\t\t}} />)(f());\n\t}\n}\nexport const a = async (f, X, tag) => <div className=\"a-a a-b a-c a-d a-e a-f a-g a-h\" style={{\n\t\"--a\": await f(),\n\t\"--b\": (f(),2),\n\t\"--c\": new X,\n\t\"--d\": tag`x`,\n\t\"--e\": 10n,\n\t\"--f\": f.n++,\n\t\"--g\": f.v=3\n}} />;\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nclass A {\n\t#w = 1;\n\trender(f) {\n\t\treturn ((__devupSpread0) => <div {...this.#w} {...__devupSpread0} className={`${(\"className\" in Object(__devupSpread0) ? __devupSpread0.className : this.#w?.className) || \"\"} a-a`} style={{\n\t\t\t...{ \"--a\": this.#w },\n\t\t\t...\"style\" in Object(__devupSpread0) ? __devupSpread0.style : this.#w?.style\n\t\t}} />)(f());\n\t}\n}\nexport const a = async (f, X, tag) => <div className=\"a-a a-b a-c a-d a-e a-f a-g a-h\" style={{\n\t\"--a\": await f(),\n\t\"--b\": (f(),2),\n\t\"--c\": new X,\n\t\"--d\": tag`x`,\n\t\"--e\": 10n,\n\t\"--f\": f.n++,\n\t\"--g\": f.v=3\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_selector-2.snap b/libs/extractor/src/snapshots/extractor__tests__extract_selector-2.snap index 91ed4fbf2..7abad9202 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_selector-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_selector-2.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {Center} from '@devup-ui/core'\n <Center\n _active={\n variant !== 'disabled' && {\n boxShadow: 'none',\n transform: 'scale(0.95)',\n }\n }\n _hover={\n variant !== 'disabled' && {\n boxShadow: [\n '0px 1px 3px 0px rgba(0, 0, 0, 0.25)',\n null,\n '0px 0px 15px 0px rgba(0, 0, 0, 0.25)',\n ],\n }\n }\n {...props}\n >\n {children}\n </Center>\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {Center} from '@devup-ui/core'\n <Center\n _active={\n variant !== 'disabled' && {\n boxShadow: 'none',\n transform: 'scale(0.95)',\n }\n }\n _hover={\n variant !== 'disabled' && {\n boxShadow: [\n '0px 1px 3px 0px rgba(0, 0, 0, 0.25)',\n null,\n '0px 0px 15px 0px rgba(0, 0, 0, 0.25)',\n ],\n }\n }\n {...props}\n >\n {children}\n </Center>\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -97,5 +97,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n<div {...props} className={`a b c ${variant !== \"disabled\" ? \"d e\" : \"\"} ${variant !== \"disabled\" ? \"f g\" : \"\"} ${props?.className || \"\"}`} style={props?.style}>\n {children}\n </div>;\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n<div {...props} className={`${props?.className || \"\"} a b c ${variant !== \"disabled\" ? \"d e\" : \"\"} ${variant !== \"disabled\" ? \"f g\" : \"\"}`} style={props?.style}>\n {children}\n </div>;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__extract_style_props_with_class_name-9.snap b/libs/extractor/src/snapshots/extractor__tests__extract_style_props_with_class_name-9.snap index eea9d4bdc..899efdf91 100644 --- a/libs/extractor/src/snapshots/extractor__tests__extract_style_props_with_class_name-9.snap +++ b/libs/extractor/src/snapshots/extractor__tests__extract_style_props_with_class_name-9.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { Box, Button as DevupButton, Center, css } from '@devup-ui/core'\nimport clsx from 'clsx'\n\n<DevupButton\n boxSizing=\"border-box\"\n className={clsx(\n variants[variant],\n isError && variant === 'default' && errorClassNames,\n className,\n )}\n typography={\n isPrimary\n ? {\n sm: 'buttonS',\n md: 'buttonM',\n }[size]\n : undefined\n }\n {...props}\n />\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { Box, Button as DevupButton, Center, css } from '@devup-ui/core'\nimport clsx from 'clsx'\n\n<DevupButton\n boxSizing=\"border-box\"\n className={clsx(\n variants[variant],\n isError && variant === 'default' && errorClassNames,\n className,\n )}\n typography={\n isPrimary\n ? {\n sm: 'buttonS',\n md: 'buttonM',\n }[size]\n : undefined\n }\n {...props}\n />\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -21,5 +21,5 @@ ToBTreeSet { "buttonS", ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\nimport clsx from \"clsx\";\n<button {...props} className={`${clsx(variants[variant], isError && variant === \"default\" && errorClassNames, className) || \"\"} a ${isPrimary ? {\n\t\"md\": \"typo-buttonM\",\n\t\"sm\": \"typo-buttonS\"\n}[size] || \"\" : \"\"}`} style={props?.style} />;\n", + code: "import \"@devup-ui/core/devup-ui.css\";\nimport clsx from \"clsx\";\n<button {...props} className={`${(\"className\" in Object(props) ? props.className : clsx(variants[variant], isError && variant === \"default\" && errorClassNames, className)) || \"\"} a ${isPrimary ? {\n\t\"md\": \"typo-buttonM\",\n\t\"sm\": \"typo-buttonS\"\n}[size] || \"\" : \"\"}`} style={props?.style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__import_alias_custom_named_export.snap b/libs/extractor/src/snapshots/extractor__tests__import_alias_custom_named_export.snap index da0a4d6d0..694121d63 100644 --- a/libs/extractor/src/snapshots/extractor__tests__import_alias_custom_named_export.snap +++ b/libs/extractor/src/snapshots/extractor__tests__import_alias_custom_named_export.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import emotionStyled from '@emotion/styled'\nconst Button = emotionStyled.button({ bg: 'purple', p: 3 })\n\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases,\n},).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import emotionStyled from '@emotion/styled'\nconst Button = emotionStyled.button({ bg: 'purple', p: 3 })\n\",\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases\n},).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst Button = ({ style, className, ...rest }) => <button {...rest} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst Button = ({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__import_alias_emotion_styled.snap b/libs/extractor/src/snapshots/extractor__tests__import_alias_emotion_styled.snap index 245dbb604..36575061c 100644 --- a/libs/extractor/src/snapshots/extractor__tests__import_alias_emotion_styled.snap +++ b/libs/extractor/src/snapshots/extractor__tests__import_alias_emotion_styled.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import styled from '@emotion/styled'\nconst Button = styled.button({ bg: 'red', p: 4 })\n\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases,\n},).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import styled from '@emotion/styled'\nconst Button = styled.button({ bg: 'red', p: 4 })\n\",\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases\n},).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst Button = ({ style, className, ...rest }) => <button {...rest} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst Button = ({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__import_alias_multiple_aliases.snap b/libs/extractor/src/snapshots/extractor__tests__import_alias_multiple_aliases.snap index 76f4237b1..544825143 100644 --- a/libs/extractor/src/snapshots/extractor__tests__import_alias_multiple_aliases.snap +++ b/libs/extractor/src/snapshots/extractor__tests__import_alias_multiple_aliases.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import styled from '@emotion/styled'\nconst Button = styled.button({ bg: 'red' })\n\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases,\n},).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import styled from '@emotion/styled'\nconst Button = styled.button({ bg: 'red' })\n\",\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases\n},).unwrap())" --- ToBTreeSet { styles: { @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst Button = ({ style, className, ...rest }) => <button {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst Button = ({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__import_alias_renamed_import.snap b/libs/extractor/src/snapshots/extractor__tests__import_alias_renamed_import.snap index 0dc8e1db1..8b5b46622 100644 --- a/libs/extractor/src/snapshots/extractor__tests__import_alias_renamed_import.snap +++ b/libs/extractor/src/snapshots/extractor__tests__import_alias_renamed_import.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import myStyled from '@emotion/styled'\nconst Button = myStyled.button({ bg: 'green', p: 2 })\n\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases,\n},).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import myStyled from '@emotion/styled'\nconst Button = myStyled.button({ bg: 'green', p: 2 })\n\",\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases\n},).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst Button = ({ style, className, ...rest }) => <button {...rest} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst Button = ({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__import_alias_styled_components.snap b/libs/extractor/src/snapshots/extractor__tests__import_alias_styled_components.snap index 664c9fa69..7b93f36df 100644 --- a/libs/extractor/src/snapshots/extractor__tests__import_alias_styled_components.snap +++ b/libs/extractor/src/snapshots/extractor__tests__import_alias_styled_components.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import styled from 'styled-components'\nconst Card = styled(\"div\")({ bg: 'blue', m: 2 })\n\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases,\n},).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import styled from 'styled-components'\nconst Card = styled(\"div\")({ bg: 'blue', m: 2 })\n\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: aliases\n},).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst Card = ({ style, className, ...rest }) => <div {...rest} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst Card = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__inline_imported_constants.snap b/libs/extractor/src/snapshots/extractor__tests__inline_imported_constants.snap index c3fa67766..23c9a16dd 100644 --- a/libs/extractor/src/snapshots/extractor__tests__inline_imported_constants.snap +++ b/libs/extractor/src/snapshots/extractor__tests__inline_imported_constants.snap @@ -475,5 +475,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport * as Devup from \"@devup-ui/react\";\nimport { PRIMARY, SIZE, NEG, TEMPLATE, colors, ALIAS, MUTABLE, DYNAMIC, renamed, alsoMutable, reBase, notThere, gone, value, ns, NEG_STRING, TEMPLATE_EXPRESSION, SPACED, NOT_OBJECT, helper, looped } from \"./tokens\";\nimport orange from \"./tokens\";\nimport * as tokens from \"./tokens\";\nimport named from \"./named\";\nimport { handler } from \"./handler\";\nimport cjs, { COMPILED, FROM_REQUIRE, DESTRUCTURED, MUTATED, DYNAMIC } from \"./cjs-tokens\";\nimport cjsObject, { OBJ, OVERRIDDEN, LATER } from \"./cjs-object\";\nimport twelve from \"./cjs-value\";\nimport twice from \"./cjs-twice\";\nimport marker, { named as markerNamed } from \"./cjs-marker\";\nimport { unused } from \"./unused\";\nimport missing from \"./missing\";\nexport const a = <div {...colors} onClick={handler} className={`a b c d e f g h i k m n o p q r s t v x y z aa ac ae ag ai ak am ao ${colors?.className || \"\"}`} style={{\n\t...{\n\t\t\"--j\": MUTABLE,\n\t\t\"--l\": DYNAMIC,\n\t\t\"--u\": missing,\n\t\t\"--w\": NEG_STRING,\n\t\t\"--_\": NOT_OBJECT,\n\t\t\"--ab\": colors.skip,\n\t\t\"--a-d\": colors.nope,\n\t\t\"--af\": colors[key],\n\t\t\"--ah\": alsoMutable,\n\t\t\"--aj\": notThere,\n\t\t\"--al\": gone,\n\t\t\"--an\": helper,\n\t\t\"--ap\": looped\n\t},\n\t...colors?.style\n}} />;\nexport const b = \"a\";\nexport const c = <div className=\"aq\" />;\nexport const d = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const e = (PRIMARY) => <div className=\"ar\" style={{ \"--as\": PRIMARY }} />;\nexport const f = {\n\tPRIMARY,\n\tunused,\n\tSIZE: tokens.SIZE\n};\nexport const g = <div color={PRIMARY} />;\nexport const h = <Devup.Inner.Box color={4} />;\nexport const i = Devup[\"css\"]({ color: SIZE });\nexport const l = <div className=\"at au av aw ay az a0 a1 a2 a3 a4 a6 a8 a_\" style={{\n\t\"--ax\": MUTATED,\n\t\"--a5\": twice.A,\n\t\"--a7\": DYNAMIC,\n\t\"--a9\": marker\n}} />;\nexport const j = <div className=\"z ag\" style={{\n\t\"--_\": `red`.length,\n\t\"--ah\": `red`[`length`]\n}} />;\nexport const k = ({ style, className, ...rest }) => <div {...rest} className={[\"ba\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport * as Devup from \"@devup-ui/react\";\nimport { PRIMARY, SIZE, NEG, TEMPLATE, colors, ALIAS, MUTABLE, DYNAMIC, renamed, alsoMutable, reBase, notThere, gone, value, ns, NEG_STRING, TEMPLATE_EXPRESSION, SPACED, NOT_OBJECT, helper, looped } from \"./tokens\";\nimport orange from \"./tokens\";\nimport * as tokens from \"./tokens\";\nimport named from \"./named\";\nimport { handler } from \"./handler\";\nimport cjs, { COMPILED, FROM_REQUIRE, DESTRUCTURED, MUTATED, DYNAMIC } from \"./cjs-tokens\";\nimport cjsObject, { OBJ, OVERRIDDEN, LATER } from \"./cjs-object\";\nimport twelve from \"./cjs-value\";\nimport twice from \"./cjs-twice\";\nimport marker, { named as markerNamed } from \"./cjs-marker\";\nimport { unused } from \"./unused\";\nimport missing from \"./missing\";\nexport const a = <div {...colors} onClick={handler} className={`${colors?.className || \"\"} a b c d e f g h i k m n o p q r s t v x y z aa ac ae ag ai ak am ao`} style={{\n\t...{\n\t\t\"--j\": MUTABLE,\n\t\t\"--l\": DYNAMIC,\n\t\t\"--u\": missing,\n\t\t\"--w\": NEG_STRING,\n\t\t\"--_\": NOT_OBJECT,\n\t\t\"--ab\": colors.skip,\n\t\t\"--a-d\": colors.nope,\n\t\t\"--af\": colors[key],\n\t\t\"--ah\": alsoMutable,\n\t\t\"--aj\": notThere,\n\t\t\"--al\": gone,\n\t\t\"--an\": helper,\n\t\t\"--ap\": looped\n\t},\n\t...colors?.style\n}} />;\nexport const b = \"a\";\nexport const c = <div className=\"aq\" />;\nexport const d = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const e = (PRIMARY) => <div className=\"ar\" style={{ \"--as\": PRIMARY }} />;\nexport const f = {\n\tPRIMARY,\n\tunused,\n\tSIZE: tokens.SIZE\n};\nexport const g = <div color={PRIMARY} />;\nexport const h = <Devup.Inner.Box color={4} />;\nexport const i = Devup[\"css\"]({ color: SIZE });\nexport const l = <div className=\"at au av aw ay az a0 a1 a2 a3 a4 a6 a8 a_\" style={{\n\t\"--ax\": MUTATED,\n\t\"--a5\": twice.A,\n\t\"--a7\": DYNAMIC,\n\t\"--a9\": marker\n}} />;\nexport const j = <div className=\"z ag\" style={{\n\t\"--_\": `red`.length,\n\t\"--ah\": `red`[`length`]\n}} />;\nexport const k = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"ba\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__layer_record_places_every_declaration.snap b/libs/extractor/src/snapshots/extractor__tests__layer_record_places_every_declaration.snap index aa462f70d..7b41b3038 100644 --- a/libs/extractor/src/snapshots/extractor__tests__layer_record_places_every_declaration.snap +++ b/libs/extractor/src/snapshots/extractor__tests__layer_record_places_every_declaration.snap @@ -147,5 +147,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst A = ({ style, className, ...rest }) => <div {...rest} className={[`i k l m ${cond ? \"a\" : \"b\"} ${{\n\t\"bottom\": \"c\",\n\t\"bottom-left\": \"c d\",\n\t\"bottom-right\": \"c e\",\n\t\"left\": \"d\",\n\t\"right\": \"e\",\n\t\"top\": \"f\",\n\t\"top-left\": \"d f\",\n\t\"top-right\": \"e f\"\n}[pos] || \"\"} ${{\n\t\"a\": \"g\",\n\t\"b\": \"h\"\n}[key] || \"\"} ${typo ? `typo-${typo}` : \"\"}`, className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--j\": w },\n\t...style\n}} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst A = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[`i k l m ${cond ? \"a\" : \"b\"} ${{\n\t\"bottom\": \"c\",\n\t\"bottom-left\": \"c d\",\n\t\"bottom-right\": \"c e\",\n\t\"left\": \"d\",\n\t\"right\": \"e\",\n\t\"top\": \"f\",\n\t\"top-left\": \"d f\",\n\t\"top-right\": \"e f\"\n}[pos] || \"\"} ${{\n\t\"a\": \"g\",\n\t\"b\": \"h\"\n}[key] || \"\"} ${typo ? `typo-${typo}` : \"\"}`, className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--j\": w },\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__namespace_import_of_a_default_only_package_redirects.snap b/libs/extractor/src/snapshots/extractor__tests__namespace_import_of_a_default_only_package_redirects.snap index 361a2cc45..5ee515390 100644 --- a/libs/extractor/src/snapshots/extractor__tests__namespace_import_of_a_default_only_package_redirects.snap +++ b/libs/extractor/src/snapshots/extractor__tests__namespace_import_of_a_default_only_package_redirects.snap @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst Member = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nconst Tagged = ({ style, className, ...rest }) => <span {...rest} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst Member = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nconst Tagged = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__package_imported_whole.snap b/libs/extractor/src/snapshots/extractor__tests__package_imported_whole.snap index c983f75bc..1fffdfd3b 100644 --- a/libs/extractor/src/snapshots/extractor__tests__package_imported_whole.snap +++ b/libs/extractor/src/snapshots/extractor__tests__package_imported_whole.snap @@ -9,10 +9,10 @@ expression: outputs ), ( "import Devup from '@devup-ui/react';\nexport const S = Devup.styled.div({ color: 'red' });\nexport const T = Devup.styled('span', { color: 'blue' });\nexport const U = Devup.styled.p.attrs({ role: 'note' })({ m: 1 });\nexport const g = <Devup.Global styles={{ body: { p: 0 } }} />;", - "import \"@devup-ui/react/devup-ui-0.css\";\nimport Devup from \"@devup-ui/react\";\nexport const S = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const T = ({ style, className, ...rest }) => <span {...rest} className={[\"a-b\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const U = (__devupProps) => (({ style, className, ...rest }) => <p {...rest} className={[\"a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"note\" }\n});\nexport const g = <Devup.Global />;\n", + "import \"@devup-ui/react/devup-ui-0.css\";\nimport Devup from \"@devup-ui/react\";\nexport const S = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const T = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-b\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const U = (__devupProps) => (({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"note\" }\n});\nexport const g = <Devup.Global />;\n", ), ( "import * as Devup from '@devup-ui/react';\nimport { styled } from '@devup-ui/react';\nexport const S = styled.div({ p: 1 });\nexport const o = other.styled.div({ p: 1 });\nexport const m = <motion.div />;", - "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as Devup from \"@devup-ui/react\";\nexport const S = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const o = other.styled.div({ p: 1 });\nexport const m = <motion.div />;\n", + "import \"@devup-ui/react/devup-ui-0.css\";\nimport * as Devup from \"@devup-ui/react\";\nexport const S = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const o = other.styled.div({ p: 1 });\nexport const m = <motion.div />;\n", ), ] diff --git a/libs/extractor/src/snapshots/extractor__tests__read_once_in_every_place.snap b/libs/extractor/src/snapshots/extractor__tests__read_once_in_every_place.snap index 06a9bd75a..49dd95d0f 100644 --- a/libs/extractor/src/snapshots/extractor__tests__read_once_in_every_place.snap +++ b/libs/extractor/src/snapshots/extractor__tests__read_once_in_every_place.snap @@ -4,5 +4,5 @@ expression: "ToBTreeSet::from(output)" --- ToBTreeSet { styles: {}, - code: "import { jsx as _jsx } from \"react/jsx-runtime\";\nexport const a = async (f, g, list, C) => ((__devupSpread0, __devupSpread1, __devupSpread2, __devupSpread3, __devupSpread4, __devupSpread5) => <div {...__devupSpread0} data-x={list} aria-label=\"x\" disabled xlink:href={g} onClick={async () => await g()} title={__devupSpread1} className={__devupSpread0?.className || \"\"} style={__devupSpread0?.style}>\n text\n {__devupSpread2}\n {...__devupSpread3}\n {__devupSpread4}\n {__devupSpread5}\n after\n </div>)(f(), ((DevupAs0) => <DevupAs0 />)(C || \"b\" || \"div\"), <>{g}</>, list(), <span>{g}</span>, await g());\nexport const b = function* (f, g) {\n\tyield ((__devupSpread6, __devupSpread7) => <div {...__devupSpread6} title={__devupSpread7} className={__devupSpread6?.className || \"\"} style={__devupSpread6?.style} />)(f(), yield g);\n};\nexport const c = (props) => _jsx(\"div\", props);\nexport const d = async (f, g) => _jsx(\"div\", {\n\t...f(),\n\t[g()]: await g(),\n\tclassName: f()?.className || \"\",\n\tstyle: f()?.style\n});\nexport const e = async (rest, g) => _jsx(\"div\", {\n\tid: await g(),\n\t...rest,\n\tclassName: rest?.className || \"\",\n\tstyle: rest?.style\n});\nexport const h = (f, g, k) => ((__devupSpread8, __devupSpread9) => _jsx(\"div\", {\n\tonClick: __devupSpread8,\n\t[k]: 2,\n\t...__devupSpread9,\n\t[k]: 1,\n\ttitle: \"x\",\n\tclassName: \"z\",\n\tstyle: __devupSpread9?.style\n}))(g(), f());\nexport const i = (rest) => _jsx(\"div\", {\n\t...rest,\n\tclassName: \"a\",\n\tstyle: rest?.style\n});\nexport const j = (rest) => <div {...{ ...rest }} className={{ ...rest }?.className || \"\"} style={{ ...rest }?.style} />;\n", + code: "import { jsx as _jsx } from \"react/jsx-runtime\";\nexport const a = async (f, g, list, C) => ((__devupSpread0, __devupSpread1, __devupSpread2, __devupSpread3, __devupSpread4, __devupSpread5) => <div {...__devupSpread0} data-x={list} aria-label=\"x\" disabled xlink:href={g} onClick={async () => await g()} title={__devupSpread1} className={__devupSpread0?.className || \"\"} style={__devupSpread0?.style}>\n text\n {__devupSpread2}\n {...__devupSpread3}\n {__devupSpread4}\n {__devupSpread5}\n after\n </div>)(f(), ((DevupAs0) => <DevupAs0 />)(C || \"b\" || \"div\"), <>{g}</>, list(), <span>{g}</span>, await g());\nexport const b = function* (f, g) {\n\tyield ((__devupSpread6, __devupSpread7) => <div {...__devupSpread6} title={__devupSpread7} className={__devupSpread6?.className || \"\"} style={__devupSpread6?.style} />)(f(), yield g);\n};\nexport const c = (props) => _jsx(\"div\", props);\nexport const d = async (f, g) => _jsx(\"div\", {\n\t...f(),\n\t[g()]: await g(),\n\tclassName: f()?.className || \"\",\n\tstyle: f()?.style\n});\nexport const e = async (rest, g) => _jsx(\"div\", {\n\tid: await g(),\n\t...rest,\n\tclassName: rest?.className || \"\",\n\tstyle: rest?.style\n});\nexport const h = (f, g, k) => ((__devupSpread8, __devupSpread9) => _jsx(\"div\", {\n\tonClick: __devupSpread8,\n\t[k]: 2,\n\t...__devupSpread9,\n\t[k]: 1,\n\ttitle: \"x\",\n\tclassName: (\"className\" in Object(__devupSpread9) ? __devupSpread9.className : \"z\") || \"\",\n\tstyle: __devupSpread9?.style\n}))(g(), f());\nexport const i = (rest) => _jsx(\"div\", {\n\t...rest,\n\tclassName: \"a\",\n\tstyle: rest?.style\n});\nexport const j = (rest) => <div {...{ ...rest }} className={{ ...rest }?.className || \"\"} style={{ ...rest }?.style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__rest_props-2.snap b/libs/extractor/src/snapshots/extractor__tests__rest_props-2.snap index fd7f2da4b..a857b9b9b 100644 --- a/libs/extractor/src/snapshots/extractor__tests__rest_props-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__rest_props-2.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import { VStack } from '@devup-ui/core'\n\nexport default function Card({\n children,\n className,\n ...props\n}) {\n return (\n <VStack\n _active={{\n boxShadow: 'none',\n transform: 'scale(0.95)',\n }}\n className={className}\n {...props}\n >\n {children}\n </VStack>\n )\n}\n\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr\"import { VStack } from '@devup-ui/core'\n\nexport default function Card({\n children,\n className,\n ...props\n}) {\n return (\n <VStack\n _active={{\n boxShadow: 'none',\n transform: 'scale(0.95)',\n }}\n className={className}\n {...props}\n >\n {children}\n </VStack>\n )\n}\n\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -57,5 +57,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\nexport default function Card({ children, className, ...props }) {\n\treturn <div {...props} className={`${className || \"\"} a b c d`} style={props?.style}>\n {children}\n </div>;\n}\n", + code: "import \"@devup-ui/core/devup-ui.css\";\nexport default function Card({ children, className, ...props }) {\n\treturn <div {...props} className={`${(\"className\" in Object(props) ? props.className : className) || \"\"} a b c d`} style={props?.style}>\n {children}\n </div>;\n}\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__rest_props.snap b/libs/extractor/src/snapshots/extractor__tests__rest_props.snap index b6e6778a0..8a6feec1c 100644 --- a/libs/extractor/src/snapshots/extractor__tests__rest_props.snap +++ b/libs/extractor/src/snapshots/extractor__tests__rest_props.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Flex} from '@devup-ui/core'\n <Flex opacity={0.5} {...props} />\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr\"import {Flex} from '@devup-ui/core'\n <Flex opacity={0.5} {...props} />\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -27,5 +27,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\n<div {...props} className={`a b ${props?.className || \"\"}`} style={props?.style} />;\n", + code: "import \"@devup-ui/core/devup-ui.css\";\n<div {...props} className={`${props?.className || \"\"} a b`} style={props?.style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__spread_props.snap b/libs/extractor/src/snapshots/extractor__tests__spread_props.snap index 73bba2cab..002b72f0d 100644 --- a/libs/extractor/src/snapshots/extractor__tests__spread_props.snap +++ b/libs/extractor/src/snapshots/extractor__tests__spread_props.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\nconst Component = (props) => {\n return <Box {...props} bg=\"red\" />;\n}\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\nconst Component = (props) => {\n return <Box {...props} bg=\"red\" />;\n}\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst Component = (props) => {\n\treturn <div {...props} className={`a-a ${props?.className || \"\"}`} style={props?.style} />;\n};\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst Component = (props) => {\n\treturn <div {...props} className={`${props?.className || \"\"} a-a`} style={props?.style} />;\n};\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__spreads_are_read_once.snap b/libs/extractor/src/snapshots/extractor__tests__spreads_are_read_once.snap index 195bb7e80..180b22d09 100644 --- a/libs/extractor/src/snapshots/extractor__tests__spreads_are_read_once.snap +++ b/libs/extractor/src/snapshots/extractor__tests__spreads_are_read_once.snap @@ -45,5 +45,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { jsx as _jsx } from \"react/jsx-runtime\";\nexport const a = (register, rest, C) => ((__devupSpread1) => <div {...__devupSpread1} {...rest} className={`a-a ${rest?.className || \"\"} ${__devupSpread1?.className || \"\"}`} style={{\n\t...rest?.style,\n\t...__devupSpread1?.style\n}}>\n {((__devupSpread0) => <C {...__devupSpread0} className={__devupSpread0?.className || \"\"} style={__devupSpread0?.style} />)(useFocusRing())}\n </div>)(register(\"email\"));\nexport const b = async (load) => ((__devupSpread2) => <div {...__devupSpread2} className={`a-b ${__devupSpread2?.className || \"\"}`} style={__devupSpread2?.style} />)(await load());\nexport const c = (props) => <div {...{\n\tonClick: props.onClick,\n\ttitle: \"x\"\n}} {...props.rest} className={`a-c ${props.rest?.className || \"\"}`} style={props.rest?.style} />;\nexport const d = (f) => ((__devupSpread3) => _jsx(\"div\", {\n\t...__devupSpread3,\n\tclassName: `a-d ${__devupSpread3?.className || \"\"}`,\n\tstyle: __devupSpread3?.style\n}))(f());\nexport const e = async (f, g) => ((__devupSpread4, __devupSpread5) => _jsx(\"div\", {\n\t...__devupSpread4,\n\ttitle: __devupSpread5,\n\tclassName: __devupSpread4?.className || \"\",\n\tstyle: __devupSpread4?.style\n}))(f(), await g());\nexport const g = async (f, g, h) => ((__devupSpread6, __devupSpread7, __devupSpread8) => <div id={__devupSpread6} {...__devupSpread7} title={__devupSpread8} onClick={h} className={__devupSpread7?.className || \"\"} style={__devupSpread7?.style} />)(h(), f(), await g());\nexport const h = async (f, g) => ((__devupSpread9, __devupSpread10) => <div {...__devupSpread9} className={__devupSpread9?.className || \"\"} style={__devupSpread9?.style}>{__devupSpread10}</div>)(f(), await g());\nexport const i = async (f, g) => _jsx(\"div\", {\n\t...f(),\n\tclassName: await g() || \"\",\n\tstyle: f()?.style\n});\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { jsx as _jsx } from \"react/jsx-runtime\";\nexport const a = (register, rest, C) => ((__devupSpread1) => <div {...__devupSpread1} {...rest} className={`${(\"className\" in Object(rest) ? rest.className : __devupSpread1?.className) || \"\"} a-a`} style={\"style\" in Object(rest) ? rest.style : __devupSpread1?.style}>\n {((__devupSpread0) => <C {...__devupSpread0} className={__devupSpread0?.className || \"\"} style={__devupSpread0?.style} />)(useFocusRing())}\n </div>)(register(\"email\"));\nexport const b = async (load) => ((__devupSpread2) => <div {...__devupSpread2} className={`${__devupSpread2?.className || \"\"} a-b`} style={__devupSpread2?.style} />)(await load());\nexport const c = (props) => <div {...{\n\tonClick: props.onClick,\n\ttitle: \"x\"\n}} {...props.rest} className={`${props.rest?.className || \"\"} a-c`} style={props.rest?.style} />;\nexport const d = (f) => ((__devupSpread3) => _jsx(\"div\", {\n\t...__devupSpread3,\n\tclassName: `${__devupSpread3?.className || \"\"} a-d`,\n\tstyle: __devupSpread3?.style\n}))(f());\nexport const e = async (f, g) => ((__devupSpread4, __devupSpread5) => _jsx(\"div\", {\n\t...__devupSpread4,\n\ttitle: __devupSpread5,\n\tclassName: __devupSpread4?.className || \"\",\n\tstyle: __devupSpread4?.style\n}))(f(), await g());\nexport const g = async (f, g, h) => ((__devupSpread6, __devupSpread7, __devupSpread8) => <div id={__devupSpread6} {...__devupSpread7} title={__devupSpread8} onClick={h} className={__devupSpread7?.className || \"\"} style={__devupSpread7?.style} />)(h(), f(), await g());\nexport const h = async (f, g) => ((__devupSpread9, __devupSpread10) => <div {...__devupSpread9} className={__devupSpread9?.className || \"\"} style={__devupSpread9?.style}>{__devupSpread10}</div>)(f(), await g());\nexport const i = async (f, g) => _jsx(\"div\", {\n\t...f(),\n\tclassName: (\"className\" in Object(f()) ? f().className : await g()) || \"\",\n\tstyle: f()?.style\n});\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__style_results_read_as_values.snap b/libs/extractor/src/snapshots/extractor__tests__style_results_read_as_values.snap index 8bb5ad296..4d5e1bc52 100644 --- a/libs/extractor/src/snapshots/extractor__tests__style_results_read_as_values.snap +++ b/libs/extractor/src/snapshots/extractor__tests__style_results_read_as_values.snap @@ -141,5 +141,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { Global } from \"@devup-ui/react/compat\";\nconst fadeIn = \"a-a\";\nconst slide = \"a-b\";\nconst card = \"a-c\";\nexport const a = \"a-d a-e\";\nexport const b = \"a-c a-f\";\nexport const c = \"a-g\";\nexport const d = <div className=\"a-d a-h\" />;\nexport const E = ({ style, className, ...rest }) => <div {...rest} className={[\"a-i\", className].filter(Boolean).join(\" \")} style={style} />;\n;\nexport const g = <Global />;\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nimport { Global } from \"@devup-ui/react/compat\";\nconst fadeIn = \"a-a\";\nconst slide = \"a-b\";\nconst card = \"a-c\";\nexport const a = \"a-d a-e\";\nexport const b = \"a-c a-f\";\nexport const c = \"a-g\";\nexport const d = <div className=\"a-d a-h\" />;\nexport const E = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-i\", className].filter(Boolean).join(\" \")} style={style} />;\n;\nexport const g = <Global />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-10.snap b/libs/extractor/src/snapshots/extractor__tests__styled-10.snap index 85cf73c6c..c28fd8095 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-10.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-10.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledComponent = styled(CustomComponent)`\n background: red;\n color: blue;\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {styled} from '@devup-ui/core'\n const StyledComponent = styled(CustomComponent)`\n background: red;\n color: blue;\n `\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, ...rest }) => <CustomComponent {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, as: DevupAs = CustomComponent, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-11.snap b/libs/extractor/src/snapshots/extractor__tests__styled-11.snap index 03fdfcfda..c93ce180d 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-11.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-11.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.aside<{ test: string }>({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.aside<{ test: string }>({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <aside {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"aside\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-2.snap b/libs/extractor/src/snapshots/extractor__tests__styled-2.snap index ba2b5c8d1..f6ef344a7 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-2.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"article\")`\n background: red;\n color: blue;\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"article\")`\n background: red;\n color: blue;\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <article {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"article\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-3.snap b/libs/extractor/src/snapshots/extractor__tests__styled-3.snap index 13b9d5d53..775db0e0f 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-3.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"footer\")({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"footer\")({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <footer {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"footer\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-4.snap b/libs/extractor/src/snapshots/extractor__tests__styled-4.snap index 0fb06a00f..72d7b78a6 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-4.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-4.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.aside({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.aside({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <aside {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"aside\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-5.snap b/libs/extractor/src/snapshots/extractor__tests__styled-5.snap index 676942e6c..bddab10f7 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-5.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-5.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled, Text} from '@devup-ui/core'\n const StyledComponent = styled(Text)({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled, Text} from '@devup-ui/core'\n const StyledComponent = styled(Text)({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, ...rest }) => <span {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-6.snap b/libs/extractor/src/snapshots/extractor__tests__styled-6.snap index 53fdebe05..02247e0ef 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-6.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-6.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled, Text} from '@devup-ui/core'\n const StyledComponent = styled(Text)`\n background: red;\n color: blue;\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {styled, Text} from '@devup-ui/core'\n const StyledComponent = styled(Text)`\n background: red;\n color: blue;\n `\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, ...rest }) => <span {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-7.snap b/libs/extractor/src/snapshots/extractor__tests__styled-7.snap index a2298ab8c..619c9e387 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-7.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-7.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled, VStack} from '@devup-ui/core'\n const StyledComponent = styled(VStack)({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled, VStack} from '@devup-ui/core'\n const StyledComponent = styled(VStack)({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -39,5 +39,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, ...rest }) => <div {...rest} className={[\"a b a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-8.snap b/libs/extractor/src/snapshots/extractor__tests__styled-8.snap index 08caee07b..f4491959a 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-8.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-8.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled, VStack} from '@devup-ui/core'\n const StyledComponent = styled(VStack)`\n background: red;\n color: blue;\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {styled, VStack} from '@devup-ui/core'\n const StyledComponent = styled(VStack)`\n background: red;\n color: blue;\n `\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -49,5 +49,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, ...rest }) => <div {...rest} className={[\"a b a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b a b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-9.snap b/libs/extractor/src/snapshots/extractor__tests__styled-9.snap index d531f3264..3bc32524d 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled-9.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled-9.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledComponent = styled(CustomComponent)({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledComponent = styled(CustomComponent)({ bg: \"red\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, ...rest }) => <CustomComponent {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, as: DevupAs = CustomComponent, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled.snap b/libs/extractor/src/snapshots/extractor__tests__styled.snap index 55b01da51..dff273ebd 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.section`\n background: red;\n color: blue;\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.section`\n background: red;\n color: blue;\n `\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <section {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"section\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_accepts_both_call_forms.snap b/libs/extractor/src/snapshots/extractor__tests__styled_accepts_both_call_forms.snap index 8c53b9ab5..019897cf6 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_accepts_both_call_forms.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_accepts_both_call_forms.snap @@ -45,5 +45,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst twoArg = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nconst twoArgComponent = ({ style, className, ...rest }) => <div {...rest} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nconst curried = ({ style, className, ...rest }) => <span {...rest} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />;\nconst member = ({ style, className, ...rest }) => <p {...rest} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst twoArg = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nconst twoArgComponent = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nconst curried = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />;\nconst member = ({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_components_attrs_and_every_styled_import.snap b/libs/extractor/src/snapshots/extractor__tests__styled_components_attrs_and_every_styled_import.snap index bf4eabcbc..b398d2c9d 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_components_attrs_and_every_styled_import.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_components_attrs_and_every_styled_import.snap @@ -55,5 +55,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nexport const A = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const B = ({ style, className, ...rest }) => <span {...rest} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const C = (__devupProps) => (({ style, className, ...rest }) => <button {...rest} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ type: \"button\" }\n});\nexport const D = ({ style, className, ...rest }) => <Base {...rest} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const E = (__devupProps) => (({ style, className, ...rest }) => <input {...rest} className={[\"e\", className].filter(Boolean).join(\" \")} style={style} />)(((__devupProps) => ({\n\t...__devupProps,\n\t...typeof extra === \"function\" ? extra(__devupProps) : extra\n}))(((__devupProps) => ({\n\t...__devupProps,\n\t...((props) => ({ size: props.small ? 5 : 10 }))(__devupProps)\n}))(__devupProps)));\nexport const G = other.div.attrs({ role: \"note\" })({ margin: 1 });\nexport const I = factory.attrs({ id: \"i\" })({ margin: 3 });\nexport const J = other(Base).attrs({ id: \"j\" })({ margin: 4 });\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nexport const A = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const B = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const C = (__devupProps) => (({ style, className, as: DevupAs = \"button\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ type: \"button\" }\n});\nexport const D = ({ style, className, as: DevupAs = Base, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />;\nexport const E = (__devupProps) => (({ style, className, as: DevupAs = \"input\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"small\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"e\", className].filter(Boolean).join(\" \")} style={style} />)(((__devupContext) => ({\n\t...__devupContext,\n\tclassName: [__devupContext.className, __devupProps.className].filter(Boolean).join(\" \") || undefined\n}))(((__devupContext) => ((__devupAttrs) => ({\n\t...__devupContext,\n\t...__devupAttrs,\n\tclassName: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(\" \") || undefined,\n\tstyle: {\n\t\t...__devupContext.style,\n\t\t...__devupAttrs.style\n\t}\n}))({\n\t...{},\n\t...typeof extra === \"function\" ? extra(__devupContext) : extra\n}))(((__devupContext) => ((__devupAttrs) => ({\n\t...__devupContext,\n\t...__devupAttrs,\n\tclassName: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(\" \") || undefined,\n\tstyle: {\n\t\t...__devupContext.style,\n\t\t...__devupAttrs.style\n\t}\n}))({\n\t...{},\n\t...((props) => ({ size: props.small ? 5 : 10 }))(__devupContext)\n}))({\n\t...__devupProps,\n\tclassName: undefined\n}))));\nexport const G = other.div.attrs({ role: \"note\" })({ margin: 1 });\nexport const I = factory.attrs({ id: \"i\" })({ margin: 3 });\nexport const J = other(Base).attrs({ id: \"j\" })({ margin: 4 });\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_components_import_surface_fully_redirects.snap b/libs/extractor/src/snapshots/extractor__tests__styled_components_import_surface_fully_redirects.snap index 3d908e9f2..fd53f0c2c 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_components_import_surface_fully_redirects.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_components_import_surface_fully_redirects.snap @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { ThemeProvider, useTheme, withTheme, ServerStyleSheet, StyleSheetManager, isStyledComponent } from \"@devup-ui/react/compat\";\nconst S = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nimport { ThemeProvider, useTheme, withTheme, ServerStyleSheet, StyleSheetManager, isStyledComponent } from \"@devup-ui/react/compat\";\nconst S = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_components_of_any_base.snap b/libs/extractor/src/snapshots/extractor__tests__styled_components_of_any_base.snap index eaa910573..6b88aa070 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_components_of_any_base.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_components_of_any_base.snap @@ -5,22 +5,22 @@ expression: outputs [ ( "export const A = styled(motion.div)`color: red;`;", - "export const A = ({ style, className, ...rest }) => <motion.div {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;", + "export const A = ({ style, className, as: DevupAs = motion.div, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;", ), ( "export const B = styled((Motion as any).Div.Inner)({ color: 'red' });", - "export const B = ({ style, className, ...rest }) => <Motion.Div.Inner {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;", + "export const B = ({ style, className, as: DevupAs = (Motion as any).Div.Inner, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;", ), ( "export const C = styled(forwardRef((p, r) => <div ref={r} {...p} />)).attrs({ role: 'x' })`color: red;`;", - "export const C = ((DevupStyled) => (__devupProps) => (({ style, className, ...rest }) => <DevupStyled {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"x\" }\n}))(forwardRef((p, r) => <div ref={r} {...p} />));", + "export const C = ((DevupStyled) => (__devupProps) => (({ style, className, as: DevupAs = DevupStyled, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)({\n\t...__devupProps,\n\t...{ role: \"x\" }\n}))(forwardRef((p, r) => <div ref={r} {...p} />));", ), ( "export const D = (tag) => styled(tag, { color: 'red' });", - "export const D = (tag) => ((DevupStyled) => ({ style, className, ...rest }) => <DevupStyled {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)(tag);", + "export const D = (tag) => ((DevupStyled) => ({ style, className, as: DevupAs = DevupStyled, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)(tag);", ), ( "export const E = styled(getThing().div)`color: red;`;", - "export const E = ((DevupStyled) => ({ style, className, ...rest }) => <DevupStyled {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)(getThing().div);", + "export const E = ((DevupStyled) => ({ style, className, as: DevupAs = DevupStyled, forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)(getThing().div);", ), ] diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_components_theme_resolves_to_css_variables.snap b/libs/extractor/src/snapshots/extractor__tests__styled_components_theme_resolves_to_css_variables.snap index 25e59c338..1d9deb6c4 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_components_theme_resolves_to_css_variables.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_components_theme_resolves_to_css_variables.snap @@ -91,5 +91,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst Flat = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nconst Nested = ({ style, className, ...rest }) => <span {...rest} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nconst Destructured = ({ style, className, ...rest }) => <p {...rest} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />;\nconst Surrounded = ({ style, className, ...rest }) => <b {...rest} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />;\nconst NotTheme = ({ style, className, ...rest }) => <i {...rest} className={[\"e\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--f\": (p=>p.color)(rest) },\n\t...style\n}} />;\nconst OtherRoot = ({ style, className, ...rest }) => <s {...rest} className={[\"e\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--f\": (p=>q.theme.brand)(rest) },\n\t...style\n}} />;\nconst ArrayParam = ({ style, className, ...rest }) => <q {...rest} className={[\"e\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--f\": (([p])=>p.theme.brand)(rest) },\n\t...style\n}} />;\nconst NoParam = ({ style, className, ...rest }) => <em {...rest} className={[\"e\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--f\": (()=>`red`)(rest) },\n\t...style\n}} />;\nconst Indexed = ({ style, className, ...rest }) => <u {...rest} className={[\"g\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst Flat = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nconst Nested = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nconst Destructured = ({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />;\nconst Surrounded = ({ style, className, as: DevupAs = \"b\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />;\nconst NotTheme = ({ style, className, as: DevupAs = \"i\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"e\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--f\": (p=>p.color)(rest) },\n\t...style\n}} />;\nconst OtherRoot = ({ style, className, as: DevupAs = \"s\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"e\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--f\": (p=>q.theme.brand)(rest) },\n\t...style\n}} />;\nconst ArrayParam = ({ style, className, as: DevupAs = \"q\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"e\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--f\": (([p])=>p.theme.brand)(rest) },\n\t...style\n}} />;\nconst NoParam = ({ style, className, as: DevupAs = \"em\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"e\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--f\": (()=>`red`)(rest) },\n\t...style\n}} />;\nconst Indexed = ({ style, className, as: DevupAs = \"u\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"g\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_object_form_theme_interpolation.snap b/libs/extractor/src/snapshots/extractor__tests__styled_object_form_theme_interpolation.snap index aac4951f0..7e4b2fb37 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_object_form_theme_interpolation.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_object_form_theme_interpolation.snap @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst Themed = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst Themed = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_typescript_wrappers.snap b/libs/extractor/src/snapshots/extractor__tests__styled_typescript_wrappers.snap index c8bd504f4..6a61617db 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_typescript_wrappers.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_typescript_wrappers.snap @@ -75,5 +75,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui.css\";\nconst A = ({ style, className, ...rest }) => <div {...rest} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nconst B = ({ style, className, ...rest }) => <span {...rest} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nconst C = ({ style, className, ...rest }) => <p {...rest} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />;\nconst D = ({ style, className, ...rest }) => <div {...rest} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />;\nconst E = ({ style, className, ...rest }) => <span {...rest} className={[\"e\", className].filter(Boolean).join(\" \")} style={style} />;\nconst F = ({ style, className, ...rest }) => <p {...rest} className={[\"f\", className].filter(Boolean).join(\" \")} style={style} />;\nconst G = ({ style, className, ...rest }) => <div {...rest} className={[\"g\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui.css\";\nconst A = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a\", className].filter(Boolean).join(\" \")} style={style} />;\nconst B = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"b\", className].filter(Boolean).join(\" \")} style={style} />;\nconst C = ({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"c\", className].filter(Boolean).join(\" \")} style={style} />;\nconst D = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"d\", className].filter(Boolean).join(\" \")} style={style} />;\nconst E = ({ style, className, as: DevupAs = \"span\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"e\", className].filter(Boolean).join(\" \")} style={style} />;\nconst F = ({ style, className, as: DevupAs = \"p\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"f\", className].filter(Boolean).join(\" \")} style={style} />;\nconst G = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"g\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_spread.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_spread.snap index ee23535b0..7dee6e5f8 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_spread.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_spread.snap @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst baseStyles = { bg: \"red\" };\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst baseStyles = { bg: \"red\" };\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-2.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-2.snap index 689d3538c..16a126b16 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-2.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"div\")({ bg: \"$text\", fontSize: 16 })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"div\")({ bg: \"$text\", fontSize: 16 })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-3.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-3.snap index 9942d2b3e..33b6d0c7d 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-3.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.div`\n background: var(--text);\n color: var(--primary);\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.div`\n background: var(--text);\n color: var(--primary);\n `\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-4.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-4.snap index 198742bd3..88108f9c9 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-4.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-4.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled, Box} from '@devup-ui/core'\n const StyledComponent = styled(Box)({ bg: \"$text\", _hover: { bg: \"$primary\" } })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled, Box} from '@devup-ui/core'\n const StyledComponent = styled(Box)({ bg: \"$text\", _hover: { bg: \"$primary\" } })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -29,5 +29,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-5.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-5.snap index ffcc930d7..5f6f80166 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-5.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable-5.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"div\")`\n background-color: var(--text);\n padding: 16px;\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"div\")`\n background-color: var(--text);\n padding: 16px;\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable.snap index bacac4a77..7ec1c1c08 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.div({ bg: \"$text\", color: \"$primary\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.div({ bg: \"$text\", color: \"$primary\" })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-2.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-2.snap index 26b8ec17d..10d1cd3d9 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-2.snap @@ -23,5 +23,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet primaryColor = \"blue\";\nlet padding = \"16px\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": padding,\n\t\t\"--b\": primaryColor\n\t},\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet primaryColor = \"blue\";\nlet padding = \"16px\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": padding,\n\t\t\"--b\": primaryColor\n\t},\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-3.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-3.snap index 220164c96..f8501a92c 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-3.snap @@ -23,5 +23,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet primaryColor = \"blue\";\nlet padding = \"16px\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": primaryColor,\n\t\t\"--b\": padding\n\t},\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet primaryColor = \"blue\";\nlet padding = \"16px\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": primaryColor,\n\t\t\"--b\": padding\n\t},\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-4.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-4.snap index 7ef7dacc6..b0164f555 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-4.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-4.snap @@ -23,5 +23,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet primaryColor = \"blue\";\nlet padding = \"16px\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": primaryColor,\n\t\t\"--b\": padding\n\t},\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet primaryColor = \"blue\";\nlet padding = \"16px\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": primaryColor,\n\t\t\"--b\": padding\n\t},\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-5.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-5.snap index 9c93db528..80ba6c524 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-5.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-5.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"div\")`\n color: ${obj.color};\n padding: ${func()};\n background: ${obj.func()};\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"div\")`\n color: ${obj.color};\n padding: ${func()};\n background: ${obj.func()};\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -32,5 +32,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b a-c\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": func(),\n\t\t\"--b\": obj.color,\n\t\t\"--c\": obj.func()\n\t},\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b a-c\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": func(),\n\t\t\"--b\": obj.color,\n\t\t\"--c\": obj.func()\n\t},\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-6.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-6.snap index 5cbac3d66..0c04d7749 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-6.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-6.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"div\")({ bg: obj.bg, padding: func(), color: obj.color() })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"div\")({ bg: obj.bg, padding: func(), color: obj.color() })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -32,5 +32,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b a-c\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": obj.bg,\n\t\t\"--b\": func(),\n\t\t\"--c\": obj.color()\n\t},\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b a-c\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": obj.bg,\n\t\t\"--b\": func(),\n\t\t\"--c\": obj.color()\n\t},\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-7.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-7.snap index 1b1c4ab7a..51c4d525d 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-7.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion-7.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.div({ bg: obj.bg, padding: func(), color: obj.color() })\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.div({ bg: obj.bg, padding: func(), color: obj.color() })\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -32,5 +32,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b a-c\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": obj.bg,\n\t\t\"--b\": func(),\n\t\t\"--c\": obj.color()\n\t},\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b a-c\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": obj.bg,\n\t\t\"--b\": func(),\n\t\t\"--c\": obj.color()\n\t},\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion.snap index d787fa7e6..2db8da3da 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion.snap @@ -24,5 +24,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet color = \"red\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--a\": color },\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet color = \"red\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--a\": color },\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-2.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-2.snap index e2f71ae84..6e78f2cab 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-2.snap @@ -23,5 +23,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet fontSize = \"18px\";\nconst StyledComponent = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": fontSize,\n\t\t\"--b\": (props=>props.color||`black`)(rest)\n\t},\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet fontSize = \"18px\";\nconst StyledComponent = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": fontSize,\n\t\t\"--b\": (props=>props.color||`black`)(rest)\n\t},\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-3.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-3.snap index 1592b811c..a7536c8b6 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-3.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-3.snap @@ -32,5 +32,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet margin = \"10px\";\nlet padding = \"20px\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b a-c\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": padding,\n\t\t\"--b\": margin,\n\t\t\"--c\": (props=>props.bg||`white`)(rest)\n\t},\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nlet margin = \"10px\";\nlet padding = \"20px\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"bg\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b a-c\", className].filter(Boolean).join(\" \")} style={{\n\t...{\n\t\t\"--a\": padding,\n\t\t\"--b\": margin,\n\t\t\"--c\": (props=>props.bg||`white`)(rest)\n\t},\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-4.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-4.snap index efc24a0d5..1e3890291 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-4.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props-4.snap @@ -25,5 +25,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst isActive = true;\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst isActive = true;\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props.snap b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props.snap index ba146ded7..b98038f37 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styled_with_variable_like_emotion_props.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.div`\n background: ${props => props.bg};\n color: red;\n `\n \"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled.div`\n background: ${props => props.bg};\n color: red;\n `\n \",\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -24,5 +24,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--a\": (props=>props.bg)(rest) },\n\t...style\n}} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"bg\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--a\": (props=>props.bg)(rest) },\n\t...style\n}} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__styles_the_build_cannot_read.snap b/libs/extractor/src/snapshots/extractor__tests__styles_the_build_cannot_read.snap index 672277716..5d3858260 100644 --- a/libs/extractor/src/snapshots/extractor__tests__styles_the_build_cannot_read.snap +++ b/libs/extractor/src/snapshots/extractor__tests__styles_the_build_cannot_read.snap @@ -99,5 +99,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nexport const a = (base) => base;\nexport const b = (on) => on ? \"a-a\" : \"\";\nexport const c = \"a-b a-c\";\nexport const d = (on, base) => `${on ? base : \"\"} ${on ? \"\" : \"a-d\"}`;\nexport const e = \"a-e\";\nexport const f = (rest, handler, k) => <div {...rest} {...{\n\tonClick: handler,\n\t[k]: 1\n}} className={`a-e ${{\n\tonClick: handler,\n\t[k]: 1\n}?.className || \"\"} ${rest?.className || \"\"}`} style={{\n\t...{\n\t\tonClick: handler,\n\t\t[k]: 1\n\t}?.style,\n\t...rest?.style\n}} />;\nexport const g = (theme, key) => <div className=\"a-f a-g\" style={{\n\t\"--a\": theme.colors[key],\n\t\"--b\": ({a:`red`,...theme.more})[`b`]\n}} />;\nexport const h = (Base) => ({ style, className, ...rest }) => <div {...rest} className={[Base, className].filter(Boolean).join(\" \")} style={style} />;\nexport const i = \"a-h\";\nexport const j = \"a-i\";\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nexport const a = (base) => base;\nexport const b = (on) => on ? \"a-a\" : \"\";\nexport const c = \"a-b a-c\";\nexport const d = (on, base) => `${on ? base : \"\"} ${on ? \"\" : \"a-d\"}`;\nexport const e = \"a-e\";\nexport const f = (rest, handler, k) => <div {...rest} {...{\n\tonClick: handler,\n\t[k]: 1\n}} className={`${(\"className\" in Object({\n\tonClick: handler,\n\t[k]: 1\n}) ? {\n\tonClick: handler,\n\t[k]: 1\n}.className : rest?.className) || \"\"} a-e`} style={\"style\" in Object({\n\tonClick: handler,\n\t[k]: 1\n}) ? {\n\tonClick: handler,\n\t[k]: 1\n}.style : rest?.style} />;\nexport const g = (theme, key) => <div className=\"a-f a-g\" style={{\n\t\"--a\": theme.colors[key],\n\t\"--b\": ({a:`red`,...theme.more})[`b`]\n}} />;\nexport const h = (Base) => ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[Base, className].filter(Boolean).join(\" \")} style={style} />;\nexport const i = \"a-h\";\nexport const j = \"a-i\";\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__support_transpile_mjs-5.snap b/libs/extractor/src/snapshots/extractor__tests__support_transpile_mjs-5.snap index d09fc0b10..de1dfab55 100644 --- a/libs/extractor/src/snapshots/extractor__tests__support_transpile_mjs-5.snap +++ b/libs/extractor/src/snapshots/extractor__tests__support_transpile_mjs-5.snap @@ -1,6 +1,6 @@ --- source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.js\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/core\";\ne(o, { className: \"a\", bg: variable, style: { color: \"blue\" }, ...props })\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false\n}).unwrap())" +expression: "ToBTreeSet::from(extract(\"test.js\",\nr#\"import { jsx as e } from \"react/jsx-runtime\";\nimport { Box as o } from \"@devup-ui/core\";\ne(o, { className: \"a\", bg: variable, style: { color: \"blue\" }, ...props })\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())" --- ToBTreeSet { styles: { @@ -14,5 +14,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\ne(\"div\", {\n\t...props,\n\tclassName: \"a a\",\n\tstyle: {\n\t\t...{ \"--b\": variable },\n\t\t...{ color: \"blue\" }\n\t}\n});\n", + code: "import \"@devup-ui/core/devup-ui.css\";\nimport { jsx as e } from \"react/jsx-runtime\";\ne(\"div\", {\n\t...props,\n\tclassName: `${(\"className\" in Object(props) ? props.className : \"a\") || \"\"} a`,\n\tstyle: {\n\t\t...{ \"--b\": variable },\n\t\t...\"style\" in Object(props) ? props.style : { color: \"blue\" }\n\t}\n});\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap b/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap index 2594e53fe..5b3972c5c 100644 --- a/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap +++ b/libs/extractor/src/snapshots/extractor__tests__template_interpolations.snap @@ -97,5 +97,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/react/devup-ui-0.css\";\nconst C = \"red\";\nconst O = .5;\nconst SEL = \".x\";\nconst SNIPPET = \"display: flex;\";\nconst mixin = \"a-a\";\n;\n;\nexport const k = \"a-b\";\nexport const c = \"a-c a-d\";\nexport const d = `a-e ${mixin}`;\nexport const A = ({ style, className, ...rest }) => <div {...rest} className={[`a-d ${mixin} ${((p) => p.active && mixin)(rest) || \"\"}`, className].filter(Boolean).join(\" \")} style={style} />;\nexport const B = ({ style, className, ...rest }) => <div {...rest} className={[\"a-f\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/react/devup-ui-0.css\";\nconst C = \"red\";\nconst O = .5;\nconst SEL = \".x\";\nconst SNIPPET = \"display: flex;\";\nconst mixin = \"a-a\";\n;\n;\nexport const k = \"a-b\";\nexport const c = \"a-c a-d\";\nexport const d = `a-e ${mixin}`;\nexport const A = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"active\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[`a-d ${mixin} ${((p) => p.active && mixin)(rest) || \"\"}`, className].filter(Boolean).join(\" \")} style={style} />;\nexport const B = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-f\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__wrong_styled_with_variable_like_emotion_props-2.snap b/libs/extractor/src/snapshots/extractor__tests__wrong_styled_with_variable_like_emotion_props-2.snap index 6cfabe47b..ac93daa9a 100644 --- a/libs/extractor/src/snapshots/extractor__tests__wrong_styled_with_variable_like_emotion_props-2.snap +++ b/libs/extractor/src/snapshots/extractor__tests__wrong_styled_with_variable_like_emotion_props-2.snap @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = (({ style, className, ...rest }) => <div {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)``;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = (({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />)``;\n", } diff --git a/libs/extractor/src/snapshots/extractor__tests__wrong_styled_with_variable_like_emotion_props.snap b/libs/extractor/src/snapshots/extractor__tests__wrong_styled_with_variable_like_emotion_props.snap index d5b1fe1eb..835b2952d 100644 --- a/libs/extractor/src/snapshots/extractor__tests__wrong_styled_with_variable_like_emotion_props.snap +++ b/libs/extractor/src/snapshots/extractor__tests__wrong_styled_with_variable_like_emotion_props.snap @@ -15,5 +15,5 @@ ToBTreeSet { }, ), }, - code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) => <div {...rest} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, as: DevupAs = \"div\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a\", className].filter(Boolean).join(\" \")} style={style} />;\n", } diff --git a/libs/extractor/src/style_values.rs b/libs/extractor/src/style_values.rs index eebc48a63..440b99451 100644 --- a/libs/extractor/src/style_values.rs +++ b/libs/extractor/src/style_values.rs @@ -11,11 +11,15 @@ use oxc_span::SPAN; use oxc_syntax::symbol::SymbolId; use rustc_hash::FxHashMap; +use crate::ExtractStyleValue; + /// The name a style API call gives pub enum StyleValue { /// A class `css()` gives: in CSS text it is a mixin, composed rather than - /// written as text, so it is read only outside CSS text - Class(String), + /// written as text, so it is read only outside CSS text. The styles behind + /// it, when the build knows them, let a later style composed with it + /// replace its declarations. + Class(String, Option<Vec<ExtractStyleValue>>), /// A name `keyframes()` gives Keyframes(String), } @@ -24,6 +28,8 @@ pub enum StyleValue { pub struct StyleValues { scoping: Option<Scoping>, values: FxHashMap<SymbolId, StyleValue>, + /// The styles behind `css()` classes the file imports, by binding + imported: FxHashMap<String, Vec<ExtractStyleValue>>, /// The `const`s the file binds to a string, which a `className` reads as /// the classes they hold texts: FxHashMap<SymbolId, String>, @@ -34,10 +40,15 @@ impl StyleValues { Self { scoping: Some(scoping), values: FxHashMap::default(), + imported: FxHashMap::default(), texts: FxHashMap::default(), } } + pub fn import(&mut self, imported: FxHashMap<String, Vec<ExtractStyleValue>>) { + self.imported = imported; + } + /// The `const` `id` binds, whose value no code changes pub fn constant(&self, id: &BindingPattern<'_>) -> Option<SymbolId> { let symbol = id.get_binding_identifier()?.symbol_id.get()?; @@ -52,6 +63,51 @@ impl StyleValues { self.values.insert(symbol, value); } + /// The binding `expression` reads, when it reads one + pub fn symbol(&self, expression: &Expression<'_>) -> Option<SymbolId> { + let Expression::Identifier(identifier) = expression else { + return None; + }; + self.scoping + .as_ref()? + .get_reference(identifier.reference_id.get()?) + .symbol_id() + } + + /// The binding the JSX element name `identifier` reads + pub fn reference_symbol( + &self, + identifier: &oxc_ast::ast::IdentifierReference<'_>, + ) -> Option<SymbolId> { + self.scoping + .as_ref()? + .get_reference(identifier.reference_id.get()?) + .symbol_id() + } + + /// The styles behind the `css()` class `expression` reads + pub fn styles(&self, expression: &Expression<'_>) -> Option<&[ExtractStyleValue]> { + let Expression::Identifier(identifier) = expression else { + return None; + }; + let symbol = self + .scoping + .as_ref()? + .get_reference(identifier.reference_id.get()?) + .symbol_id()?; + let scoping = self.scoping.as_ref()?; + match self.values.get(&symbol) { + Some(StyleValue::Class(_, Some(styles))) => Some(styles), + Some(_) => None, + None => scoping + .symbol_flags(symbol) + .is_import() + .then(|| self.imported.get(scoping.symbol_name(symbol))) + .flatten() + .map(Vec::as_slice), + } + } + pub fn insert_text(&mut self, symbol: SymbolId, text: String) { self.texts.insert(symbol, text); } @@ -171,8 +227,8 @@ impl<'a> VisitMut<'a> for Reads<'_, 'a> { .and_then(|symbol| self.values.get(&symbol)) { let value = match value { - StyleValue::Class(_) if self.in_text => return, - StyleValue::Class(value) | StyleValue::Keyframes(value) => value, + StyleValue::Class(..) if self.in_text => return, + StyleValue::Class(value, _) | StyleValue::Keyframes(value) => value, }; *it = Expression::new_string_literal( SPAN, diff --git a/libs/extractor/src/styled_reads.rs b/libs/extractor/src/styled_reads.rs new file mode 100644 index 000000000..2f8033dfd --- /dev/null +++ b/libs/extractor/src/styled_reads.rs @@ -0,0 +1,323 @@ +//! The props a styled component's style functions and attrs read: the +//! component keeps those away from the tag it renders unless the tag takes +//! them as attributes, and an element using the component keeps passing them + +use oxc_ast::ast::{ + ArrowFunctionExpression, BindingPattern, Expression, FormalParameters, Function, + IdentifierReference, ObjectPropertyKind, StaticMemberExpression, +}; +use oxc_ast_visit::{Visit, walk}; + +use crate::utils::unwrap_syntax_only; + +/// The props read by name, and whether some function reads the props whole, +/// so which props it needs only the runtime tells +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub struct Reads { + pub names: Vec<String>, + pub whole: bool, +} + +impl Reads { + fn push(&mut self, name: &str) { + if !self.names.iter().any(|existing| existing == name) { + self.names.push(name.to_string()); + } + } + + /// The functions `expression` holds where a style or attrs reads props: + /// the expression itself, or a value of a rule object + pub fn read_in(&mut self, expression: &Expression<'_>) { + match unwrap_syntax_only(expression) { + Expression::ArrowFunctionExpression(arrow) => self.function(&arrow.params, |v| { + v.visit_arrow_function_body(arrow); + }), + Expression::FunctionExpression(function) => self.function(&function.params, |v| { + v.visit_function_body_of(function); + }), + Expression::ObjectExpression(object) => { + for property in &object.properties { + match property { + ObjectPropertyKind::ObjectProperty(property) => { + self.read_in(&property.value); + } + ObjectPropertyKind::SpreadProperty(spread) => { + self.read_in(&spread.argument); + } + } + } + } + Expression::ArrayExpression(array) => { + for element in &array.elements { + if let Some(element) = element.as_expression() { + self.read_in(element); + } + } + } + _ => {} + } + } + + fn function(&mut self, params: &FormalParameters<'_>, body: impl FnOnce(&mut PropsUse<'_>)) { + let Some(first) = params.items.first() else { + return; + }; + match &first.pattern { + BindingPattern::BindingIdentifier(identifier) => { + let mut uses = PropsUse { + name: identifier.name.as_str(), + reads: self, + }; + body(&mut uses); + } + BindingPattern::ObjectPattern(object) => { + for property in &object.properties { + match property.key.static_name() { + Some(name) if !property.computed => self.push(&name), + _ => self.whole = true, + } + } + if object.rest.is_some() { + self.whole = true; + } + } + _ => self.whole = true, + } + } +} + +struct PropsUse<'r> { + name: &'r str, + reads: &'r mut Reads, +} + +impl PropsUse<'_> { + fn visit_arrow_function_body(&mut self, arrow: &ArrowFunctionExpression<'_>) { + match &arrow.body { + oxc_ast::ast::ArrowFunctionBody::FunctionBody(body) => self.visit_function_body(body), + body => { + if let Some(expression) = body.as_expression() { + self.visit_expression(expression); + } + } + } + } + + fn visit_function_body_of(&mut self, function: &Function<'_>) { + if let Some(body) = &function.body { + self.visit_function_body(body); + } + } +} + +impl<'a> Visit<'a> for PropsUse<'_> { + fn visit_static_member_expression(&mut self, it: &StaticMemberExpression<'a>) { + if let Expression::Identifier(object) = &it.object + && object.name == self.name + { + self.reads.push(&it.property.name); + return; + } + walk::walk_static_member_expression(self, it); + } + + fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) { + if it.name == self.name { + self.reads.whole = true; + } + } +} + +/// What `shouldForwardProp` decides for a prop name, as the build evaluates it +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Forward { + Always(bool), + Is(String), + In(Vec<String>), + StartsWith(String), + /// A prop React passes to an element, as `isPropValid` or styled-components' + /// default validator tells + Valid, + Not(Box<Forward>), + And(Box<Forward>, Box<Forward>), + Or(Box<Forward>, Box<Forward>), +} + +impl Forward { + /// The prop names the rule compares with by name + fn names(&self, names: &mut Vec<String>) { + match self { + Forward::Is(prop) => names.push(prop.clone()), + Forward::In(props) => names.extend(props.iter().cloned()), + Forward::Not(rule) => rule.names(names), + Forward::And(left, right) | Forward::Or(left, right) => { + left.names(names); + right.names(names); + } + Forward::Always(_) | Forward::StartsWith(_) | Forward::Valid => {} + } + } + + #[must_use] + pub fn forwards(&self, name: &str) -> bool { + match self { + Forward::Always(value) => *value, + Forward::Is(prop) => prop == name, + Forward::In(props) => props.iter().any(|prop| prop == name), + Forward::StartsWith(prefix) => name.starts_with(prefix.as_str()), + Forward::Valid => crate::prop_valid::is_prop_valid(name), + Forward::Not(rule) => !rule.forwards(name), + Forward::And(left, right) => left.forwards(name) && right.forwards(name), + Forward::Or(left, right) => left.forwards(name) || right.forwards(name), + } + } + + /// `shouldForwardProp` as the build evaluates it: a function of the prop + /// name comparing it with strings, `includes`, `startsWith`, `isPropValid` + /// or the validator styled-components passes, joined by `!`, `&&` and `||` + #[must_use] + pub fn read(expression: &Expression<'_>) -> Option<Self> { + let (params, body) = match unwrap_syntax_only(expression) { + Expression::ArrowFunctionExpression(arrow) => ( + &arrow.params, + match &arrow.body { + oxc_ast::ast::ArrowFunctionBody::FunctionBody(body) => single_expression(body)?, + body => body.as_expression()?, + }, + ), + Expression::FunctionExpression(function) => ( + &function.params, + single_expression(function.body.as_ref()?)?, + ), + _ => return None, + }; + let name = |index: usize| match params.items.get(index).map(|p| &p.pattern) { + Some(BindingPattern::BindingIdentifier(identifier)) => Some(identifier.name.as_str()), + _ => None, + }; + // Without a parameter no identifier reads the prop, so only literals decide + rule(body, name(0).unwrap_or_default(), name(1)) + } +} + +fn single_expression<'b, 'a>( + body: &'b oxc_ast::ast::FunctionBody<'a>, +) -> Option<&'b Expression<'a>> { + match body.statements.as_slice() { + [oxc_ast::ast::Statement::ReturnStatement(statement)] => statement.argument.as_ref(), + _ => None, + } +} + +fn rule(expression: &Expression<'_>, prop: &str, validator: Option<&str>) -> Option<Forward> { + use oxc_ast::ast::{BinaryOperator, LogicalOperator, UnaryOperator}; + let is_prop = |e: &Expression<'_>| matches!(unwrap_syntax_only(e), Expression::Identifier(i) if i.name == prop); + let string = |e: &Expression<'_>| match unwrap_syntax_only(e) { + Expression::StringLiteral(literal) => Some(literal.value.to_string()), + _ => None, + }; + Some(match unwrap_syntax_only(expression) { + Expression::BooleanLiteral(literal) => Forward::Always(literal.value), + Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::LogicalNot => { + Forward::Not(Box::new(rule(&unary.argument, prop, validator)?)) + } + Expression::LogicalExpression(logical) => { + let left = Box::new(rule(&logical.left, prop, validator)?); + let right = Box::new(rule(&logical.right, prop, validator)?); + match logical.operator { + LogicalOperator::And => Forward::And(left, right), + LogicalOperator::Or => Forward::Or(left, right), + LogicalOperator::Coalesce => return None, + } + } + Expression::BinaryExpression(binary) => { + let value = if is_prop(&binary.left) { + string(&binary.right)? + } else if is_prop(&binary.right) { + string(&binary.left)? + } else { + return None; + }; + match binary.operator { + BinaryOperator::StrictEquality | BinaryOperator::Equality => Forward::Is(value), + BinaryOperator::StrictInequality | BinaryOperator::Inequality => { + Forward::Not(Box::new(Forward::Is(value))) + } + _ => return None, + } + } + Expression::CallExpression(call) => { + let [argument] = call.arguments.as_slice() else { + return None; + }; + let argument = argument.as_expression()?; + match unwrap_syntax_only(&call.callee) { + Expression::Identifier(callee) + if is_prop(argument) + && (Some(callee.name.as_str()) == validator + || callee.name == "isPropValid") => + { + Forward::Valid + } + Expression::StaticMemberExpression(member) + if member.property.name == "startsWith" && is_prop(&member.object) => + { + Forward::StartsWith(string(argument)?) + } + Expression::StaticMemberExpression(member) + if member.property.name == "includes" && is_prop(argument) => + { + let Expression::ArrayExpression(array) = unwrap_syntax_only(&member.object) + else { + return None; + }; + Forward::In( + array + .elements + .iter() + .map(|element| string(element.as_expression()?)) + .collect::<Option<_>>()?, + ) + } + _ => return None, + } + } + _ => return None, + }) +} + +/// Whether a styled component passes the prop `name` on to what it renders: +/// a `$` prop never, otherwise as `forward` decides, or by default a tag only +/// what it takes as attributes and never `theme`, a component every prop +pub fn passes(name: &str, renders_tag: bool, forward: Option<&Forward>) -> bool { + if name.starts_with('$') { + return false; + } + match forward { + Some(forward) => forward.forwards(name), + None if renders_tag => name != "theme" && crate::prop_valid::is_prop_valid(name), + None => true, + } +} + +/// The props a styled component reads and keeps away from what it renders, +/// and `theme` when a tag does not take it +pub fn withheld(reads: &Reads, renders_tag: bool, forward: Option<&Forward>) -> Vec<String> { + let mut named = reads.names.clone(); + if let Some(forward) = forward { + forward.names(&mut named); + } + let mut withheld: Vec<String> = Vec::new(); + for name in named { + if !passes(&name, renders_tag, forward) && !withheld.contains(&name) { + withheld.push(name); + } + } + if renders_tag + && !passes("theme", true, forward) + && !withheld.iter().any(|name| name == "theme") + { + withheld.push("theme".to_string()); + } + withheld +} diff --git a/libs/extractor/src/styled_source_tests/compatibility.rs b/libs/extractor/src/styled_source_tests/compatibility.rs index bf91f7296..6b18b368c 100644 --- a/libs/extractor/src/styled_source_tests/compatibility.rs +++ b/libs/extractor/src/styled_source_tests/compatibility.rs @@ -1,6 +1,19 @@ use super::*; use oxc_codegen::Codegen; +#[test] +#[serial] +fn attrs_input_remains_when_filtered_prepared_component_retargets_then_extends() { + // Given: inherited captures and attrs needing a prop the render filter withholds. + let source = format!( + "{IMPORT}const Base=styled.div.attrs(p=>({{'data-tone':p.tone}})).withConfig({{shouldForwardProp:p=>p!=='tone'&&p!=='blocked'}})({{color:'red',...(on?{{color:'blue'}}:{{}})}});const Mid=Base.withComponent('section');const Card=styled(Mid)({{p:1}});const element=<Card tone='olive' blocked='drop' ref={{callerRef}} children='child' as='article' forwardedAs='nav' />;" + ); + // When: JSX extraction filters explicit caller attributes. + let output = compile("test.tsx", &source, true); + // Then: the attrs input survives while the unread denied prop is removed, with React/target props intact. + assert!(output.contains("const element = <Card tone=\"olive\" ref={callerRef} children=\"child\" as=\"article\" forwardedAs=\"nav\" />;"), "{output}"); +} + #[rstest] #[case("tsx")] #[case("jsx")] @@ -9,13 +22,13 @@ fn output_is_byte_identical_when_source_accepts_jsx( #[case] extension: &str, #[values(true, false)] single_css: bool, ) { - // Given: baseline bytes captured with the installed main WASM, not a new snapshot. + // Given: the JSX baseline with polymorphic rendering from #710. let filename = format!("test.{extension}"); let source = "import { styled } from '@devup-ui/react';\nexport const Card = styled('article', { p: 2, color: 'olive' });"; let expected = if single_css { - "import \"@devup-ui/react/devup-ui.css\";\nexport const Card = ({ style, className, ...rest }) => <article {...rest} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n" + "import \"@devup-ui/react/devup-ui.css\";\nexport const Card = ({ style, className, as: DevupAs = \"article\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a b\", className].filter(Boolean).join(\" \")} style={style} />;\n" } else { - "import \"@devup-ui/react/devup-ui-0.css\";\nexport const Card = ({ style, className, ...rest }) => <article {...rest} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n" + "import \"@devup-ui/react/devup-ui-0.css\";\nexport const Card = ({ style, className, as: DevupAs = \"article\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"a-a a-b\", className].filter(Boolean).join(\" \")} style={style} />;\n" }; // When: the real extractor compiles the control. let output = compile(&filename, source, single_css); diff --git a/libs/extractor/src/styled_source_tests/execution.rs b/libs/extractor/src/styled_source_tests/execution.rs index 323a76c96..0f5d46ff7 100644 --- a/libs/extractor/src/styled_source_tests/execution.rs +++ b/libs/extractor/src/styled_source_tests/execution.rs @@ -2,6 +2,76 @@ use super::*; use boa_engine::{Context, Source}; use oxc_codegen::Codegen; +#[rstest] +#[case("Link")] +#[case("motion.div")] +#[serial] +fn actual_target_and_captures_survive_when_typescript_retargets_then_extends(#[case] target: &str) { + // Given: a prepared base, retained attrs, a retarget and a further extension. + let source = format!( + "{IMPORT}const trace=[];const Link=props=>props;const Override=props=>props;const motion={{div:Link}};const on=true;const Base=styled.div.attrs({{id:'retargeted'}})({{color:'red',...((trace.push('base'),on)?{{color:'blue'}}:{{}})}});const Mid=Base.withComponent({target});const Card=styled(Mid)({{p:1}});" + ); + let output = compile("test.ts", &source, true); + let arena = Allocator::default(); + let mut program = parse(&arena, "test.ts", &output); + let calls = renders(&program); + assert_eq!(calls.len(), 3); + let helper = &calls[0].helper; + program + .body + .retain(|statement| !matches!(statement, Statement::ImportDeclaration(_))); + let code = Codegen::new().build(&program).code; + // When: the emitted extension renders its default and an actual component override. + let script = format!( + "const {helper}=(target,props)=>({{target,props}});{code}\nconst first=Card({{className:'caller',forwardedAs:'nav'}});const second=Card({{as:Override,forwardedAs:'aside'}});JSON.stringify([first.target===Link,second.target===Override,first.props.as,second.props.as,first.props.id,second.props.id,first.props.className.includes('caller'),trace]);" + ); + let mut context = Context::default(); + let result = context.eval(Source::from_bytes(&script)); + assert!(result.is_ok(), "{result:?}"); + // Then: identity, forwarded props, attrs and single ordered captures survive retargeting. + assert_eq!( + result.ok().and_then(|result| result + .as_string() + .map(|value| value.to_std_string_escaped())), + Some( + r#"[true,true,"nav","aside","retargeted","retargeted",true,["base","base"]]"# + .to_string() + ) + ); +} + +#[rstest] +#[case("styled(Component)({ color: 'red' })")] +#[case("styled(motion.div)({ color: 'red' })")] +#[case("styled(tag)({ color: 'red' })")] +#[serial] +fn inherited_target_is_preserved_when_typescript_component_renders(#[case] factory: &str) { + // Given: two extensions of a component, member or runtime-bound base in non-JSX TS. + let source = format!( + "{IMPORT}const Component = (props) => props; const motion = {{ div: Component }}; const tag = Component; const Base = {factory}; const Middle = styled(Base)({{ background: 'blue' }}); const Card = styled(Middle)`padding: 4px;`;" + ); + let output = compile("test.ts", &source, true); + let arena = Allocator::default(); + let mut program = parse(&arena, "test.ts", &output); + let calls = renders(&program); + assert_eq!(calls.len(), 3); + let helper = &calls[0].helper; + program + .body + .retain(|statement| !matches!(statement, Statement::ImportDeclaration(_))); + let code = Codegen::new().build(&program).code; + // When: the emitted extension renders through the actual factory. + let script = format!( + "const {helper} = (target, props) => ({{target, props}}); {code}\nCard({{children: 'child'}}).target === Component;" + ); + let mut context = Context::default(); + let result = context + .eval(Source::from_bytes(&script)) + .unwrap_or_else(|error| panic!("inherited-target renderer evaluation failed: {error}")); + // Then: the extension renders the original component, never its string name. + assert_eq!(result.as_boolean(), Some(true)); +} + #[rstest] #[case("styled(tag)({ color: 'red' })")] #[case("styled(targets[name])({ color: 'red' })")] @@ -67,7 +137,7 @@ fn caller_props_win_when_dynamic_styles_are_rendered(#[case] factory: &str) { let code = Codegen::new().build(&program).code; // When: the generated call renders with caller overrides. let script = format!( - "const {helper} = (target, props) => ({{target, props}}); function getColor() {{ return 'red'; }} {code}\nconst first = Card({{className: 'caller', children: 'child', onClick: 7, ref: 8, as: 'section'}}); const variable = Object.keys(first.props.style)[0]; const second = Card({{style: {{[variable]: 'blue'}}, role: 'link'}}); JSON.stringify([first.target, first.props.className.includes('caller'), first.props.style[variable], first.props.children, first.props.onClick, first.props.ref, first.props.as, second.props.style[variable]]);" + "const {helper} = (target, props) => ({{target, props}}); function getColor() {{ return 'red'; }} {code}\nconst first = Card({{className: 'caller', children: 'child', onClick: 7, ref: 8, as: 'section', forwardedAs: 'nav'}}); const variable = Object.keys(first.props.style)[0]; const second = Card({{style: {{[variable]: 'blue'}}, role: 'link'}}); JSON.stringify([first.target, first.props.className.includes('caller'), first.props.style[variable], first.props.children, first.props.onClick, first.props.ref, first.props.as, second.props.style[variable]]);" ); let mut context = Context::default(); let result = context @@ -79,6 +149,6 @@ fn caller_props_win_when_dynamic_styles_are_rendered(#[case] factory: &str) { .as_string() .unwrap_or_else(|| panic!("caller-props renderer JSON result must be a string")) .to_std_string_escaped(), - r#"["article",true,"red","child",7,8,"section","blue"]"#, + r#"["section",true,"red","child",7,8,"nav","blue"]"#, ); } diff --git a/libs/extractor/src/styled_source_tests/lowering.rs b/libs/extractor/src/styled_source_tests/lowering.rs index 258117059..3b0ff0f0b 100644 --- a/libs/extractor/src/styled_source_tests/lowering.rs +++ b/libs/extractor/src/styled_source_tests/lowering.rs @@ -68,13 +68,13 @@ fn passthrough_props_remain_when_attrs_wrap_the_renderer( #[case] attrs: &str, #[values(true, false)] single_css: bool, ) { - // Given: caller as/ref/children are ordinary props, not a request for new semantics. + // Given: attrs wrap a renderer with caller as/ref/children. let source = format!( "{IMPORT}const Card = styled('article'){attrs}`color: red;`;\nconst element = Card({{ as: 'section', ref: callerRef, children: 'child' }});" ); // When: extraction compiles the attrs-wrapped renderer. let output = compile("test.ts", &source, single_css); - // Then: the native target and rest spread remain; ref/as are not stripped or reinterpreted. + // Then: the actual default target and rest spread remain, with forwardedAs supplying output as. let arena = Allocator::default(); let program = parse(&arena, "test.ts", &output); let calls = renders(&program); @@ -86,9 +86,13 @@ fn passthrough_props_remain_when_attrs_wrap_the_renderer( .iter() .map(|(key, _)| key.as_str()) .collect::<Vec<_>>(), - ["...", "className", "style"] + ["...", "className", "style", "as"] ); - assert_eq!(calls[0].props[0].1, "rest"); + assert_eq!( + calls[0].props[0].1, + "Object.fromEntries(Object.entries((({ \"theme\": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))" + ); + assert_eq!(calls[0].props[3].1, "forwardedAs"); assert!(output.contains("as: \"section\"")); assert!(output.contains("ref: callerRef")); assert!(output.contains("children: \"child\"")); diff --git a/libs/extractor/src/styled_source_tests/mod.rs b/libs/extractor/src/styled_source_tests/mod.rs index d8dd637c1..97d60fc22 100644 --- a/libs/extractor/src/styled_source_tests/mod.rs +++ b/libs/extractor/src/styled_source_tests/mod.rs @@ -72,7 +72,7 @@ struct Render { } #[derive(Default)] -struct Renders(Vec<Render>); +struct Renders(Vec<Render>, Vec<Target>); impl<'a> Visit<'a> for Renders { fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'a>) { @@ -93,8 +93,24 @@ impl<'a> Visit<'a> for Renders { keys.iter() .map(std::convert::AsRef::as_ref) .collect::<Vec<_>>(), - ["style", "className"] + ["style", "className", "as", "forwardedAs"] ); + let targets: Vec<_> = pattern + .properties + .iter() + .filter_map(|property| match &property.value { + BindingPattern::AssignmentPattern(value) => Some(&value.right), + _ => None, + }) + .collect(); + assert_eq!(targets.len(), 1); + self.1.push(match targets[0] { + Expression::StringLiteral(tag) => Target::Tag(tag.value.to_string()), + Expression::Identifier(component) => { + Target::Component(component.name.to_string()) + } + expression => Target::Member(readable_code(expression)), + }); assert_eq!( pattern .rest @@ -106,6 +122,9 @@ impl<'a> Visit<'a> for Renders { } } walk_arrow_function_expression(self, arrow); + if arrow.params.items.iter().any(|parameter| matches!(¶meter.pattern, BindingPattern::ObjectPattern(pattern) if pattern.properties.iter().any(|property| property.key.static_name().is_some_and(|name| name == "forwardedAs")))) { + self.1.pop(); + } } fn visit_call_expression(&mut self, call: &CallExpression<'a>) { @@ -134,23 +153,19 @@ impl<'a> Visit<'a> for Renders { } }) .collect(); - if !props - .iter() - .any(|(key, value)| key == "..." && value == "rest") + if !props.iter().any(|(key, _)| key == "...") || !props.iter().any(|(key, _)| key == "className") || !props.iter().any(|(key, _)| key == "style") { return; } - let target = match target.as_expression() { - Some(Expression::StringLiteral(tag)) => Target::Tag(tag.value.to_string()), - Some(Expression::Identifier(component)) => { - Target::Component(component.name.to_string()) - } - Some(expression @ Expression::StaticMemberExpression(_)) => { - Target::Member(readable_code(expression)) - } - other => panic!("unexpected render target: {other:?}"), + assert!( + matches!(target.as_expression(), Some(Expression::Identifier(identifier)) if identifier.name == "DevupAs") + ); + let target = match &self.1[self.1.len() - 1] { + Target::Tag(name) => Target::Tag(name.clone()), + Target::Component(name) => Target::Component(name.clone()), + Target::Member(name) => Target::Member(name.clone()), }; let (helper, reference) = match &call.callee { Expression::Identifier(helper) => (helper.name.to_string(), helper.reference_id.get()), diff --git a/libs/extractor/src/tests/styled_transient_tests.rs b/libs/extractor/src/tests/styled_transient_tests.rs new file mode 100644 index 000000000..8535bd155 --- /dev/null +++ b/libs/extractor/src/tests/styled_transient_tests.rs @@ -0,0 +1,260 @@ +//! Runtime-only transient props at the generated styled forwarding boundary. + +use boa_engine::{Context, Source}; +use oxc_allocator::Allocator; +use oxc_ast::ast::{JSXAttributeItem, JSXAttributeName, JSXAttributeValue, JSXElement}; +use oxc_ast_visit::Visit; +use oxc_parser::Parser; +use oxc_span::{GetSpan, SourceType, Span}; +use rstest::rstest; +use serial_test::serial; + +use super::readable_code; + +/// Replace only the generated JSX renderer with an observable target/props record. +/// All generated forwarding, style reads and attrs expressions execute unchanged. +#[derive(Default)] +struct Renderer { + replacement: Option<(Span, String)>, +} + +impl<'a> Visit<'a> for Renderer { + fn visit_jsx_element(&mut self, element: &JSXElement<'a>) { + assert_eq!(self.replacement, None, "fixture must generate one renderer"); + assert_eq!(element.children.len(), 0); + let mut properties = Vec::new(); + for attribute in &element.opening_element.attributes { + match attribute { + JSXAttributeItem::SpreadAttribute(spread) => properties.push(format!( + "...({})", + crate::utils::readable_code(&spread.argument) + )), + JSXAttributeItem::Attribute(attribute) => { + let JSXAttributeName::Identifier(name) = &attribute.name else { + panic!("generated renderer must use named attributes"); + }; + let Some(JSXAttributeValue::ExpressionContainer(value)) = &attribute.value + else { + panic!("generated renderer must use expression attributes"); + }; + let expression = value.expression.as_expression().unwrap(); + properties.push(format!( + "\"{}\": ({})", + name.name, + crate::utils::readable_code(expression) + )); + } + } + } + self.replacement = Some(( + element.span(), + format!( + "({{ target: DevupAs, props: {{ {} }} }})", + properties.join(", ") + ), + )); + } +} + +fn executable_component(code: String) -> String { + let mut code = code + .strip_prefix("import \"@devup-ui/core/devup-ui.css\";\n") + .unwrap_or(&code) + .to_owned(); + assert!( + !code.contains("import "), + "unexpected generated import: {code}" + ); + let allocator = Allocator::default(); + let parsed = Parser::new(&allocator, &code, SourceType::tsx()).parse(); + assert_eq!(parsed.diagnostics.len(), 0, "{code}"); + let mut renderer = Renderer::default(); + renderer.visit_program(&parsed.program); + let (span, replacement) = renderer.replacement.expect("generated styled renderer"); + code.replace_range( + usize::try_from(span.start).unwrap()..usize::try_from(span.end).unwrap(), + &replacement, + ); + code +} + +#[rstest] +#[case::tag_default("styled('div')", (false, false))] +#[case::component_default("styled(Link)", (true, true))] +#[case::tag_predicate( + "styled('div', { shouldForwardProp: (prop) => prop !== 'tone' })", + (true, false) +)] +#[case::component_predicate( + "styled(Link, { shouldForwardProp: (prop) => prop !== 'tone' })", + (true, false) +)] +#[case::tag_always_true("styled('div', { shouldForwardProp: () => true })", (true, true))] +#[case::component_always_true("styled(Link, { shouldForwardProp: () => true })", (true, true))] +#[serial] +fn filters_runtime_transients_when_forwarding_styled_rest( + #[case] factory: &str, + #[case] forwarded: (bool, bool), + #[values("", ".attrs((p) => ({ ...p.extra, id: p.$attrs }))")] attrs: &str, +) { + // Given: transient keys supplied only at execution time, never in styled source. + let source = format!( + "import {{styled}} from '@devup-ui/core'\n\ + const Subject = {factory}{attrs}`color: ${{(p) => p.$tone}}; background: ${{(p) => p.tone}};`" + ); + let code = readable_code(&source); + for runtime_key in ["$runtimeOnly", "$🦀", "$fromAttrs"] { + assert!( + !code.contains(runtime_key), + "runtime key enumerated: {code}" + ); + } + let component = executable_component(code); + let script = format!( + r" + const Link = 'component'; + {component} + const input = {{ + $runtimeOnly: 'secret', '$': 1, '$🦀': 2, + $tone: 'orchid', $attrs: 'attrs-id', tone: 'navy', + theme: {{ marker: true }}, id: 'caller-id', + 'data-ok': 'yes', 'aria-label': 'label', + customPayload: 42, children: 'child', + className: 'caller', style: {{ opacity: 0.25 }}, + forwardedAs: 'forwarded-target', + extra: {{ $fromAttrs: 'secret', title: 'attrs-title' }} + }}; + const before = JSON.stringify(input); + const rendered = [Subject(input), Subject({{ ...input, as: 'override' }})]; + JSON.stringify({{ + transients: rendered.map((r) => Object.keys(r.props).filter((k) => k.startsWith('$'))), + styleValues: rendered.map((r) => Object.values(r.props.style)), + targets: rendered.map((r) => r.target), + preserved: rendered.map((r) => [ + r.props.id, r.props['data-ok'], r.props['aria-label'], + r.props.customPayload, r.props.children, r.props.as, + r.props.className.includes('caller'), r.props.style.opacity, + 'theme' in r.props, 'tone' in r.props, r.props.title || null + ]), + unchanged: before === JSON.stringify(input) + }}) + " + ); + + // When: execute the Rust-produced component, including attrs and both as paths. + let value = Context::default() + .eval(Source::from_bytes(&script)) + .expect("generated component must execute"); + let result: serde_json::Value = serde_json::from_str( + &value + .as_string() + .expect("JSON result") + .to_std_string_escaped(), + ) + .unwrap(); + + // Then: no transient reaches either target, while style reads retain its value. + assert_eq!(result["transients"], serde_json::json!([[], []])); + for values in result["styleValues"].as_array().unwrap() { + assert!( + values + .as_array() + .unwrap() + .contains(&serde_json::json!("orchid")) + ); + assert!( + values + .as_array() + .unwrap() + .contains(&serde_json::json!("navy")) + ); + } + let target = if factory.starts_with("styled(Link") { + "component" + } else { + "div" + }; + assert_eq!(result["targets"], serde_json::json!([target, "override"])); + let (id, title) = if attrs.is_empty() { + ("caller-id", None) + } else { + ("attrs-id", Some("attrs-title")) + }; + let preserved = serde_json::json!([ + id, + "yes", + "label", + 42, + "child", + "forwarded-target", + true, + 0.25, + forwarded.0, + forwarded.1, + title + ]); + assert_eq!( + result["preserved"], + serde_json::json!([preserved, preserved]) + ); + assert_eq!(result["unchanged"], true); +} + +#[rstest] +#[case::component("styled(Link)")] +#[case::tag_always_true("styled('div', { shouldForwardProp: () => true })")] +#[serial] +fn filters_runtime_transients_when_no_props_are_statically_withheld(#[case] factory: &str) { + // Given: no style or attrs reads to populate the named withholding list. + let component = executable_component(readable_code(&format!( + "import {{styled}} from '@devup-ui/core'\nconst Subject = {factory}({{ color: 'red' }})" + ))); + let script = format!( + r" + const Link = 'component'; + {component} + const input = {{ $runtimeOnly: 1, '$': 2, '$🦀': 3, id: 'kept', 'x$y': 4, '': 5 }}; + const before = JSON.stringify(input); + const props = Subject(input).props; + JSON.stringify({{ + transients: Object.keys(props).filter((k) => k.startsWith('$')), + kept: [props.id, props['x$y'], props['']], + unchanged: before === JSON.stringify(input) + }}) + " + ); + + // When: execute the generated component's raw-rest forwarding branch. + let value = Context::default() + .eval(Source::from_bytes(&script)) + .expect("generated component must execute"); + let result: serde_json::Value = serde_json::from_str( + &value + .as_string() + .expect("JSON result") + .to_std_string_escaped(), + ) + .unwrap(); + + // Then: only prefix-$ keys are omitted and caller input is untouched. + assert_eq!( + result, + serde_json::json!({ + "transients": [], "kept": ["kept", 4, 5], "unchanged": true + }) + ); +} + +#[test] +#[serial] +fn keeps_ordinary_jsx_spreads_when_no_styled_rest_is_generated() { + // Given: an ordinary JSX spread, not a styled component definition. + let source = "export const node = <div {...props} />;"; + + // When: extract the ordinary JSX module. + let code = readable_code(source); + + // Then: forwarding is unchanged and no transient filter is generated. + assert!(code.contains("<div {...props} />"), "{code}"); + assert!(!code.contains("Object.fromEntries"), "{code}"); +} diff --git a/libs/extractor/src/utils.rs b/libs/extractor/src/utils.rs index 15a011a7d..c659ddd38 100644 --- a/libs/extractor/src/utils.rs +++ b/libs/extractor/src/utils.rs @@ -823,7 +823,10 @@ fn branch<'b, 'a>(expression: &'b Expression<'a>) -> Option<Branch<'b, 'a>> { /// `value` as a class: itself when it is a string, nothing otherwise, as the /// libraries skip `true` and other non-class values -fn string_class<'a>(ast_builder: &AstBuilder<'a>, value: &Expression<'a>) -> Expression<'a> { +pub(super) fn string_class<'a>( + ast_builder: &AstBuilder<'a>, + value: &Expression<'a>, +) -> Expression<'a> { if matches!( value, Expression::StringLiteral(_) | Expression::TemplateLiteral(_) diff --git a/libs/extractor/src/vanilla_extract.rs b/libs/extractor/src/vanilla_extract.rs index 2c0f1e820..9a6dfdc49 100644 --- a/libs/extractor/src/vanilla_extract.rs +++ b/libs/extractor/src/vanilla_extract.rs @@ -1362,18 +1362,22 @@ fn inner_json(json: &str) -> &str { .trim() } -/// Styles `entry` composes, transitively and in order, each once +/// Styles `entry` composes, transitively and in order; a style composed again +/// later is listed again, as its declarations then win. `composing` holds the +/// styles being expanded, so a style composing itself stops. fn collect_bases<'a>( collected: &'a CollectedStyles, entry: &'a StyleEntry, - seen: &mut FxHashSet<&'a str>, + composing: &mut Vec<&'a str>, bases: &mut Vec<(&'a str, &'a StyleEntry)>, ) { for base in &entry.bases { - if seen.insert(base.as_str()) + if !composing.contains(&base.as_str()) && let Some(base_entry) = collected.styles.get(base) { - collect_bases(collected, base_entry, seen, bases); + composing.push(base); + collect_bases(collected, base_entry, composing, bases); + composing.pop(); bases.push((base.as_str(), base_entry)); } } @@ -1388,34 +1392,39 @@ fn composed_css( name: &str, entry: &StyleEntry, ) -> String { - let mut seen = FxHashSet::default(); - seen.insert(name); + let mut composing = vec![name]; let mut bases = Vec::new(); - collect_bases(collected, entry, &mut seen, &mut bases); + collect_bases(collected, entry, &mut composing, &mut bases); + // One argument per style: `css()` merges them, a later declaration + // replacing an earlier one, which one object holding both would not do let mut rules = Vec::with_capacity(bases.len() + 1); - let mut classes = Vec::new(); + let mut all_classes: Vec<&str> = Vec::new(); for (base_name, base) in &bases { let json = collected.resolve_json(&base.json, keyframes_names); - let inner = inner_json(&json); - if !inner.is_empty() { - rules.push(inner.to_string()); + if !inner_json(&json).is_empty() { + rules.push(json); + } + all_classes.extend(base.classes.iter().map(String::as_str)); + all_classes.extend(referenced_classes.get(base_name).copied()); + } + all_classes.extend(entry.classes.iter().map(String::as_str)); + all_classes.extend(referenced_classes.get(name).copied()); + let mut classes: Vec<&str> = Vec::with_capacity(all_classes.len()); + for class in all_classes { + if !classes.contains(&class) { + classes.push(class); } - classes.extend(base.classes.iter().map(String::as_str)); - classes.extend(referenced_classes.get(base_name).copied()); } - classes.extend(entry.classes.iter().map(String::as_str)); - classes.extend(referenced_classes.get(name).copied()); let own = collected.resolve_json(&entry.json, keyframes_names); let css = if bases.is_empty() { format!("css({own})") } else { - let inner = inner_json(&own); - if !inner.is_empty() { - rules.push(inner.to_string()); + if !inner_json(&own).is_empty() { + rules.push(own); } - format!("css({{{}}})", rules.join(",")) + format!("css({})", rules.join(", ")) }; if classes.is_empty() { css @@ -1746,7 +1755,7 @@ globalStyle(`${tone.primary} > span`, { fontWeight: 700 })" const _ve0 = css({"color":"blue"}) const _ve1 = css({"color":"red"}) + " f0__ve1" const _ve2 = css({"padding":"4px","content":"sm"}) -export const combined = css({"color":"red","margin":"1px"}) + " f0__ve1 external" +export const combined = css({"color":"red"}, {"margin":"1px"}) + " f0__ve1 external" globalCss({ ".f0__ve1 > span": {"fontWeight":700} }) export const tone = { "0": _ve0, "primary": _ve1 } export const space = { "sm": _ve2 } @@ -1754,6 +1763,26 @@ export const none = {}"# ); } + // Each composed style is an argument of its own, so a later one's + // declaration replaces an earlier one's, a style composed again included + #[test] + #[serial] + fn test_composition_keeps_later_declarations() { + assert_eq!( + generate( + "const first = style({ color: 'red', margin: 1 }) +const second = style({ color: 'blue' }) +export const later = style([first, second]) +export const again = style([first, second, first])" + ), + r#"import { css } from '@devup-ui/react' +export const again = css({"color":"red","margin":"1px"}, {"color":"blue"}, {"color":"red","margin":"1px"}) +const first = css({"color":"red","margin":"1px"}) +export const later = css({"color":"red","margin":"1px"}, {"color":"blue"}) +const second = css({"color":"blue"})"# + ); + } + #[test] #[serial] fn test_composition_is_transitive() { @@ -1771,8 +1800,8 @@ export const hover = style({ selectors: { [`${a}:hover &`]: { color: 'blue' } } ), r#"import { css } from '@devup-ui/react' const a = css({"color":"red"}) + " f0_a" -const b = css({"color":"red","margin":"2px"}) + " f0_a" -export const c = css({"color":"red","margin":"2px","padding":"3px"}) + " f0_a" +const b = css({"color":"red"}, {"margin":"2px"}) + " f0_a" +export const c = css({"color":"red"}, {"margin":"2px"}, {"padding":"3px"}) + " f0_a" const e = css({}) export const f = css({"color":"red"}) + " f0_a" export const g = css({}) diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index 31c48b95d..5e7c642e1 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -1,6 +1,7 @@ use crate::as_visit::As; use crate::class_arguments::{class_arguments, flatten_classes}; use crate::component::ExportVariableKind; +use crate::composition::{KnownPart, KnownSide, KnownStyles, set_prop_order}; use crate::css_utils::{ TemplateStyles, css_to_style_template, keyframes_to_keyframes_style, optimize_css_block, template_css_text, @@ -21,7 +22,10 @@ use crate::extractor::{ LiteralHandling, extract_style_from_expression, flatten_spreads, }, extract_style_from_jsx::extract_style_from_jsx, - extract_style_from_styled::{extract_style_from_styled, take_styled_modifiers}, + extract_style_from_styled::{ + StyledDefinition, StyledExtraction, extended, extract_style_from_styled, read_forward, + take_styled_modifiers, with_component, + }, styled_jsx::StyledJsx, }; use crate::gen_class_name::{gen_class_names, merge_expression_for_class_name}; @@ -54,6 +58,7 @@ use oxc_ast_visit::walk_mut::{ walk_variable_declarator, walk_variable_declarators, }; use oxc_syntax::number::NumberBase; +use oxc_syntax::operator::LogicalOperator; use strum::IntoEnumIterator; use crate::utils::{ @@ -62,8 +67,8 @@ use crate::utils::{ get_str_by_property_key, get_string_by_literal_expression, get_string_by_property_key, is_pure, jsx_expression_to_style_order, key_error, readable_argument, readable_code, reads_directly, reads_spreads_once, reads_unknown, runtime_classes, runtime_value, runtime_value_error, - spread_error, stays_attribute, style_arguments, uncomposable_error, unplaced_error, - unreadable_styles, unused_error, unwrap_syntax_only, unwrap_syntax_only_mut, + spread_error, stays_attribute, string_class, style_arguments, uncomposable_error, + unplaced_error, unreadable_styles, unused_error, unwrap_syntax_only, unwrap_syntax_only_mut, }; use oxc_ast::builder::AstBuilder; use oxc_span::{GetSpan, SPAN}; @@ -71,6 +76,15 @@ use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; use std::rc::Rc; +/// `false ?? right` is `false`, which composes nothing +fn coalesce_keeps_left(logical: &oxc_ast::ast::LogicalExpression<'_>) -> bool { + logical.operator == LogicalOperator::Coalesce + && matches!( + unwrap_syntax_only(&logical.left), + Expression::BooleanLiteral(_) + ) +} + fn property_stays(property: &ObjectProperty<'_>) -> bool { property .key @@ -182,6 +196,17 @@ pub struct DevupVisitor<'a> { runtime_types: usize, /// The classes and keyframes names the file binds to a `const` style_values: crate::style_values::StyleValues, + /// The styles of the last `css()` call giving a class, by where it starts, + /// for the `const` it initializes + css_styles: Option<(u32, Vec<ExtractStyleValue>)>, + /// The styles behind `css()` classes the file imports, by binding + imported_css: FxHashMap<String, Vec<ExtractStyleValue>>, + /// The styled component just built, by where it starts, for the `const` + /// it initializes + pending_styled: Option<(u32, StyledDefinition<'a>)>, + /// The styled components the file binds to a `const`, which a component + /// extending one renders in its place + styled_definitions: FxHashMap<oxc_syntax::symbol::SymbolId, StyledDefinition<'a>>, /// What the build compiles away: the imports of the package it removes, /// and the bindings aliasing them, which only its calls and elements read compiled_names: FxHashSet<String>, @@ -321,6 +346,269 @@ impl<'a> DevupVisitor<'a> { } } } + + /// `css(...)` composing a class whose styles the build knows: the parts' + /// styles merge, a later declaration replacing an earlier one. `None` when + /// no part is such a class, or a part is one composing does not read. + fn compose_known_styles(&mut self, call: &CallExpression<'a>) -> Option<Expression<'a>> { + let arguments: Vec<&Expression<'a>> = call + .arguments + .iter() + .map(|argument| match argument { + Argument::SpreadElement(spread) => &spread.argument, + argument => argument.to_expression(), + }) + .collect(); + if !arguments + .iter() + .any(|argument| self.reads_known_styles(argument)) + { + return None; + } + let mut parts = Vec::new(); + for argument in &arguments { + self.known_parts(argument, &mut parts)?; + } + self.unknown_arguments("css", &call.arguments); + self.changed_arguments("css", &call.arguments); + + let mut composition = crate::composition::Composition::default(); + let mut classes = Vec::new(); + for part in parts { + match part { + KnownPart::Styles(side) => { + let props = self.part_props(call.span.start, side); + composition.apply(&self.ast, props); + } + KnownPart::Conditional { + test, + consequent, + alternate, + } => { + let consequent = self.part_props(call.span.start, consequent); + let alternate = self.part_props(call.span.start, alternate); + composition.apply_conditional(&self.ast, &test, consequent, alternate); + } + KnownPart::Class(mut class) => { + self.style_values.read_in(&self.ast, &mut class); + classes.push(class); + } + } + } + let known = composition.unconditional(); + let mut props = composition.into_props(); + // Class names come out in reverse, so they read in composing order + props.reverse(); + let class_name = + gen_class_names(&self.ast, &mut props, None, self.split_filename.as_deref()); + self.styles + .extend(props.into_iter().flat_map(ExtractStyleProp::into_extract)); + let composed_class = classes.is_empty(); + let result = + merge_expression_for_class_name(&self.ast, classes.into_iter().chain(class_name)) + .unwrap_or_else(|| Expression::new_string_literal(SPAN, "", None, &self.ast)); + if composed_class && let Some(known) = known { + self.css_styles = Some((call.span.start, known)); + } + Some(result) + } + + /// Whether `expression`, or a part of it, is a class whose styles the + /// build knows + fn reads_known_styles(&self, expression: &Expression<'a>) -> bool { + match unwrap_syntax_only(expression) { + Expression::ArrayExpression(array) => array.elements.iter().any(|element| { + element + .as_expression() + .is_some_and(|element| self.reads_known_styles(element)) + }), + Expression::LogicalExpression(logical) => self.reads_known_styles(&logical.right), + Expression::ConditionalExpression(conditional) => { + self.reads_known_styles(&conditional.consequent) + || self.reads_known_styles(&conditional.alternate) + } + expression => self.style_values.styles(expression).is_some(), + } + } + + /// The parts `expression` composes, in order; `None` for a shape this + /// path does not read, which the general one then reads + fn known_parts( + &self, + expression: &Expression<'a>, + parts: &mut Vec<KnownPart<'a>>, + ) -> Option<()> { + let clone = |expression: &Expression<'a>| expression.clone_in(self.ast.allocator()); + let (test, consequent, alternate) = match unwrap_syntax_only(expression) { + Expression::ArrayExpression(array) => { + for element in &array.elements { + self.known_parts(element.as_expression()?, parts)?; + } + return Some(()); + } + Expression::LogicalExpression(logical) if logical.operator == LogicalOperator::And => { + (&logical.left, &logical.right, None) + } + // `left || right` and `left ?? right`: `left` while it applies, `right` + // otherwise + Expression::LogicalExpression(logical) => { + return match self.known_side(&logical.left)? { + KnownSide::Styles(side) => { + parts.push(KnownPart::Styles(side)); + Some(()) + } + KnownSide::Empty if coalesce_keeps_left(logical) => Some(()), + KnownSide::Empty => self.known_parts(&logical.right, parts), + KnownSide::Class(left) => { + let test = if logical.operator == LogicalOperator::Or { + clone(&left) + } else { + Expression::new_binary_expression( + SPAN, + clone(&left), + BinaryOperator::Inequality, + Expression::new_null_literal(SPAN, &self.ast), + &self.ast, + ) + }; + let right = self.known_side(&logical.right)?; + self.push_choice( + parts, + &test, + KnownSide::Class(string_class(&self.ast, &left)), + right, + ); + Some(()) + } + }; + } + Expression::ConditionalExpression(conditional) => ( + &conditional.test, + &conditional.consequent, + Some(&conditional.alternate), + ), + expression => { + match self.known_side(expression)? { + KnownSide::Styles(side) => parts.push(KnownPart::Styles(side)), + KnownSide::Class(class) => parts.push(KnownPart::Class(class)), + KnownSide::Empty => {} + } + return Some(()); + } + }; + let consequent = self.known_side(consequent)?; + let alternate = alternate.map_or(Some(KnownSide::Empty), |alternate| { + self.known_side(alternate) + })?; + self.push_choice(parts, test, consequent, alternate); + Some(()) + } + + /// `test ? consequent : alternate` among the parts, its classes and its + /// styles each choosing on their own + fn push_choice( + &self, + parts: &mut Vec<KnownPart<'a>>, + test: &Expression<'a>, + consequent: KnownSide<'a>, + alternate: KnownSide<'a>, + ) { + let clone = |expression: &Expression<'a>| expression.clone_in(self.ast.allocator()); + let mut classes = [None, None]; + let mut styles = [None, None]; + for (index, side) in [consequent, alternate].into_iter().enumerate() { + match side { + KnownSide::Styles(side) => styles[index] = Some(side), + KnownSide::Class(class) => classes[index] = Some(class), + KnownSide::Empty => {} + } + } + if classes.iter().any(Option::is_some) { + let [consequent, alternate] = classes.map(|class| { + class.unwrap_or_else(|| Expression::new_string_literal(SPAN, "", None, &self.ast)) + }); + parts.push(KnownPart::Class(Expression::new_conditional_expression( + SPAN, + clone(test), + consequent, + alternate, + &self.ast, + ))); + } + if styles.iter().any(Option::is_some) { + let [consequent, alternate] = styles.map(Option::unwrap_or_default); + parts.push(KnownPart::Conditional { + test: clone(test), + consequent, + alternate, + }); + } + } + + fn known_side(&self, expression: &Expression<'a>) -> Option<KnownSide<'a>> { + let expression = unwrap_syntax_only(expression); + if let Some(styles) = self.style_values.styles(expression) { + return Some(KnownSide::Styles(vec![KnownStyles::Known(styles.to_vec())])); + } + match expression { + Expression::ObjectExpression(_) => Some(KnownSide::Styles(vec![KnownStyles::Rules( + expression.clone_in(self.ast.allocator()), + )])), + Expression::NullLiteral(_) | Expression::BooleanLiteral(_) => Some(KnownSide::Empty), + Expression::Identifier(identifier) if identifier.name == "undefined" => { + Some(KnownSide::Empty) + } + Expression::Identifier(_) + | Expression::StaticMemberExpression(_) + | Expression::ComputedMemberExpression(_) + | Expression::StringLiteral(_) + | Expression::TemplateLiteral(_) => { + Some(KnownSide::Class(expression.clone_in(self.ast.allocator()))) + } + _ => None, + } + } + + /// The styles of a part, each at the order the part gives it + fn part_props( + &mut self, + offset: u32, + styles: Vec<KnownStyles<'a>>, + ) -> Vec<ExtractStyleProp<'a>> { + let mut props = Vec::new(); + for styles in styles { + match styles { + KnownStyles::Known(values) => { + props.extend(values.into_iter().map(ExtractStyleProp::Static)); + } + KnownStyles::Rules(mut rules) => { + self.style_values.read_in(&self.ast, &mut rules); + let ExtractResult { + mut styles, + style_order, + .. + } = extract_style_from_expression( + &self.ast, + None, + &mut rules, + 0, + &None, + LiteralHandling::ExpandResponsiveThemeToken, + ); + if let Some(value) = runtime_value(&styles) { + self.errors.push((offset, unused_error("css", &value))); + } + if let Some(order) = style_order { + for prop in &mut styles { + set_prop_order(prop, order); + } + } + props.extend(styles); + } + } + } + props + } pub fn new( allocator: &'a Allocator, filename: &str, @@ -361,6 +649,10 @@ impl<'a> DevupVisitor<'a> { spreads_read_once: 0, runtime_types: 0, style_values: crate::style_values::StyleValues::default(), + css_styles: None, + pending_styled: None, + styled_definitions: FxHashMap::default(), + imported_css: FxHashMap::default(), compiled_names: FxHashSet::default(), unknown_bindings: crate::imported_constants::Unknown::default(), composes_unknown: false, @@ -481,18 +773,17 @@ impl<'a> DevupVisitor<'a> { fn binds_style_results(&self, program: &Program<'a>) -> bool { let mut class_names = DynamicClassNames::default(); oxc_ast_visit::Visit::visit_program(&mut class_names, program); - class_names.found - || program.body.iter().any(|statement| { - matches!(statement, Statement::ImportDeclaration(import) - if (import.source.value == self.package - || import.source.value == self.compat_package.as_str()) - && import.specifiers.iter().flatten().any(|specifier| match specifier { - ImportSpecifier(specifier) => { - matches!(specifier.imported.name().as_str(), "css" | "keyframes") - } - _ => true, - })) - }) + class_names.found || program.body.iter().any(|statement| { + matches!(statement, Statement::ImportDeclaration(import) + if (import.source.value == self.package + || import.source.value == self.compat_package.as_str()) + && import.specifiers.iter().flatten().any(|specifier| match specifier { + ImportSpecifier(specifier) => { + matches!(specifier.imported.name().as_str(), "css" | "keyframes" | "styled") + } + _ => true, + })) + }) } /// Put the spreads of `props` that may change when read again, and the @@ -588,6 +879,10 @@ impl<'a> DevupVisitor<'a> { /// `StyleX` variables and themes the program imports from other modules, /// by the name it binds them to + pub fn import_css(&mut self, styles: FxHashMap<String, Vec<ExtractStyleValue>>) { + self.imported_css = styles; + } + pub fn import_stylex( &mut self, vars: FxHashMap<String, FxHashMap<String, String>>, @@ -1140,6 +1435,8 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { .semantic .into_scoping(), ); + self.style_values + .import(std::mem::take(&mut self.imported_css)); } walk_program(self, it); self.style_values.read_names(&self.ast, it); @@ -1211,9 +1508,12 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { _ => None, }; let styled_imports = &self.styled_imports; - let attrs = factory.map_or_else(Vec::new, |factory| { - take_styled_modifiers(&self.ast, factory, |name| styled_imports.contains(name)) - }); + let (attrs, mut configs) = factory.map_or_else( + || (Vec::new(), Vec::new()), + |factory| { + take_styled_modifiers(&self.ast, factory, |name| styled_imports.contains(name)) + }, + ); let factory = match it { Expression::CallExpression(call) => Some(&mut call.callee), Expression::TaggedTemplateExpression(tag) => Some(&mut tag.tag), @@ -1226,8 +1526,13 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { matches!(unwrap_syntax_only(options), Expression::ObjectExpression(_)) }) && matches!(&call.callee, Expression::Identifier(ident) if self.styled_imports.contains(ident.name.as_str())) + && let Some(options) = call.arguments.pop() { - call.arguments.truncate(1); + configs.insert(0, options.into_expression()); + } + let (forward, forward_error) = read_forward(&configs.iter().collect::<Vec<_>>()); + if let Some(error) = forward_error { + self.errors.push(error); } walk_expression(self, it); @@ -1271,12 +1576,23 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.unknown_arguments("styled", &call.arguments); self.changed_arguments("styled", &call.arguments); } - let (result, new_expr, errors) = extract_style_from_styled( + let inherited = extended(it) + .and_then(|base| self.style_values.symbol(base)) + .and_then(|symbol| self.styled_definitions.get(&symbol)) + .filter(|definition| definition.extendable()); + let start = it.span().start; + let StyledExtraction { + result, + expression, + errors, + definition, + } = extract_style_from_styled( &self.ast, it, self.split_filename.as_deref(), &self.imports, - &attrs, + (&attrs, forward), + inherited, &mut self.styled_jsx, ); self.errors.extend(errors); @@ -1286,10 +1602,38 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { .into_iter() .flat_map(ExtractStyleProp::into_extract), ); - *it = new_expr; + self.pending_styled = definition.map(|definition| (start, definition)); + *it = expression; } } + // `Component.withComponent(target)` on a styled component the file + // defines renders its styles as `target` + if let Expression::CallExpression(call) = it + && let [ + Argument::StringLiteral(_) + | Argument::Identifier(_) + | Argument::StaticMemberExpression(_), + ] = call.arguments.as_slice() + && let Expression::StaticMemberExpression(member) = &call.callee + && member.property.name == "withComponent" + && let Some(definition) = self + .style_values + .symbol(&member.object) + .and_then(|symbol| self.styled_definitions.get(&symbol)) + && let Some((component, definition)) = with_component( + &self.ast, + definition, + call.arguments[0].to_expression(), + self.split_filename.as_deref(), + &mut self.styled_jsx, + ) + { + let start = call.span.start; + *it = component; + self.pending_styled = Some((start, definition)); + } + // Handle StyleX: stylex.create({...}) calls if let Expression::CallExpression(call) = it && self.is_stylex_call(&call.callee, &StylexFunction::Create) @@ -1692,7 +2036,14 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { )); } - if let Expression::CallExpression(call) = it { + if let Expression::CallExpression(call) = it + && self + .util_type(&call.callee) + .is_some_and(|util| matches!(util.as_ref(), UtilType::Css)) + && let Some(composed) = self.compose_known_styles(call) + { + *it = composed; + } else if let Expression::CallExpression(call) = it { if let Some(util_type) = self.util_type(&call.callee) { for argument in &mut call.arguments { let expression = match argument { @@ -1765,6 +2116,20 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { if styles.is_empty() { Expression::new_string_literal(SPAN, "", None, &self.ast) } else { + let known = composed_classes.is_empty().then(|| { + let mut known = crate::composition::Composition::default(); + let mut props: Vec<ExtractStyleProp<'a>> = styles + .iter() + .map(|prop| prop.clone_in(self.ast.allocator())) + .collect(); + if let Some(order) = style_order { + for prop in &mut props { + set_prop_order(prop, order); + } + } + known.apply(&self.ast, props); + known.unconditional() + }); // css can not reachable let class_name = gen_class_names( &self.ast, @@ -1777,6 +2142,11 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.styles.extend( styles.into_iter().flat_map(ExtractStyleProp::into_extract), ); + if let Some(Some(known)) = known + && matches!(class_name, Some(Expression::StringLiteral(_))) + { + self.css_styles = Some((offset, known)); + } if let Some(cls) = class_name { cls } else { @@ -2313,6 +2683,8 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { } .filter(|util| util.is_css() || matches!(util.as_ref(), UtilType::Keyframes)) .and_then(|util| Some((util, self.style_values.constant(&it.id)?))); + let start = it.init.as_ref().map(|init| init.span().start); + let styled_binding = self.style_values.constant(&it.id); let class_text = match &it.init { Some(Expression::StringLiteral(text)) => Some(text.value.to_string()), @@ -2329,6 +2701,13 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { walk_variable_declarator(self, it); + if let Some((at, definition)) = self.pending_styled.take() + && Some(at) == start + && let Some(symbol) = styled_binding + { + self.styled_definitions.insert(symbol, definition); + } + if let Some((text, symbol)) = class_text { self.style_values.insert_text(symbol, text); } @@ -2337,10 +2716,14 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { && let Some(Expression::StringLiteral(value)) = &it.init { let value = value.value.to_string(); + let styles = self + .css_styles + .take() + .and_then(|(at, styles)| (Some(at) == start).then_some(styles)); self.style_values.insert( symbol, if util.is_css() { - crate::style_values::StyleValue::Class(value) + crate::style_values::StyleValue::Class(value, styles) } else { crate::style_values::StyleValue::Keyframes(value) }, @@ -2475,6 +2858,25 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { fn visit_jsx_element(&mut self, elem: &mut JSXElement<'a>) { walk_jsx_element(self, elem); + // A styled component the file defines drops the props it neither reads + // nor passes on, as styled-components and Emotion do at runtime + if let JSXElementName::IdentifierReference(name) = &elem.opening_element.name + && let Some(definition) = self + .style_values + .reference_symbol(name) + .and_then(|symbol| self.styled_definitions.get(&symbol)) + { + elem.opening_element + .attributes + .retain(|attribute| match attribute { + Attribute(attribute) => match &attribute.name { + Identifier(name) => definition.takes(&name.name), + oxc_ast::ast::JSXAttributeName::NamespacedName(_) => true, + }, + JSXAttributeItem::SpreadAttribute(_) => true, + }); + } + // `<Global styles={...} />` is Emotion's spelling of a global stylesheet. // Lift the rules out and strip every attribute, leaving a component that // renders nothing — the same shape `createGlobalStyle` collapses to. diff --git a/packages/components/src/components/Checkbox/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Checkbox/__tests__/__snapshots__/index.browser.test.tsx.snap index 4845f4a2d..9a9418e48 100644 --- a/packages/components/src/components/Checkbox/__tests__/__snapshots__/index.browser.test.tsx.snap +++ b/packages/components/src/components/Checkbox/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -4,7 +4,7 @@ exports[`Checkbox should render basic checkbox 1`] = ` "<div> <div class="display-0-flex--0 align-items-0-center--255 gap-0-8px--255"> <label class="position-0-relative--255 display-0-flex--255 align-items-0-center--255 justify-content-0-center--255 width-0-16px--255 height-0-16px--255 cursor-0-pointer--255" for="_r_0_"> - <input id="_r_0_" class="background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1 " type="checkbox"> + <input id="_r_0_" class=" background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1" type="checkbox"> </label> <label class="cursor-0-pointer--255" for="_r_0_"> <span class="font-size-0-14px--255 user-select-0-none--255 color-0-var_lp_--text_cm_light-dark_lp__h_2F2F2F_cm__h_EDEDED_rp__rp_--255"> @@ -19,7 +19,7 @@ exports[`Checkbox should render checked checkbox 1`] = ` "<div> <div class="display-0-flex--0 align-items-0-center--255 gap-0-8px--255"> <label class="position-0-relative--255 display-0-flex--255 align-items-0-center--255 justify-content-0-center--255 width-0-16px--255 height-0-16px--255 cursor-0-pointer--255" for="_r_1_"> - <input id="_r_1_" class="background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1 " type="checkbox" checked=""> + <input id="_r_1_" class=" background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1" type="checkbox" checked=""> <svg height="10" viewBox="0 0 12 10" width="12" xmlns="http://www.w3.org/2000/svg" class="opacity-0-1--255 pointer-events-0-none--255 z-index-0-1--255"> <path clip-rule="evenodd" d="M11.6474 0.807113C12.0992 1.23373 12.1195 1.94575 11.6929 2.39745L5.31789 9.14745C5.10214 9.37589 4.80069 9.50369 4.48649 9.49992C4.1723 9.49615 3.874 9.36114 3.6638 9.12759L0.288803 5.37759C-0.126837 4.91576 -0.0893993 4.20444 0.372424 3.7888C0.834247 3.37315 1.54557 3.41059 1.96121 3.87242L4.51994 6.71544L10.0571 0.852551C10.4837 0.400843 11.1957 0.3805 11.6474 0.807113Z" fill="var(--checkIcon, #FFF)" fill-rule="evenodd"> </path> @@ -38,7 +38,7 @@ exports[`Checkbox should render disabled checkbox 1`] = ` "<div> <div class="display-0-flex--0 align-items-0-center--255 gap-0-8px--255"> <label class="position-0-relative--255 display-0-flex--255 align-items-0-center--255 justify-content-0-center--255 width-0-16px--255 height-0-16px--255 cursor-0-not-allowed--255" for="_r_2_"> - <input disabled="" id="_r_2_" class="background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 cursor-0-not-allowed--1 " type="checkbox"> + <input disabled="" id="_r_2_" class=" background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 cursor-0-not-allowed--1" type="checkbox"> </label> <label class="cursor-0-not-allowed--255" for="_r_2_"> <span class="font-size-0-14px--255 user-select-0-none--255 color-0-light-dark_lp__h_D6D7DE_cm__h_6F6E6E_rp_--255"> @@ -53,7 +53,7 @@ exports[`Checkbox should render disabled and checked checkbox 1`] = ` "<div> <div class="display-0-flex--0 align-items-0-center--255 gap-0-8px--255"> <label class="position-0-relative--255 display-0-flex--255 align-items-0-center--255 justify-content-0-center--255 width-0-16px--255 height-0-16px--255 cursor-0-not-allowed--255" for="_r_3_"> - <input disabled="" id="_r_3_" class="background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 cursor-0-not-allowed--1 " type="checkbox" checked=""> + <input disabled="" id="_r_3_" class=" background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 cursor-0-not-allowed--1" type="checkbox" checked=""> <svg height="10" viewBox="0 0 12 10" width="12" xmlns="http://www.w3.org/2000/svg" class="opacity-0-1--255 pointer-events-0-none--255 z-index-0-1--255"> <path clip-rule="evenodd" d="M11.6474 0.807113C12.0992 1.23373 12.1195 1.94575 11.6929 2.39745L5.31789 9.14745C5.10214 9.37589 4.80069 9.50369 4.48649 9.49992C4.1723 9.49615 3.874 9.36114 3.6638 9.12759L0.288803 5.37759C-0.126837 4.91576 -0.0893993 4.20444 0.372424 3.7888C0.834247 3.37315 1.54557 3.41059 1.96121 3.87242L4.51994 6.71544L10.0571 0.852551C10.4837 0.400843 11.1957 0.3805 11.6474 0.807113Z" fill="light-dark(#D6D7DE, #47474A)" fill-rule="evenodd"> </path> @@ -72,7 +72,7 @@ exports[`Checkbox should render checkbox with custom child 1`] = ` "<div> <div class="display-0-flex--0 align-items-0-center--255 gap-0-8px--255"> <label class="position-0-relative--255 display-0-flex--255 align-items-0-center--255 justify-content-0-center--255 width-0-16px--255 height-0-16px--255 cursor-0-pointer--255" for="_r_4_"> - <input id="_r_4_" class="background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1 " type="checkbox"> + <input id="_r_4_" class=" background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1" type="checkbox"> </label> <label class="cursor-0-pointer--255" for="_r_4_"> <div> @@ -87,7 +87,7 @@ exports[`Checkbox should render with custom colors 1`] = ` "<div> <div class="display-0-flex--0 align-items-0-center--255 gap-0-8px--255"> <label class="position-0-relative--255 display-0-flex--255 align-items-0-center--255 justify-content-0-center--255 width-0-16px--255 height-0-16px--255 cursor-0-pointer--255" for="_r_h_"> - <input id="_r_h_" class="background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1 " style="--primary: #ff0000; --border: #00ff00; --inputBg: #ffff00;" type="checkbox"> + <input id="_r_h_" class=" background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1" style="--primary: #ff0000; --border: #00ff00; --inputBg: #ffff00;" type="checkbox"> </label> <label class="cursor-0-pointer--255" for="_r_h_"> <span class="font-size-0-14px--255 user-select-0-none--255 color-0-var_lp_--text_cm_light-dark_lp__h_2F2F2F_cm__h_EDEDED_rp__rp_--255" style="--text: #0000ff;"> @@ -102,7 +102,7 @@ exports[`Checkbox should render with partial custom colors 1`] = ` "<div> <div class="display-0-flex--0 align-items-0-center--255 gap-0-8px--255"> <label class="position-0-relative--255 display-0-flex--255 align-items-0-center--255 justify-content-0-center--255 width-0-16px--255 height-0-16px--255 cursor-0-pointer--255" for="_r_i_"> - <input id="_r_i_" class="background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1 " style="--primary: #ff0000;" type="checkbox"> + <input id="_r_i_" class=" background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1" style="--primary: #ff0000;" type="checkbox"> </label> <label class="cursor-0-pointer--255" for="_r_i_"> <span class="font-size-0-14px--255 user-select-0-none--255 color-0-var_lp_--text_cm_light-dark_lp__h_2F2F2F_cm__h_EDEDED_rp__rp_--255" style="--text: #0000ff;"> @@ -117,7 +117,7 @@ exports[`Checkbox should render checked checkbox with custom colors 1`] = ` "<div> <div class="display-0-flex--0 align-items-0-center--255 gap-0-8px--255"> <label class="position-0-relative--255 display-0-flex--255 align-items-0-center--255 justify-content-0-center--255 width-0-16px--255 height-0-16px--255 cursor-0-pointer--255" for="_r_j_"> - <input id="_r_j_" class="background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1 " style="--primary: #ff0000; --border: #00ff00; --inputBg: #ffff00;" type="checkbox" checked=""> + <input id="_r_j_" class=" background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_20_pc__cm__h_FFF_s_80_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_30_pc__cm__h_000_s_70_pc__rp__rp_-_a__c_active-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_100_pc__cm__h_000_s_15_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_100_pc__cm__h_FFF_s_15_pc__rp__rp_-_a__c_checked_c_hover-1 background-0-light-dark_lp_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6159D4_rp__s_10_pc__cm__h_FFF_s_90_pc__rp__cm_color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_6670F9_rp__s_20_pc__cm__h_000_s_80_pc__rp__rp_-_a__c_hover-1 border-0-1px_s_solid_s_var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_hover-1 cursor-0-pointer--1" style="--primary: #ff0000; --border: #00ff00; --inputBg: #ffff00;" type="checkbox" checked=""> <svg height="10" viewBox="0 0 12 10" width="12" xmlns="http://www.w3.org/2000/svg" class="opacity-0-1--255 pointer-events-0-none--255 z-index-0-1--255" style="--checkIcon: #000000;"> <path clip-rule="evenodd" d="M11.6474 0.807113C12.0992 1.23373 12.1195 1.94575 11.6929 2.39745L5.31789 9.14745C5.10214 9.37589 4.80069 9.50369 4.48649 9.49992C4.1723 9.49615 3.874 9.36114 3.6638 9.12759L0.288803 5.37759C-0.126837 4.91576 -0.0893993 4.20444 0.372424 3.7888C0.834247 3.37315 1.54557 3.41059 1.96121 3.87242L4.51994 6.71544L10.0571 0.852551C10.4837 0.400843 11.1957 0.3805 11.6474 0.807113Z" fill="var(--checkIcon, #FFF)" fill-rule="evenodd"> </path> @@ -136,7 +136,7 @@ exports[`Checkbox should render disabled checkbox with custom colors 1`] = ` "<div> <div class="display-0-flex--0 align-items-0-center--255 gap-0-8px--255"> <label class="position-0-relative--255 display-0-flex--255 align-items-0-center--255 justify-content-0-center--255 width-0-16px--255 height-0-16px--255 cursor-0-not-allowed--255" for="_r_k_"> - <input disabled="" id="_r_k_" class="background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 cursor-0-not-allowed--1 " style="--primary: #ff0000; --border: #00ff00; --inputBg: #ffff00;" type="checkbox"> + <input disabled="" id="_r_k_" class=" background-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_-_a__c_checked-1 border-0-none-_a__c_checked-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_checked_c_disabled-1 background-0-light-dark_lp__h_F0F0F3_cm__h_47474A_rp_-_a__c_disabled-1 accent-color-0-var_lp_--primary_cm_light-dark_lp__h_6159D4_cm__h_6670F9_rp__rp_--1 appearance-0-none--1 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E0E0E0_cm__h_333_rp__rp_--1 border-radius-0-2px--1 display-0-block--1 height-0-100_pc_--1 left-0-0--1 margin-0-0--1 opacity-0-1--1 pointer-events-0-none--1 position-0-absolute--1 top-0-0--1 width-0-100_pc_--1 z-index-0-0--1 cursor-0-not-allowed--1" style="--primary: #ff0000; --border: #00ff00; --inputBg: #ffff00;" type="checkbox"> </label> <label class="cursor-0-not-allowed--255" for="_r_k_"> <span class="font-size-0-14px--255 user-select-0-none--255 color-0-light-dark_lp__h_D6D7DE_cm__h_6F6E6E_rp_--255" style="--text: #0000ff;"> diff --git a/packages/components/src/components/Input/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Input/__tests__/__snapshots__/index.browser.test.tsx.snap index be182a6d2..e0a0a3e29 100644 --- a/packages/components/src/components/Input/__tests__/__snapshots__/index.browser.test.tsx.snap +++ b/packages/components/src/components/Input/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -83,7 +83,7 @@ exports[`Input should pass className prop to icon component 1`] = ` exports[`Input should pass props to ClearButton component 1`] = ` "<div> - <button aria-label="clear-button" type="button" class="align-items-0-center--1 background-0-var_lp_--negative20_cm_light-dark_lp__h_0003_cm__h_FFF6_rp__rp_--1 border-0-none--1 border-radius-0-50_pc_--1 height-0-20px--1 width-0-20px--1 color-0-var_lp_--base_cm_light-dark_lp__h_FFF_cm__h_000_rp__rp_--1 cursor-0-pointer--1 display-0-flex--1 justify-content-0-center--1 padding-0-2px--1 position-0-absolute--1 right-0-12px--1 top-0-50_pc_--1 transform-0-translateY_lp_-50_pc__rp_--1 "> + <button aria-label="clear-button" type="button" class=" align-items-0-center--1 background-0-var_lp_--negative20_cm_light-dark_lp__h_0003_cm__h_FFF6_rp__rp_--1 border-0-none--1 border-radius-0-50_pc_--1 height-0-20px--1 width-0-20px--1 color-0-var_lp_--base_cm_light-dark_lp__h_FFF_cm__h_000_rp__rp_--1 cursor-0-pointer--1 display-0-flex--1 justify-content-0-center--1 padding-0-2px--1 position-0-absolute--1 right-0-12px--1 top-0-50_pc_--1 transform-0-translateY_lp_-50_pc__rp_--1"> <svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <path d="M18 6L6 18" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> </path> diff --git a/packages/components/src/components/Select/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Select/__tests__/__snapshots__/index.browser.test.tsx.snap index 4e6276f64..87fc5ed16 100644 --- a/packages/components/src/components/Select/__tests__/__snapshots__/index.browser.test.tsx.snap +++ b/packages/components/src/components/Select/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -2,7 +2,7 @@ exports[`Select should render 1`] = ` "<div> - <div class="display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 "> + <div class=" display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 "> <button aria-label="Select toggle" type="button" aria-expanded="false" class="border-radius-0-8px--2 outline-0-2px_s_solid-_a__c_focus-visible-1 box-sizing-0-border-box--1 cursor-0-pointer--1 font-weight-0-700--1 outline-offset-0-2px--1 position-0-relative--1 transition-0-_d_25s--1 color-0-var_lp_--text_cm__h_272727_rp_-_a__c_active-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_20_pc__cm__h_FFF_s_80_pc__rp_-_a__c_active-1 border-0-1px_s_solid_s_var_lp_--primary_cm__h_8163E1_rp_-_a__c_active-1 color-0-_h_D6D7DE-_a__c_disabled-1 background-color-0-_h_F0F0F3-_a__c_disabled-1 cursor-0-not-allowed-_a__c_disabled-1 border-color-0-var_lp_--border_cm__h_E4E4E4_rp_-_a__c_disabled-1 outline-color-0-var_lp_--primaryFocus_cm__h_9385D3_rp_-_a__c_focus-visible-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_10_pc__cm__h_FFF_s_90_pc__rp_-_a__c_hover-1 color-0-var_lp_--text_cm__h_F6F6F6_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 background-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 color-0-_h_373737-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 background-color-0-_h_47474A-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-transparent-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_674DC7_rp__s_10_pc__cm_var_lp_--inputBackground_cm__h_2E2E2E_rp__s_90_pc__rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 outline-color-0-var_lp_--primaryFocus_cm__h_927CE4_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_focus-visible-1 background-0-var_lp_--inputBackground_cm__h_2E2E2E_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a_-1 background-0-var_lp_--inputBackground_cm__h_FFF_rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm__h_E4E4E4_rp_--1 border-radius-0-10px--1 color-0-var_lp_--text_cm__h_272727_rp_--1 font-size-0-14px--1 font-size-4-15px--1 letter-spacing-0--_d_02em--1 letter-spacing-4--_d_03em--1 padding-right-0-16px--1 padding-left-0-16px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "> <div class="max-width-0-100_pc_--255 margin-right-0-auto--255 margin-left-0-auto--255 position-0-relative--255 width-0-fit-content--255"> <div class=" line-height-0-1_d_2--255 min-height-0-1_d_2em--255 "> @@ -16,7 +16,7 @@ exports[`Select should render 1`] = ` exports[`Select should render with x and y properties 1`] = ` "<div> - <div aria-label="Select" class="display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 "> + <div aria-label="Select" class=" display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 "> <button aria-label="Select toggle" type="button" aria-expanded="false" class="border-radius-0-8px--2 outline-0-2px_s_solid-_a__c_focus-visible-1 box-sizing-0-border-box--1 cursor-0-pointer--1 font-weight-0-700--1 outline-offset-0-2px--1 position-0-relative--1 transition-0-_d_25s--1 color-0-var_lp_--text_cm__h_272727_rp_-_a__c_active-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_20_pc__cm__h_FFF_s_80_pc__rp_-_a__c_active-1 border-0-1px_s_solid_s_var_lp_--primary_cm__h_8163E1_rp_-_a__c_active-1 color-0-_h_D6D7DE-_a__c_disabled-1 background-color-0-_h_F0F0F3-_a__c_disabled-1 cursor-0-not-allowed-_a__c_disabled-1 border-color-0-var_lp_--border_cm__h_E4E4E4_rp_-_a__c_disabled-1 outline-color-0-var_lp_--primaryFocus_cm__h_9385D3_rp_-_a__c_focus-visible-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_10_pc__cm__h_FFF_s_90_pc__rp_-_a__c_hover-1 color-0-var_lp_--text_cm__h_F6F6F6_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 background-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 color-0-_h_373737-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 background-color-0-_h_47474A-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-transparent-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_674DC7_rp__s_10_pc__cm_var_lp_--inputBackground_cm__h_2E2E2E_rp__s_90_pc__rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 outline-color-0-var_lp_--primaryFocus_cm__h_927CE4_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_focus-visible-1 background-0-var_lp_--inputBackground_cm__h_2E2E2E_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a_-1 background-0-var_lp_--inputBackground_cm__h_FFF_rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm__h_E4E4E4_rp_--1 border-radius-0-10px--1 color-0-var_lp_--text_cm__h_272727_rp_--1 font-size-0-14px--1 font-size-4-15px--1 letter-spacing-0--_d_02em--1 letter-spacing-4--_d_03em--1 padding-right-0-16px--1 padding-left-0-16px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "> <div class="max-width-0-100_pc_--255 margin-right-0-auto--255 margin-left-0-auto--255 position-0-relative--255 width-0-fit-content--255"> <div class=" line-height-0-1_d_2--255 min-height-0-1_d_2em--255 "> @@ -30,7 +30,7 @@ exports[`Select should render with x and y properties 1`] = ` exports[`Select should render with overflow screen 1`] = ` "<div> - <div class="display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 "> + <div class=" display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 "> <button aria-label="Select toggle" type="button" aria-expanded="true" class="border-radius-0-8px--2 outline-0-2px_s_solid-_a__c_focus-visible-1 box-sizing-0-border-box--1 cursor-0-pointer--1 font-weight-0-700--1 outline-offset-0-2px--1 position-0-relative--1 transition-0-_d_25s--1 color-0-var_lp_--text_cm__h_272727_rp_-_a__c_active-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_20_pc__cm__h_FFF_s_80_pc__rp_-_a__c_active-1 border-0-1px_s_solid_s_var_lp_--primary_cm__h_8163E1_rp_-_a__c_active-1 color-0-_h_D6D7DE-_a__c_disabled-1 background-color-0-_h_F0F0F3-_a__c_disabled-1 cursor-0-not-allowed-_a__c_disabled-1 border-color-0-var_lp_--border_cm__h_E4E4E4_rp_-_a__c_disabled-1 outline-color-0-var_lp_--primaryFocus_cm__h_9385D3_rp_-_a__c_focus-visible-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_10_pc__cm__h_FFF_s_90_pc__rp_-_a__c_hover-1 color-0-var_lp_--text_cm__h_F6F6F6_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 background-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 color-0-_h_373737-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 background-color-0-_h_47474A-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-transparent-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_674DC7_rp__s_10_pc__cm_var_lp_--inputBackground_cm__h_2E2E2E_rp__s_90_pc__rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 outline-color-0-var_lp_--primaryFocus_cm__h_927CE4_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_focus-visible-1 background-0-var_lp_--inputBackground_cm__h_2E2E2E_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a_-1 background-0-var_lp_--inputBackground_cm__h_FFF_rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm__h_E4E4E4_rp_--1 border-radius-0-10px--1 color-0-var_lp_--text_cm__h_272727_rp_--1 font-size-0-14px--1 font-size-4-15px--1 letter-spacing-0--_d_02em--1 letter-spacing-4--_d_03em--1 padding-right-0-16px--1 padding-left-0-16px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "> <div class="max-width-0-100_pc_--255 margin-right-0-auto--255 margin-left-0-auto--255 position-0-relative--255 width-0-fit-content--255"> <div class=" line-height-0-1_d_2--255 min-height-0-1_d_2em--255 "> @@ -38,23 +38,23 @@ exports[`Select should render with overflow screen 1`] = ` </div> </div> </button> - <div aria-label="Select container" class="display-0-flex--0 flex-direction-0-column--0 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E4E4E4_cm__h_434343_rp__rp_--1 border-radius-0-8px--1 bottom-0--4px--1 box-shadow-0-0_s_2px_s_2px_s_0_s_var_lp_--base10_cm_light-dark_lp__h_0000001A_cm__h_FFFFFF1A_rp__rp_--1 height-0-fit-content--1 width-0-fit-content--1 gap-0-6px--1 min-width-0-232px--1 padding-0-10px--1 position-0-fixed--1 user-select-0-none--1 z-index-0-1--1 " style="top: 10px; left: 0px; bottom: 778px;"> - <div aria-label="Select option" data-value="Option 1" class="display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 color-0-var_lp_--selectDisabled_cm_light-dark_lp__h_C4C5D1_cm__h_45464D_rp__rp_--1 cursor-0-default--1 font-weight-0-400--1 gap-0-0--1 "> + <div aria-label="Select container" class=" display-0-flex--0 flex-direction-0-column--0 background-0-var_lp_--inputBg_cm_light-dark_lp__h_FFF_cm__h_2E2E2E_rp__rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm_light-dark_lp__h_E4E4E4_cm__h_434343_rp__rp_--1 border-radius-0-8px--1 bottom-0--4px--1 box-shadow-0-0_s_2px_s_2px_s_0_s_var_lp_--base10_cm_light-dark_lp__h_0000001A_cm__h_FFFFFF1A_rp__rp_--1 height-0-fit-content--1 width-0-fit-content--1 gap-0-6px--1 min-width-0-232px--1 padding-0-10px--1 position-0-fixed--1 user-select-0-none--1 z-index-0-1--1" style="top: 10px; left: 0px; bottom: 778px;"> + <div aria-label="Select option" data-value="Option 1" class=" display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 color-0-var_lp_--selectDisabled_cm_light-dark_lp__h_C4C5D1_cm__h_45464D_rp__rp_--1 cursor-0-default--1 font-weight-0-400--1 gap-0-0--1"> Option 1 </div> - <div aria-label="Select option" data-value="Option 2" class="display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 background-0-var_lp_--primaryBg_cm_light-dark_lp__h_F4F3FA_cm__h_F4F3FA0D_rp__rp_-_a__c_hover-1 color-0-var_lp_--title_cm_light-dark_lp__h_1A1A1A_cm__h_FAFAFA_rp__rp_--1 cursor-0-pointer--1 font-weight-0-400--1 gap-0-0--1 "> + <div aria-label="Select option" data-value="Option 2" class=" display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 background-0-var_lp_--primaryBg_cm_light-dark_lp__h_F4F3FA_cm__h_F4F3FA0D_rp__rp_-_a__c_hover-1 color-0-var_lp_--title_cm_light-dark_lp__h_1A1A1A_cm__h_FAFAFA_rp__rp_--1 cursor-0-pointer--1 font-weight-0-400--1 gap-0-0--1"> Option 2 </div> - <div class="background-0-var_lp_--border_cm_light-dark_lp__h_E4E4E4_cm__h_434343_rp__rp_--1 height-0-1px--1 width-0-100_pc_--1 "> + <div class=" background-0-var_lp_--border_cm_light-dark_lp__h_E4E4E4_cm__h_434343_rp__rp_--1 height-0-1px--1 width-0-100_pc_--1"> </div> - <div aria-label="Select option" data-value="Option 3" class="display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 background-0-var_lp_--primaryBg_cm_light-dark_lp__h_F4F3FA_cm__h_F4F3FA0D_rp__rp_-_a__c_hover-1 color-0-var_lp_--title_cm_light-dark_lp__h_1A1A1A_cm__h_FAFAFA_rp__rp_--1 cursor-0-pointer--1 font-weight-0-400--1 gap-0-0--1 "> + <div aria-label="Select option" data-value="Option 3" class=" display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 background-0-var_lp_--primaryBg_cm_light-dark_lp__h_F4F3FA_cm__h_F4F3FA0D_rp__rp_-_a__c_hover-1 color-0-var_lp_--title_cm_light-dark_lp__h_1A1A1A_cm__h_FAFAFA_rp__rp_--1 cursor-0-pointer--1 font-weight-0-400--1 gap-0-0--1"> Option 3 </div> - <div aria-label="Select option" data-value="Option 4" class="display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 color-0-var_lp_--selectDisabled_cm_light-dark_lp__h_C4C5D1_cm__h_45464D_rp__rp_--1 cursor-0-default--1 font-weight-0-400--1 gap-0-0--1 "> + <div aria-label="Select option" data-value="Option 4" class=" display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 color-0-var_lp_--selectDisabled_cm_light-dark_lp__h_C4C5D1_cm__h_45464D_rp__rp_--1 cursor-0-default--1 font-weight-0-400--1 gap-0-0--1"> Option 4 </div> - <div class="display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 "> - <div aria-label="Select toggle" aria-expanded="false" class="display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 background-0-var_lp_--primaryBg_cm_light-dark_lp__h_F4F3FA_cm__h_F4F3FA0D_rp__rp_-_a__c_hover-1 color-0-var_lp_--title_cm_light-dark_lp__h_1A1A1A_cm__h_FAFAFA_rp__rp_--1 cursor-0-pointer--1 font-weight-0-400--1 gap-0-6px--1 "> + <div class=" display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 "> + <div aria-label="Select toggle" aria-expanded="false" class=" display-0-flex--0 align-items-0-center--1 border-radius-0-6px--1 height-0-40px--1 padding-right-0-10px--1 padding-left-0-10px--1 transition-0-background-color_s__d_1s_s_ease-in-out--1 background-0-var_lp_--primaryBg_cm_light-dark_lp__h_F4F3FA_cm__h_F4F3FA0D_rp__rp_-_a__c_hover-1 color-0-var_lp_--title_cm_light-dark_lp__h_1A1A1A_cm__h_FAFAFA_rp__rp_--1 cursor-0-pointer--1 font-weight-0-400--1 gap-0-6px--1"> <div class="display-0-flex--0 align-items-0-center--255 justify-content-0-space-between--255 width-0-100_pc_--255"> Option 5 <svg class="color-0-_h_8B8E9C--1" fill="none" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"> @@ -71,7 +71,7 @@ exports[`Select should render with overflow screen 1`] = ` exports[`Select should render with typography prop 1`] = ` "<div> - <div class="display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 typo-body1 "> + <div class=" display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-_a_-1 box-sizing-0-border-box-_a__s__st_-1 typo-body1"> <button aria-label="Select toggle" type="button" aria-expanded="false" class="border-radius-0-8px--2 outline-0-2px_s_solid-_a__c_focus-visible-1 box-sizing-0-border-box--1 cursor-0-pointer--1 font-weight-0-700--1 outline-offset-0-2px--1 position-0-relative--1 transition-0-_d_25s--1 color-0-var_lp_--text_cm__h_272727_rp_-_a__c_active-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_20_pc__cm__h_FFF_s_80_pc__rp_-_a__c_active-1 border-0-1px_s_solid_s_var_lp_--primary_cm__h_8163E1_rp_-_a__c_active-1 color-0-_h_D6D7DE-_a__c_disabled-1 background-color-0-_h_F0F0F3-_a__c_disabled-1 cursor-0-not-allowed-_a__c_disabled-1 border-color-0-var_lp_--border_cm__h_E4E4E4_rp_-_a__c_disabled-1 outline-color-0-var_lp_--primaryFocus_cm__h_9385D3_rp_-_a__c_focus-visible-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_10_pc__cm__h_FFF_s_90_pc__rp_-_a__c_hover-1 color-0-var_lp_--text_cm__h_F6F6F6_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 background-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 color-0-_h_373737-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 background-color-0-_h_47474A-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-transparent-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_674DC7_rp__s_10_pc__cm_var_lp_--inputBackground_cm__h_2E2E2E_rp__s_90_pc__rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 outline-color-0-var_lp_--primaryFocus_cm__h_927CE4_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_focus-visible-1 background-0-var_lp_--inputBackground_cm__h_2E2E2E_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a_-1 background-0-var_lp_--inputBackground_cm__h_FFF_rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm__h_E4E4E4_rp_--1 border-radius-0-10px--1 color-0-var_lp_--text_cm__h_272727_rp_--1 font-size-0-14px--1 font-size-4-15px--1 letter-spacing-0--_d_02em--1 letter-spacing-4--_d_03em--1 padding-right-0-16px--1 padding-left-0-16px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "> <div class="max-width-0-100_pc_--255 margin-right-0-auto--255 margin-left-0-auto--255 position-0-relative--255 width-0-fit-content--255"> <div class=" line-height-0-1_d_2--255 min-height-0-1_d_2em--255 "> diff --git a/packages/components/src/components/Stepper/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Stepper/__tests__/__snapshots__/index.browser.test.tsx.snap index 96dfe014b..f4614a079 100644 --- a/packages/components/src/components/Stepper/__tests__/__snapshots__/index.browser.test.tsx.snap +++ b/packages/components/src/components/Stepper/__tests__/__snapshots__/index.browser.test.tsx.snap @@ -2,7 +2,7 @@ exports[`Stepper should render 1`] = ` "<div> - <div class="display-0-flex--0 align-items-0-center--1 gap-0-8px--1 "> + <div class=" display-0-flex--0 align-items-0-center--1 gap-0-8px--1"> <button aria-disabled="true" aria-label="Decrease button" disabled="" type="button" class="padding-0-0--255 height-0-28px--255 width-0-28px--255 border-radius-0-4px--255 outline-0-2px_s_solid-_a__c_focus-visible-1 box-sizing-0-border-box--1 cursor-0-pointer--1 font-weight-0-700--1 outline-offset-0-2px--1 position-0-relative--1 transition-0-_d_25s--1 color-0-var_lp_--text_cm__h_272727_rp_-_a__c_active-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_20_pc__cm__h_FFF_s_80_pc__rp_-_a__c_active-1 border-0-1px_s_solid_s_var_lp_--primary_cm__h_8163E1_rp_-_a__c_active-1 color-0-_h_D6D7DE-_a__c_disabled-1 background-color-0-_h_F0F0F3-_a__c_disabled-1 cursor-0-not-allowed-_a__c_disabled-1 border-color-0-var_lp_--border_cm__h_E4E4E4_rp_-_a__c_disabled-1 outline-color-0-var_lp_--primaryFocus_cm__h_9385D3_rp_-_a__c_focus-visible-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_8163E1_rp__s_10_pc__cm__h_FFF_s_90_pc__rp_-_a__c_hover-1 color-0-var_lp_--text_cm__h_F6F6F6_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 background-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_active-1 color-0-_h_373737-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 background-color-0-_h_47474A-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-transparent-_c_root_lb_data-theme_eq_dark_rb__s__a__c_disabled-1 border-color-0-var_lp_--primary_cm__h_8163E1_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 background-0-color-mix_lp_in_s_srgb_cm_var_lp_--primary_cm__h_674DC7_rp__s_10_pc__cm_var_lp_--inputBackground_cm__h_2E2E2E_rp__s_90_pc__rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_hover-1 outline-color-0-var_lp_--primaryFocus_cm__h_927CE4_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a__c_focus-visible-1 background-0-var_lp_--inputBackground_cm__h_2E2E2E_rp_-_c_root_lb_data-theme_eq_dark_rb__s__a_-1 background-0-var_lp_--inputBackground_cm__h_FFF_rp_--1 border-0-1px_s_solid_s_var_lp_--border_cm__h_E4E4E4_rp_--1 border-radius-0-10px--1 color-0-var_lp_--text_cm__h_272727_rp_--1 font-size-0-14px--1 font-size-4-15px--1 letter-spacing-0--_d_02em--1 letter-spacing-4--_d_03em--1 padding-right-0-16px--1 padding-left-0-16px--1 padding-bottom-0-10px--1 padding-top-0-10px--1 "> <div class="max-width-0-100_pc_--255 margin-right-0-auto--255 margin-left-0-auto--255 position-0-relative--255 width-0-fit-content--255"> <div class=" line-height-0-1_d_2--255 min-height-0-1_d_2em--255 "> diff --git a/packages/react/src/utils/__tests__/fixtures/styled-transient-props.jsx b/packages/react/src/utils/__tests__/fixtures/styled-transient-props.jsx new file mode 100644 index 000000000..4abfbeae7 --- /dev/null +++ b/packages/react/src/utils/__tests__/fixtures/styled-transient-props.jsx @@ -0,0 +1,72 @@ +import { createRequire } from 'node:module' +import process from 'node:process' + +import { styled } from '@devup-ui/react' + +// Bun's isolated install lets a package resolve only what it declares, and +// `react-dom` is a peer of the DOM preload's `@testing-library/react` +const domPreload = createRequire(import.meta.url).resolve('bun-test-env-dom') +const testingLib = createRequire(domPreload).resolve('@testing-library/react') +const server = createRequire(testingLib)('react-dom/server') + +const recorded = [] + +function Recorder(props) { + recorded.push(props) + return null +} + +/** + * Server-renders `Component` with `props` spread at runtime by a caller the + * build cannot tie to it, like one in another module, giving the markup, what + * React wrote to `console.error` and the props `Recorder` received. React + * warns about an invalid attribute name once per process, so each tag + * scenario uses its own `$` names. + */ +function renderWith(Component, props) { + const warnings = [] + const { error } = console + console.error = (...args) => { + warnings.push(args.join(' ')) + } + recorded.length = 0 + try { + return { + html: server.renderToStaticMarkup(<Component {...props} />), + received: [...recorded], + warnings, + } + } finally { + console.error = error + } +} + +const ordinary = { 'data-testid': 'target', id: 'target', title: 'Target' } +const transient = { $: 'bare', $id: 'transient-id', $other: 'other' } + +const Title = styled.h1` + color: ${(p) => p.$tone}; +` +const Wrapped = styled(Recorder)` + color: red; +` +const Forwarding = styled.h1.withConfig({ shouldForwardProp: () => true })` + color: red; +` +const Attributed = styled.h1.attrs({ $fromAttrs: 'attrs' })` + color: red; +` +const Heading = styled.h1` + color: red; +` + +// What each scenario observed, for the test that runs this script +process.stdout.write( + JSON.stringify({ + tag: renderWith(Title, { ...ordinary, ...transient, $tone: 'teal' }), + component: renderWith(Wrapped, { ...ordinary, ...transient }), + forwarding: renderWith(Forwarding, { ...ordinary, $forced: 'forced' }), + attrs: renderWith(Attributed, ordinary), + as: renderWith(Heading, { ...ordinary, ...transient, as: Recorder }), + }), +) diff --git a/packages/react/src/utils/__tests__/styled-transient-props.test.jsx b/packages/react/src/utils/__tests__/styled-transient-props.test.jsx new file mode 100644 index 000000000..fdf7dba00 --- /dev/null +++ b/packages/react/src/utils/__tests__/styled-transient-props.test.jsx @@ -0,0 +1,80 @@ +import { spawnSync } from 'node:child_process' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +import { describe, expect, it } from 'bun:test' + +/** The ordinary props the fixture spreads beside its `$` props */ +const ordinary = { 'data-testid': 'target', id: 'target', title: 'Target' } + +const root = new URL('../../../../../', import.meta.url) +const fixture = new URL('fixtures/styled-transient-props.jsx', import.meta.url) + +// The fixture renders in a fresh Bun process: in the suite's process, earlier +// test files leave `console.error` replaced by a mock that keeps their calls. +// A script gets no test preload, so the Devup UI plugin is preloaded here. +const child = spawnSync( + process.execPath, + [ + '--preload', + fileURLToPath(new URL('packages/bun-plugin/src/index.ts', root)), + fileURLToPath(fixture), + ], + { cwd: fileURLToPath(root), encoding: 'utf8' }, +) + +/** What the fixture observed in `scenario`, once its process exited cleanly */ +function observation(scenario) { + expect({ status: child.status, stderr: child.stderr }).toEqual({ + status: 0, + stderr: '', + }) + return JSON.parse(child.stdout)[scenario] +} + +function transientKeys(props) { + return Object.keys(props).filter((key) => key.startsWith('$')) +} + +describe('styled transient props', () => { + it('keeps unread $ props of a runtime spread off the tag while its styles read $tone into a CSS variable', () => { + const { html, warnings } = observation('tag') + + expect(warnings).toEqual([]) + expect(html).not.toMatch(/ \$[^\s=]*=/) + expect(html).toContain(' data-testid="target"') + expect(html).toContain(' id="target"') + expect(html).toContain(' title="Target"') + expect(html).toMatch(/ style="--[^"]*:teal"/) + }) + + it('passes a component target the ordinary props of a runtime spread but no $ props', () => { + const { received } = observation('component') + + expect(received).toEqual([expect.objectContaining(ordinary)]) + expect(received.map(transientKeys)).toEqual([[]]) + }) + + it('keeps $ props off the tag even when shouldForwardProp forwards every prop', () => { + const { html, warnings } = observation('forwarding') + + expect(warnings).toEqual([]) + expect(html).not.toMatch(/ \$[^\s=]*=/) + expect(html).toContain(' id="target"') + }) + + it('keeps $ props that attrs create off the tag', () => { + const { html, warnings } = observation('attrs') + + expect(warnings).toEqual([]) + expect(html).not.toMatch(/ \$[^\s=]*=/) + expect(html).toContain(' id="target"') + }) + + it('passes the component `as` names the ordinary props of a runtime spread but no $ props', () => { + const { received } = observation('as') + + expect(received).toEqual([expect.objectContaining(ordinary)]) + expect(received.map(transientKeys)).toEqual([[]]) + }) +})