Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_emotion_css_prop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/react/package.json": "Patch"
},
"note": "Emotion's css prop compiles at build time while @emotion/react is aliased: on tags and Devup UI components always, and on every element once the file imports @emotion/react or @emotion/styled or names @emotion/react in a @jsxImportSource pragma; jsx, jsxs and jsxDEV from @emotion/react/jsx-runtime (or jsx-dev-runtime) and jsx from @emotion/react compile it too, so libraries built with Emotion's JSX runtime do as well. The prop becomes the element's className, with CSS variables in its style for values only the runtime gives: arrays and conditions compose with a later part replacing what an earlier one sets, css() classes the file knows compose by their styles, strings and templates are CSS text, css`` mixins split the text around them, a function of the theme reads theme.a.b as var(--a-b), unitless numbers are px as in Emotion (constants included), and a className holding known css() classes overrides the prop as Emotion's registered classes do. A styled component the file defines renders its tag in the element's place when the prop overrides its styles and it renders a tag with no attrs or props read and the element has no spread, as or forwardedAs. Emotion's JSX runtime imports become react/jsx-runtime, the pragma names react, and jsx comes from @devup-ui/react/compat (React's createElement); @devup-ui/react/compat/css-prop types the prop on React.Attributes. New build errors, each with file:line:column and the code: a css prop part the build cannot read (a call, an element), a style object declared inside a function or with let, a binding only running the module gives or code changes, a theme function that does more than return rules or reads the theme other than as theme.a.b in a value, an interpolation CSS text cannot place or a mixin inside a nested rule, and a css prop overriding a styled component's styles where its tag cannot be rendered in place. Composing css() with a part that reads a known binding (such as a keyframes name) now reads its value",
"date": "2026-10-01T00:00:00.000Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_emotion_css_prop_contracts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/react/package.json": "Patch"
},
"note": "Recognize Emotion jsxImportSource eligibility with the same exact header rules used for rewriting, preserve authored first-use cascade-layer order while composing css props, retain exact computed theme keys, and keep Emotion numeric-length pixel semantics when constant rule arrays are inlined. Unitless values and zero retain their existing behavior. Every valid css-prop form remains supported; invalid empty JSX css expressions retain main's located syntax error. No new build-error category is introduced.",
"date": "2026-10-18T00:00:00.000Z"
}
5 changes: 3 additions & 2 deletions libs/extractor/src/build_time_values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ pub(crate) fn has_build_time_values(
option: &ExtractOption,
resolver: Option<&ModuleResolver>,
) -> bool {
let (code, _) = crate::import_alias_visit::transform_import_aliases_with_edits(
let aliased = crate::import_alias_visit::transform_import_aliases_with_edits(
code,
filename,
&option.package,
&option.import_aliases,
);
let allocator = Allocator::default();
let Some(mut program) = parse(&allocator, filename, &code) else {
let Some(mut program) = parse(&allocator, filename, &aliased.code) else {
return false;
};
let inlined = crate::imported_constants::inline_constants(
Expand All @@ -76,6 +76,7 @@ pub(crate) fn has_build_time_values(
filename,
option,
resolver,
aliased.css_prop,
);
let changes = crate::imported_constants::ChangeCheck::new(&program, filename, option, resolver);
!find(
Expand Down
42 changes: 42 additions & 0 deletions libs/extractor/src/composition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ impl<'a> Composition<'a> {
self.entries.push((key, choice));
}

/// A class applying after the composed parts, whose styles `values` the
/// build knows: what it sets replaces what they set
pub fn cover(&mut self, values: &[ExtractStyleValue]) {
let keys: Vec<CascadeKey> = values.iter().map(CascadeKey::of).collect();
self.entries.retain(|(key, _)| !keys.contains(key));
}

/// The composed styles, for class names and the stylesheet
#[must_use]
pub fn into_props(self) -> Vec<ExtractStyleProp<'a>> {
Expand Down Expand Up @@ -261,6 +268,41 @@ pub fn set_prop_order(prop: &mut ExtractStyleProp<'_>, order: u8) {
}
}

/// Whether `later` may set what `earlier` sets, so the order of their classes
/// decides which applies: a style whose key the build cannot tell may
pub fn overlaps(earlier: &[ExtractStyleProp<'_>], later: &[ExtractStyleProp<'_>]) -> bool {
match (keys(earlier), keys(later)) {
(Some(earlier), Some(later)) => later.iter().any(|key| earlier.contains(key)),
_ => true,
}
}

/// The keys `props` set, `None` when a style chosen at runtime has none;
/// classes only the runtime gives compose at runtime, so they set none here
fn keys(props: &[ExtractStyleProp<'_>]) -> Option<Vec<CascadeKey>> {
let mut keys = Vec::new();
for prop in props {
match prop {
ExtractStyleProp::Static(value) => keys.push(CascadeKey::of(value)),
ExtractStyleProp::StaticArray(props) => keys.extend(self::keys(props)?),
ExtractStyleProp::Conditional {
consequent,
alternate,
..
} => {
for side in [consequent, alternate].into_iter().flatten() {
keys.extend(self::keys(std::slice::from_ref(side.as_ref()))?);
}
}
ExtractStyleProp::Expression { .. } | ExtractStyleProp::Unreadable { .. } => {}
ExtractStyleProp::Enum { .. } | ExtractStyleProp::MemberExpression { .. } => {
return None;
}
}
}
Some(keys)
}

/// Whether every style `prop` holds has a key, so a condition around it can be
/// applied key by key
fn keyed(prop: &ExtractStyleProp<'_>) -> bool {
Expand Down
Loading
Loading