Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
9e29139
fix(extractor): let a later css() part replace an earlier one's decla…
owjs3901 Oct 1, 2026
337ddeb
test(extractor): cover known style composition branches
owjs3901 Oct 1, 2026
024a24c
chore: satisfy clippy assert_is_empty lint on Rust 1.99
owjs3901 Oct 1, 2026
71585a9
test(extractor): cover false ?? composition branch
owjs3901 Oct 1, 2026
52f4518
refactor(extractor): split coalesce check so composition lines are me…
owjs3901 Oct 1, 2026
1cedf9f
fix(extractor): compose styled extensions, merge attrs and let later …
owjs3901 Oct 1, 2026
01adab9
test(extractor): restore runtime-value composition test and cover sty…
owjs3901 Oct 1, 2026
53f740b
fix(extractor): render styled as targets and compile withComponent
owjs3901 Oct 1, 2026
b4b42cf
fix(extractor): keep read, $ and theme props off styled tags and eval…
owjs3901 Oct 1, 2026
e2cfa67
test(extractor): cover shouldForwardProp forms and element prop filte…
owjs3901 Oct 1, 2026
ce9f390
fix(extractor): forward refs from generated styled components
owjs3901 Oct 2, 2026
95d35c8
fix(extractor): compile Emotion's css prop at build time
owjs3901 Oct 2, 2026
e017367
test(extractor): cover css props on styled components with responsive…
owjs3901 Oct 2, 2026
0faa84e
fix(plugin-utils): compile the css prop where tsconfig builds JSX wit…
owjs3901 Oct 2, 2026
1a6a31e
fix(extractor): compile Emotion's ClassNames at build time
owjs3901 Oct 2, 2026
4f88bd4
test(extractor): cover ClassNames child functions taking rest or nest…
owjs3901 Oct 2, 2026
d44950f
fix(extractor): compile Emotion component selectors at build time
owjs3901 Oct 3, 2026
4988282
fix(eslint-plugin): check styleOrder only where the build reads it
owjs3901 Oct 3, 2026
fa67537
fix(eslint-plugin): write the changepack without a BOM
owjs3901 Oct 3, 2026
8d1f976
feat(eslint-plugin): follow the import aliases and style positions th…
owjs3901 Oct 3, 2026
32c3007
docs(eslint-plugin): document the rules and what counts as a style
owjs3901 Oct 3, 2026
d0028a8
fix(react): make the public and compat types accept what the build co…
owjs3901 Oct 3, 2026
87777b7
Merge remote-tracking branch 'origin/fix/eslint-alias-rules' into owj…
owjs3901 Oct 5, 2026
11e3f1c
fix(eslint-plugin): keep composition arrays out of responsive rules
owjs3901 Oct 5, 2026
72781be
fix(react): align StyleX and supported CSS property contracts
owjs3901 Oct 5, 2026
d7e7c63
fix(extractor): enforce the shared static styleOrder contract
owjs3901 Oct 5, 2026
809f8e5
fix(config): validate custom shorthand targets atomically
owjs3901 Oct 5, 2026
8931734
fix(plugins): validate shorthand options before plugin setup
owjs3901 Oct 5, 2026
d69f519
fix(extractor): validate order metadata in composed css props
owjs3901 Oct 5, 2026
af1177d
merge: update PR #706 with main ESLint checks
owjs3901 Oct 8, 2026
944e723
merge: update PR #706 with 9a953607
owjs3901 Oct 8, 2026
5a70d2a
merge: update PR #706 with db631c6c
owjs3901 Oct 8, 2026
6f801d0
Merge main a935315c prerequisite into fix/eslint-alias-rules
owjs3901 Oct 8, 2026
a20489a
Merge main b711ed1f into fix/eslint-alias-rules
owjs3901 Oct 8, 2026
36e5842
Merge main 9a953607 into fix/eslint-alias-rules
owjs3901 Oct 8, 2026
ee369ff
Merge main db631c6c into fix/eslint-alias-rules
owjs3901 Oct 8, 2026
9e59c52
Merge main 11790bef into fix/eslint-alias-rules
owjs3901 Oct 8, 2026
30247ba
merge: update PR #706 with main
owjs3901 Oct 8, 2026
c7ea90a
merge: update PR #707 with main
owjs3901 Oct 8, 2026
4669b17
merge: update PR #710 with PR #707
owjs3901 Oct 8, 2026
9e4d28a
merge: update PR #711 with PR #710
owjs3901 Oct 8, 2026
ae283a7
merge: update PR #722 with PR #711
owjs3901 Oct 8, 2026
12328b9
merge: update PR #727 with PR #722
owjs3901 Oct 8, 2026
9f94748
merge: update PR #728 with main
owjs3901 Oct 8, 2026
7a8d96b
merge: update PR #729 with PR #728
owjs3901 Oct 8, 2026
a355130
merge: update PR #730 with PR #729
owjs3901 Oct 8, 2026
0dcb717
merge: update PR #738 with PR #730
owjs3901 Oct 8, 2026
4473674
merge: update PR #763 with b711ed1f
owjs3901 Oct 8, 2026
b6545c2
merge: update PR #763 with main
owjs3901 Oct 8, 2026
8b5b4d9
merge: update PR #763 with main
owjs3901 Oct 8, 2026
4825e4b
fix(packages): restore main release baseline in types and lint stack
owjs3901 Oct 8, 2026
baabf69
fix(vite-plugin): restore main RSC CSS behavior and regression gate
owjs3901 Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_compose_known_styles.json
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_compose_styled.json
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_dead_css_property_types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/react/package.json": "Patch"
},
"note": "Reject the 18 unprefixed legacy flexbox and scroll-snap properties rejected by the compiler in component and StyleX property types. Keep vendor-prefixed compatibility properties, stroke-color and ime-mode.",
"date": "2026-10-01T00:00:00.000Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_emotion_class_names.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 <ClassNames> now compiles at build time with no runtime: the element becomes what its child function ({ css, cx, theme }) => ... renders, each css and cx call becomes the classes it composes (later styles replace earlier ones; css reads strings as CSS text and its tagged templates split around mixins; cx keeps unknown classes, reads { name: condition } objects as conditional classes and composes nested css and cx calls), and theme.a.b in their values becomes var(--a-b). Numbers in css rules are px lengths, as Emotion reads them, and top-level constants are inlined. New build errors, each at file:line:col: a <ClassNames> without exactly one child function taking { css, cx, theme } that gives what it renders at once (attributes, other children, rest or default parameters, async or generator functions, several statements); reading the theme other than as theme.a.b in a css value; using css or cx other than by calling them; a cx object with a spread, getter or method; parts css and cx cannot compose (runtime calls, spreads, a condition choosing between styles and classes). @devup-ui/react/compat exports ClassNames with the ClassNamesContent and ClassNamesArg types, and the @emotion/react declarations include them",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_emotion_component_selectors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Emotion and styled-components component selectors now compile at build time: a styled component the file defines that another style selects (${Child} { ... } in CSS text, [Child] or [`&:hover ${Child}`] as a rule key) gets a short marker class, which only such components get, and the selector reads it as .marker; a component extending it, or an element inlining it for its css prop, keeps the marker. Selectors written without & now nest under the component as stylis nests them (ul { ... } is & ul, a selector list nests each selector, :hover applies to the component itself) instead of becoming global rules, and rule-object keys starting with ., #, [, *, >, + or ~ are read as nested selectors instead of properties",
"date": "2026-10-01T00:00:00.000Z"
}
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"
}
14 changes: 14 additions & 0 deletions .changepacks/changepack_log_emotion_jsx_import_source.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/plugin-utils/package.json": "Patch",
"packages/react/package.json": "Patch",
"packages/vite-plugin/package.json": "Patch",
"packages/next-plugin/package.json": "Patch",
"packages/webpack-plugin/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch",
"packages/bun-plugin/package.json": "Patch"
},
"note": "A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_eslint_alias_rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/eslint-plugin/package.json": "Patch"
},
"note": "ESLint rules now check exactly what the build reads. The array, typography, media and styleOrder rules and css-utils-literal-only follow the import aliases the build compiles by default (@emotion/react, @emotion/styled, styled-components, @vanilla-extract/css) and the rules of styled (styled.div({...}), styled(\"div\", {...}), styled(Base)({...}, {...}); not .attrs()), createGlobalStyle({...}) and the styles of Emotion Global, and check and fix a module-level const object that only styles read. Vanilla-extract stylesheets (.css.ts/.css.js) are no longer read as Devup UI. New rule no-runtime-read reports css/keyframes/styled/Box read as a value (\"`X` is read at runtime, where it does not exist\"), and css-utils-literal-only reports globals that do not exist where a .css.ts is evaluated (window, document, process, ...). No new build errors.",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_eslint_composition_arrays.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/eslint-plugin/package.json": "Patch"
},
"note": "Do not treat composition arrays as responsive property values in the duplicate, trailing-null or single-value array rules. Preserve composition arrays and their nested responsive property diagnostics.",
"date": "2026-10-01T00:00:00.000Z"
}
13 changes: 13 additions & 0 deletions .changepacks/changepack_log_shorthand_target_validation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/plugin-utils/package.json": "Patch",
"packages/vite-plugin/package.json": "Patch",
"packages/next-plugin/package.json": "Patch",
"packages/webpack-plugin/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch",
"packages/bun-plugin/package.json": "Patch"
},
"note": "Custom shorthand targets are constrained to supported camelCase/kebab-case CSS properties, built-in aliases and valid custom properties. All five build adapters normalize options at creation before registration or extraction, including Next.js production cache reuse. Unsupported targets fail registration atomically with the original shorthand name, target and zero-based index; previously registered shorthands remain unchanged. New devup-ui option errors expose structured logical shorthands paths without inventing source coordinates. Eighteen dead unprefixed property names are rejected.",
"date": "2026-10-05T00:00:00.000Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_style_order_contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/eslint-plugin/package.json": "Patch"
},
"note": "Validate explicit static styleOrder as an integer from 1 to 254 or its canonical decimal digit string, consistently in extraction and lint, including composed Emotion css props. Keep order metadata out of library px conversion. Invalid static values and conditional branches now produce located build errors instead of silently changing cascade order.",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_forward_ref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Styled components the build generates are wrapped in React's forwardRef, so a ref passed to them reaches the element they render on React 18 as well as React 19; the file imports forwardRef from react when it defines one",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_props.json
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_targets.json
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_stylex_public_contracts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/react/package.json": "Patch"
},
"note": "Type every compiled StyleX API with opaque styles, dynamic function arguments, conditional variables, partial themes and static null/string theme contract placeholders. Unsupported runtime shapes are rejected by TypeScript; no new build errors in this change.",
"date": "2026-10-01T00:00:00.000Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_types_match_build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"packages/react/package.json": "Patch",
"packages/plugin-utils/package.json": "Patch"
},
"note": "Types now accept what the build compiles and nothing it rejects. css() takes classes, style objects, arrays and falsy parts, and the css/keyframes/globalCss tags take the static interpolations the build evaluates. styled gets .attrs(), .withConfig() and .withComponent(), and its result renders another tag. styleVars takes numbers; styleOrder is 1 to 254; ResponsiveValue is exported for hand-written custom shorthands. The compat declarations name what Emotion, styled-components and vanilla-extract give (SerializedStyles, Keyframes, RuleSet, an augmentable Theme and DefaultTheme, CSSObject and the other styled-components types) and follow the vanilla-extract APIs the evaluator registers (style arrays, the styleVariants mapper, createGlobalTheme, createGlobalThemeContract; globalKeyframes, composeStyles and generateIdentifier are gone). DevupTheme takes shadow as devup.json documents it. No new build errors.",
"date": "2026-10-01T00:00:00.000Z"
}
59 changes: 50 additions & 9 deletions bindings/devup-ui-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,16 +610,17 @@ pub fn register_theme(theme_object: JsValue) -> Result<(), JsValue> {
}

/// Internal function to register custom style-property shorthands.
pub fn register_shorthands_internal(shorthands: BTreeMap<String, Vec<String>>) {
css::set_custom_shorthands(shorthands);
pub fn register_shorthands_internal(
shorthands: BTreeMap<String, Vec<String>>,
) -> Result<(), css::shorthand::InvalidShorthandTarget> {
css::set_custom_shorthands(shorthands)
}

#[wasm_bindgen(js_name = "registerShorthands")]
#[cfg(not(tarpaulin_include))]
pub fn register_shorthands(shorthands: JsValue) -> Result<(), JsValue> {
let shorthands = serde_wasm_bindgen::from_value(shorthands).map_err(js_error)?;
register_shorthands_internal(shorthands);
Ok(())
register_shorthands_internal(shorthands).map_err(js_error)
}

#[wasm_bindgen(js_name = "getDefaultTheme")]
Expand Down Expand Up @@ -1401,7 +1402,7 @@ mod tests {
#[test]
#[serial]
fn test_get_theme_interface() {
register_shorthands_internal(BTreeMap::new());
register_shorthands_internal(BTreeMap::new()).unwrap();
let sheet = StyleSheet::default();
assert_eq!(
sheet.create_interface(
Expand All @@ -1423,7 +1424,8 @@ mod tests {
register_shorthands_internal(BTreeMap::from([(
"insetX".to_string(),
vec!["left".to_string(), "right".to_string()],
)]));
)]))
.unwrap();
assert_eq!(
get_theme_interface(
"package",
Expand All @@ -1437,7 +1439,7 @@ mod tests {
);

// test wrong case
register_shorthands_internal(BTreeMap::new());
register_shorthands_internal(BTreeMap::new()).unwrap();
let mut sheet = StyleSheet::default();
let mut theme = Theme::default();
let mut color_theme = ColorTheme::default();
Expand Down Expand Up @@ -2021,7 +2023,7 @@ mod tests {
)]);

register_theme_internal(theme);
register_shorthands_internal(shorthands);
register_shorthands_internal(shorthands).unwrap();

// Verify the theme was registered
let default_theme = GLOBAL_STYLE_SHEET.lock().unwrap().theme.get_default_theme();
Expand All @@ -2030,6 +2032,45 @@ mod tests {
css::disassemble_property("insetX").collect::<Vec<_>>(),
["left", "right"]
);
register_shorthands_internal(BTreeMap::new());
register_shorthands_internal(BTreeMap::new()).unwrap();
}

#[test]
#[serial]
fn test_register_shorthands_internal_rejects_targets_atomically() {
register_shorthands_internal(BTreeMap::from([("old".into(), vec!["py".into()])])).unwrap();
let error = register_shorthands_internal(BTreeMap::from([
("aValid".into(), vec!["width".into()]),
("bad".into(), vec!["height".into(), "widht".into()]),
]))
.unwrap_err();
assert_eq!(error.alias, "bad");
assert_eq!(error.target, "widht");
assert_eq!(error.index, 1);
assert_eq!(css::get_custom_shorthand_names(), ["old"]);
assert_eq!(
css::disassemble_property("old").collect::<Vec<_>>(),
["padding-top", "padding-bottom"]
);
register_shorthands_internal(BTreeMap::new()).unwrap();
}

#[test]
#[serial]
fn test_register_shorthands_internal_normalizes_vendor_targets() {
register_shorthands_internal(BTreeMap::from([(
"maskAlias".into(),
vec![
"WebkitMaskImage".into(),
"-webkit-mask-image".into(),
"--Mask".into(),
],
)]))
.unwrap();
assert_eq!(
css::disassemble_property("maskAlias").collect::<Vec<_>>(),
["-webkit-mask-image", "-webkit-mask-image", "--Mask"]
);
register_shorthands_internal(BTreeMap::new()).unwrap();
}
}
7 changes: 7 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,11 @@ export default [
ignores: ['**/*.md'],
},
...devupUi.configs.recommended,
// the snippets of this page are .css.ts stylesheets, which the build runs as they are
{
files: ['apps/landing/src/app/(detail)/docs/migration/vanilla-extract/**'],
rules: {
'@devup-ui/css-utils-literal-only': 'off',
},
},
]
Loading
Loading