assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({
base: {
color: 'red',
@@ -15458,7 +15481,7 @@ const styles = stylex.create({
display: stylex.firstThatWorks('grid', 'flex'),
},
},
-});"#,
+});",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15479,7 +15502,7 @@ const styles = stylex.create({
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({
base: {
width: {
@@ -15487,7 +15510,7 @@ const styles = stylex.create({
'@media (min-width: 768px)': stylex.types.length('50%'),
},
},
-});"#,
+});",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15508,13 +15531,13 @@ const styles = stylex.create({
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const base = stylex.create({
root: { color: 'red', fontSize: '16px' },
});
const composed = stylex.create({
fancy: { ...stylex.include(base.root), backgroundColor: 'blue' },
-});"#,
+});",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15535,13 +15558,13 @@ const composed = stylex.create({
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import { create, include } from '@stylexjs/stylex';
+ r"import { create, include } from '@stylexjs/stylex';
const base = create({
root: { color: 'red' },
});
const composed = create({
fancy: { ...include(base.root), padding: '8px' },
-});"#,
+});",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15562,14 +15585,14 @@ const composed = create({
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const base = stylex.create({
root: { color: 'red' },
});
const composed = stylex.create({
fancy: { ...stylex.include(base.root), backgroundColor: 'blue' },
});
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15594,8 +15617,8 @@ const el =
;"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import { create, firstThatWorks } from '@stylexjs/stylex';
-const styles = create({ base: { color: firstThatWorks('red', 'blue') } });"#,
+ r"import { create, firstThatWorks } from '@stylexjs/stylex';
+const styles = create({ base: { color: firstThatWorks('red', 'blue') } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15616,8 +15639,8 @@ const styles = create({ base: { color: firstThatWorks('red', 'blue') } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: { fontSize: 16, lineHeight: 1.5 } });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: { fontSize: 16, lineHeight: 1.5 } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15638,8 +15661,8 @@ const styles = stylex.create({ base: { fontSize: 16, lineHeight: 1.5 } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: { zIndex: stylex.firstThatWorks(10, 20) } });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: { zIndex: stylex.firstThatWorks(10, 20) } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15660,8 +15683,8 @@ const styles = stylex.create({ base: { zIndex: stylex.firstThatWorks(10, 20) } }
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: { fontSize: stylex.types.length(16) } });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: { fontSize: stylex.types.length(16) } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15682,8 +15705,8 @@ const styles = stylex.create({ base: { fontSize: stylex.types.length(16) } });"#
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: { fontSize: stylex.types.length(someVar) } });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: { fontSize: stylex.types.length(someVar) } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15704,8 +15727,8 @@ const styles = stylex.create({ base: { fontSize: stylex.types.length(someVar) }
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: { ':hover': 'invalid' } });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: { ':hover': 'invalid' } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15726,8 +15749,8 @@ const styles = stylex.create({ base: { ':hover': 'invalid' } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: 'not-an-object', active: { color: 'blue' } });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: 'not-an-object', active: { color: 'blue' } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15748,9 +15771,9 @@ const styles = stylex.create({ base: 'not-an-object', active: { color: 'blue' }
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const other = {};
-const styles = stylex.create({ ...other, base: { color: 'red' } });"#,
+const styles = stylex.create({ ...other, base: { color: 'red' } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15771,9 +15794,9 @@ const styles = stylex.create({ ...other, base: { color: 'red' } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const key = 'base';
-const styles = stylex.create({ [key]: { color: 'red' } });"#,
+const styles = stylex.create({ [key]: { color: 'red' } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15794,8 +15817,8 @@ const styles = stylex.create({ [key]: { color: 'red' } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: () => ({ color: 'red' }) });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: () => ({ color: 'red' }) });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15816,8 +15839,8 @@ const styles = stylex.create({ base: () => ({ color: 'red' }) });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: (x) => { return { color: x }; } });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: (x) => { return { color: x }; } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15838,8 +15861,8 @@ const styles = stylex.create({ base: (x) => { return { color: x }; } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: (x) => ({ fontSize: 16, height: x }) });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: (x) => ({ fontSize: 16, height: x }) });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15860,9 +15883,9 @@ const styles = stylex.create({ base: (x) => ({ fontSize: 16, height: x }) });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const fadeIn = stylex.keyframes({ from: { opacity: '0' }, to: { opacity: '1' } });
-const styles = stylex.create({ base: (dur) => ({ animationName: fadeIn, animationDuration: dur }) });"#,
+const styles = stylex.create({ base: (dur) => ({ animationName: fadeIn, animationDuration: dur }) });",
ExtractOption { package: "@devup-ui/react".to_string(), css_dir: "@devup-ui/react".to_string(), single_css: true, import_main_css: false, import_aliases: HashMap::new() },
)
.unwrap()
@@ -15877,9 +15900,9 @@ const styles = stylex.create({ base: (dur) => ({ animationName: fadeIn, animatio
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({ active: { color: 'red' } });
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15900,9 +15923,9 @@ const el =
;"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({ fallback: { color: 'gray' } });
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15923,9 +15946,9 @@ const el =
;"#
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({ base: { color: 'red' } });
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15946,9 +15969,9 @@ const el =
;"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({ base: { color: 'red' } });
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15969,9 +15992,9 @@ const el =
;"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({ base: { color: 'red' } });
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -15992,9 +16015,9 @@ const el =
;"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({ base: (x) => ({ color: x }) });
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16015,9 +16038,9 @@ const el =
;"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const base = stylex.create({ dynamic: (x) => ({ color: x, fontSize: '14px' }) });
-const composed = stylex.create({ fancy: { ...stylex.include(base.dynamic), backgroundColor: 'blue' } });"#,
+const composed = stylex.create({ fancy: { ...stylex.include(base.dynamic), backgroundColor: 'blue' } });",
ExtractOption { package: "@devup-ui/react".to_string(), css_dir: "@devup-ui/react".to_string(), single_css: true, import_main_css: false, import_aliases: HashMap::new() },
)
.unwrap()
@@ -16032,8 +16055,8 @@ const composed = stylex.create({ fancy: { ...stylex.include(base.dynamic), backg
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import { create, unknownFunction } from '@stylexjs/stylex';
-const styles = create({ base: { color: 'red' } });"#,
+ r"import { create, unknownFunction } from '@stylexjs/stylex';
+const styles = create({ base: { color: 'red' } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16050,7 +16073,7 @@ const styles = create({ base: { color: 'red' } });"#,
// Coverage tests: extract_style_from_stylex.rs
// ==========================================
- /// Line 24: Non-object argument to stylex.create()
+ /// Line 24: Non-object argument to `stylex.create()`
#[test]
#[serial]
fn test_stylex_create_non_object_arg() {
@@ -16082,9 +16105,9 @@ const styles = stylex.create("not-object");"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const other = { fontSize: '14px' };
-const styles = stylex.create({ base: { ':hover': { ...other, [Symbol()]: 'val', color: 'red' } } });"#,
+const styles = stylex.create({ base: { ':hover': { ...other, [Symbol()]: 'val', color: 'red' } } });",
ExtractOption { package: "@devup-ui/react".to_string(), css_dir: "@devup-ui/react".to_string(), single_css: true, import_main_css: false, import_aliases: HashMap::new() },
)
.unwrap()
@@ -16100,9 +16123,9 @@ const styles = stylex.create({ base: { ':hover': { ...other, [Symbol()]: 'val',
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const other = { fontSize: '14px' };
-const styles = stylex.create({ base: (x) => ({ ...other, [Symbol()]: x, color: 'red' }) });"#,
+const styles = stylex.create({ base: (x) => ({ ...other, [Symbol()]: x, color: 'red' }) });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16124,9 +16147,9 @@ const styles = stylex.create({ base: (x) => ({ ...other, [Symbol()]: x, color: '
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const someVar = getSomething();
-const styles = stylex.create({ base: (x) => ({ color: x, fontSize: someVar }) });"#,
+const styles = stylex.create({ base: (x) => ({ color: x, fontSize: someVar }) });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16143,7 +16166,7 @@ const styles = stylex.create({ base: (x) => ({ color: x, fontSize: someVar }) })
// Coverage tests: stylex.rs
// ==========================================
- /// Line 48: false from is_include_call_static — spread with non-include call
+ /// Line 48: false from `is_include_call_static` — spread with non-include call
#[test]
#[serial]
fn test_stylex_spread_non_include_call() {
@@ -16152,9 +16175,9 @@ const styles = stylex.create({ base: (x) => ({ color: x, fontSize: someVar }) })
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
function notInclude() { return {}; }
-const styles = stylex.create({ base: { ...notInclude(), color: 'red' } });"#,
+const styles = stylex.create({ base: { ...notInclude(), color: 'red' } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16167,7 +16190,7 @@ const styles = stylex.create({ base: { ...notInclude(), color: 'red' } });"#,
));
}
- /// Lines 66-67: Named import types.X()
+ /// Lines 66-67: Named import `types.X()`
#[test]
#[serial]
fn test_stylex_named_import_types() {
@@ -16176,8 +16199,8 @@ const styles = stylex.create({ base: { ...notInclude(), color: 'red' } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import { create, types } from '@stylexjs/stylex';
-const styles = create({ base: { width: types.length('100px') } });"#,
+ r"import { create, types } from '@stylexjs/stylex';
+const styles = create({ base: { width: types.length('100px') } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16190,7 +16213,7 @@ const styles = create({ base: { width: types.length('100px') } });"#,
));
}
- /// Line 70: false from is_types_call — call expression value that's not types or firstThatWorks
+ /// Line 70: false from `is_types_call` — call expression value that's not types or firstThatWorks
#[test]
#[serial]
fn test_stylex_non_types_non_ftw_call() {
@@ -16199,9 +16222,9 @@ const styles = create({ base: { width: types.length('100px') } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
function someFunc() { return 'red'; }
-const styles = stylex.create({ base: { color: someFunc() } });"#,
+const styles = stylex.create({ base: { color: someFunc() } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16223,9 +16246,9 @@ const styles = stylex.create({ base: { color: someFunc() } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const someVar = getSomething();
-const styles = stylex.create({ base: { fontSize: { default: stylex.types.length(someVar) } } });"#,
+const styles = stylex.create({ base: { fontSize: { default: stylex.types.length(someVar) } } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16247,8 +16270,8 @@ const styles = stylex.create({ base: { fontSize: { default: stylex.types.length(
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: { opacity: { default: [1, 2] } } });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: { opacity: { default: [1, 2] } } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16270,9 +16293,9 @@ const styles = stylex.create({ base: { opacity: { default: [1, 2] } } });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const spreadObj = { ':hover': 'blue' };
-const styles = stylex.create({ base: { color: { ...spreadObj, default: 'red' } } });"#,
+const styles = stylex.create({ base: { color: { ...spreadObj, default: 'red' } } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16294,9 +16317,9 @@ const styles = stylex.create({ base: { color: { ...spreadObj, default: 'red' } }
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const key = ':hover';
-const styles = stylex.create({ base: { color: { [key]: 'blue', default: 'red' } } });"#,
+const styles = stylex.create({ base: { color: { [key]: 'blue', default: 'red' } } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16322,9 +16345,9 @@ const styles = stylex.create({ base: { color: { [key]: 'blue', default: 'red' }
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({ empty: {} });
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16346,9 +16369,9 @@ const el =
;"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({ base: { color: 'red' } });
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16361,7 +16384,7 @@ const el =
;"#,
));
}
- /// Line 208: None from LogicalExpression when right side can't resolve
+ /// Line 208: None from `LogicalExpression` when right side can't resolve
#[test]
#[serial]
fn test_stylex_props_logical_unresolvable_right() {
@@ -16370,9 +16393,9 @@ const el =
;"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const styles = stylex.create({ base: { color: 'red' } });
-const el =
;"#,
+const el =
;",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16385,7 +16408,7 @@ const el =
;"#,
));
}
- /// Line 332: include with empty class_name_str — include a namespace that has no properties
+ /// Line 332: include with empty `class_name_str` — include a namespace that has no properties
#[test]
#[serial]
fn test_stylex_include_empty_namespace() {
@@ -16394,9 +16417,9 @@ const el =
;"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const base = stylex.create({ empty: {} });
-const composed = stylex.create({ test: { ...stylex.include(base.empty), color: 'red' } });"#,
+const composed = stylex.create({ test: { ...stylex.include(base.empty), color: 'red' } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16410,7 +16433,7 @@ const composed = stylex.create({ test: { ...stylex.include(base.empty), color: '
}
/// Dynamic namespace with bare expression body (not object): (x) => x
- /// Covers: extract_style_from_stylex.rs ObjectExpression else branch
+ /// Covers: `extract_style_from_stylex.rs` `ObjectExpression` else branch
#[test]
#[serial]
fn test_stylex_dynamic_bare_expression_body() {
@@ -16419,8 +16442,8 @@ const composed = stylex.create({ test: { ...stylex.include(base.empty), color: '
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: (x) => x });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: (x) => x });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16434,7 +16457,7 @@ const styles = stylex.create({ base: (x) => x });"#,
}
/// Dynamic namespace with parenthesized non-object body: (x) => (x)
- /// Covers: extract_style_from_stylex.rs ParenthesizedExpression unwrap + ObjectExpression else
+ /// Covers: `extract_style_from_stylex.rs` `ParenthesizedExpression` unwrap + `ObjectExpression` else
#[test]
#[serial]
fn test_stylex_dynamic_paren_non_object_body() {
@@ -16443,8 +16466,8 @@ const styles = stylex.create({ base: (x) => x });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
-const styles = stylex.create({ base: (x) => (x) });"#,
+ r"import stylex from '@stylexjs/stylex';
+const styles = stylex.create({ base: (x) => (x) });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16457,8 +16480,8 @@ const styles = stylex.create({ base: (x) => (x) });"#,
));
}
- /// Include-only namespace with no own styles — class_name_str starts empty
- /// Covers: visit.rs line 332 (class_name_str = included_class when empty)
+ /// Include-only namespace with no own styles — `class_name_str` starts empty
+ /// Covers: visit.rs line 332 (`class_name_str` = `included_class` when empty)
#[test]
#[serial]
fn test_stylex_include_only_no_own_styles() {
@@ -16467,9 +16490,9 @@ const styles = stylex.create({ base: (x) => (x) });"#,
assert_debug_snapshot!(ToBTreeSet::from(
extract(
"test.tsx",
- r#"import stylex from '@stylexjs/stylex';
+ r"import stylex from '@stylexjs/stylex';
const base = stylex.create({ root: { color: 'red' } });
-const composed = stylex.create({ combined: { ...stylex.include(base.root) } });"#,
+const composed = stylex.create({ combined: { ...stylex.include(base.root) } });",
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
@@ -16793,52 +16816,52 @@ const composed = stylex.create({ combined: { ...stylex.include(base.root) } });"
let cases: &[(&str, &str)] = &[
(
"identifier",
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
const hoverStyle = { opacity: 1 };
export const A = () =>
;
-"#,
+",
),
(
"call expression",
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
declare const getHover: () => object;
export const A = () =>
;
-"#,
+",
),
(
"member expression",
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
declare const styles: { hover: object };
export const A = () =>
;
-"#,
+",
),
(
"binary expression",
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
declare const a: any; declare const b: any;
export const A = () =>
;
-"#,
+",
),
(
"template literal",
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
declare const x: string;
export const A = () =>
;
-"#,
+",
),
(
"unary expression",
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
declare const v: any;
export const A = () =>
;
-"#,
+",
),
(
"computed member expression (array index)",
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
declare const arr: any[];
export const A = () =>
;
-"#,
+",
),
];
@@ -16993,20 +17016,20 @@ export const A = () =>
;
// pseudo-selector attribute.
for src in [
// BinaryExpression
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
declare const a: any; declare const b: any;
export const A = () =>
;
-"#,
+",
// StaticMemberExpression
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
declare const t: { hover: object };
export const A = () =>
;
-"#,
+",
// CallExpression
- r#"import {Box} from '@devup-ui/react'
+ r"import {Box} from '@devup-ui/react'
declare const fn: () => object;
export const A = () =>
;
-"#,
+",
] {
reset_class_map();
reset_file_map();
diff --git a/libs/extractor/src/prop_modify_utils.rs b/libs/extractor/src/prop_modify_utils.rs
index da1bbba2..d648c1f9 100644
--- a/libs/extractor/src/prop_modify_utils.rs
+++ b/libs/extractor/src/prop_modify_utils.rs
@@ -46,7 +46,7 @@ pub(crate) fn combine_conditional_class_name<'a>(
/// modify object props
/// Returns extracted Tailwind styles from static className strings
-/// `conditional_branch`: If Some, contains (condition, alternate_styles, alternate_style_order)
+/// `conditional_branch`: If Some, contains (condition, `alternate_styles`, `alternate_style_order`)
/// for generating a conditional className expression: `condition ? consequent_class : alternate_class`
#[allow(clippy::too_many_arguments)]
pub fn modify_prop_object<'a>(
@@ -164,7 +164,7 @@ pub fn modify_prop_object<'a>(
}
/// modify JSX props
/// Returns extracted Tailwind styles from static className strings
-/// `conditional_branch`: If Some, contains (condition, alternate_styles, alternate_style_order)
+/// `conditional_branch`: If Some, contains (condition, `alternate_styles`, `alternate_style_order`)
/// for generating a conditional className expression: `condition ? consequent_class : alternate_class`
#[allow(clippy::too_many_arguments)]
pub fn modify_props<'a>(
@@ -317,42 +317,32 @@ pub fn get_class_name_expression<'a>(
};
// Merge class names: [tailwind/original class names] + [devup-ui component styles]
- let expression = merge_string_expressions(
- ast_builder,
- [
- class_name_to_use,
- gen_class_names(ast_builder, styles, style_order, filename),
- ]
- .into_iter()
- .flatten()
- .chain(if class_name_prop.is_some() {
- vec![]
- } else {
- spread_props
- .iter()
- .map(|ex| {
- convert_class_name(
- ast_builder,
- &Expression::StaticMemberExpression(
- ast_builder.alloc_static_member_expression(
- SPAN,
- ex.clone_in(ast_builder.allocator),
- ast_builder.identifier_name(SPAN, ast_builder.str("className")),
- true,
- ),
- ),
- )
- })
- .collect::
>()
- })
- .collect::>()
- .as_slice(),
- );
+ let mut class_expressions = Vec::with_capacity(2 + spread_props.len());
+ 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().map(|ex| {
+ convert_class_name(
+ ast_builder,
+ &Expression::StaticMemberExpression(ast_builder.alloc_static_member_expression(
+ SPAN,
+ ex.clone_in(ast_builder.allocator),
+ ast_builder.identifier_name(SPAN, ast_builder.str("className")),
+ true,
+ )),
+ )
+ }));
+ }
+ let expression = merge_string_expressions(ast_builder, &class_expressions);
(expression, tailwind_styles)
}
-/// Apply style_order to all ExtractStyleValue items
+/// Apply `style_order` to all `ExtractStyleValue` items
fn apply_style_order_to_styles(styles: &mut [ExtractStyleValue], style_order: Option) {
if let Some(order) = style_order {
for style in styles.iter_mut() {
@@ -541,14 +531,12 @@ fn rebuild_expression_with_mapping<'a>(
/// Extract all class name strings from a template literal, including from conditional expressions
fn extract_all_classes_from_template_literal(template: &oxc_ast::ast::TemplateLiteral) -> String {
- let mut classes = Vec::new();
+ let mut classes = String::new();
// Extract from quasis (static parts of template literal)
for quasi in &template.quasis {
let raw = quasi.value.raw.as_str();
- if !raw.trim().is_empty() {
- classes.push(raw.trim().to_string());
- }
+ push_class_segment(&mut classes, raw.trim());
}
// Extract from expressions (dynamic parts)
@@ -556,18 +544,26 @@ fn extract_all_classes_from_template_literal(template: &oxc_ast::ast::TemplateLi
extract_classes_from_expression(expr, &mut classes);
}
- classes.join(" ")
+ classes
+}
+
+fn push_class_segment(classes: &mut String, value: &str) {
+ if value.is_empty() {
+ return;
+ }
+ if !classes.is_empty() {
+ classes.push(' ');
+ }
+ classes.push_str(value);
}
/// Recursively extract class name strings from an expression
-fn extract_classes_from_expression(expr: &Expression, classes: &mut Vec) {
+fn extract_classes_from_expression(expr: &Expression, classes: &mut String) {
match expr {
// Direct string literal: 'text-red-500'
Expression::StringLiteral(lit) => {
let value = lit.value.as_str().trim();
- if !value.is_empty() {
- classes.push(value.to_string());
- }
+ push_class_segment(classes, value);
}
// Ternary/conditional: cond ? 'text-red' : 'text-blue'
Expression::ConditionalExpression(cond) => {
@@ -586,9 +582,7 @@ fn extract_classes_from_expression(expr: &Expression, classes: &mut Vec)
// Template literal inside expression
Expression::TemplateLiteral(inner_template) => {
let inner_classes = extract_all_classes_from_template_literal(inner_template);
- if !inner_classes.is_empty() {
- classes.push(inner_classes);
- }
+ push_class_segment(classes, &inner_classes);
}
// Other expressions (variables, function calls, etc.) - skip, can't extract statically
_ => {}
@@ -603,35 +597,31 @@ pub fn get_style_expression<'a>(
spread_props: &[Expression<'a>],
filename: Option<&str>,
) -> Option> {
- merge_object_expressions(
- ast_builder,
- [
- gen_styles(ast_builder, styles, filename),
- style_vars
- .as_ref()
- .map(|style_vars| convert_style_vars(ast_builder, style_vars)),
- style_prop.clone_in(ast_builder.allocator),
- ]
- .into_iter()
- .flatten()
- .chain(if style_prop.is_some() {
- vec![]
- } else {
- spread_props
- .iter()
- .map(|ex| {
- Expression::StaticMemberExpression(ast_builder.alloc_static_member_expression(
- SPAN,
- ex.clone_in(ast_builder.allocator),
- ast_builder.identifier_name(SPAN, ast_builder.str("style")),
- true,
- ))
- })
- .collect::>()
- })
- .collect::>()
- .as_slice(),
- )
+ let mut style_expressions = Vec::with_capacity(3 + spread_props.len());
+ if let Some(style) = gen_styles(ast_builder, styles, filename) {
+ style_expressions.push(style);
+ }
+ if let Some(style_vars) = style_vars
+ .as_ref()
+ .map(|style_vars| convert_style_vars(ast_builder, style_vars))
+ {
+ style_expressions.push(style_vars);
+ }
+ 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().map(|ex| {
+ Expression::StaticMemberExpression(ast_builder.alloc_static_member_expression(
+ SPAN,
+ ex.clone_in(ast_builder.allocator),
+ ast_builder.identifier_name(SPAN, ast_builder.str("style")),
+ true,
+ ))
+ }));
+ }
+
+ merge_object_expressions(ast_builder, &style_expressions)
}
fn merge_string_expressions<'a>(
@@ -641,19 +631,19 @@ fn merge_string_expressions<'a>(
if expressions.is_empty() {
return None;
}
- if expressions.len() == 1
+ if let [expression] = expressions
&& !matches!(
- expressions.first().unwrap(),
+ expression,
Expression::StringLiteral(_) | Expression::TemplateLiteral(_)
)
{
- return Some(expressions.first().unwrap().clone_in(ast_builder.allocator));
+ return Some(expression.clone_in(ast_builder.allocator));
}
let mut string_literals: std::vec::Vec = vec![];
let mut other_expressions = vec![];
let mut prev_str = String::new();
- for ex in expressions.iter() {
+ for ex in expressions {
if let Expression::StringLiteral(literal) = ex {
let target_prev = prev_str.trim();
let target = literal.value.trim();
@@ -676,7 +666,7 @@ fn merge_string_expressions<'a>(
""
},
target_prev,
- if !target_prev.is_empty() { " " } else { "" },
+ if target_prev.is_empty() { "" } else { " " },
target,
if !target.is_empty() && !target.ends_with("typo-") {
" "
@@ -693,13 +683,13 @@ fn merge_string_expressions<'a>(
let target_prev = prev_str.trim();
string_literals.push(format!(
"{}{}{}",
- if !other_expressions.is_empty() {
- " "
- } else {
+ if other_expressions.is_empty() {
""
+ } else {
+ " "
},
target_prev,
- if !target_prev.is_empty() { " " } else { "" }
+ if target_prev.is_empty() { "" } else { " " }
));
other_expressions.push(ex.clone_in(ast_builder.allocator));
prev_str = String::new();
@@ -707,7 +697,7 @@ fn merge_string_expressions<'a>(
}
string_literals.push(format!(
"{}{}",
- if !prev_str.trim().is_empty() { " " } else { "" },
+ if prev_str.trim().is_empty() { "" } else { " " },
prev_str.trim(),
));
if other_expressions.is_empty() {
diff --git a/libs/extractor/src/snapshots/extractor__tests__custom_selector-4.snap b/libs/extractor/src/snapshots/extractor__tests__custom_selector-4.snap
new file mode 100644
index 00000000..05da9455
--- /dev/null
+++ b/libs/extractor/src/snapshots/extractor__tests__custom_selector-4.snap
@@ -0,0 +1,23 @@
+---
+source: libs/extractor/src/lib.rs
+expression: "ToBTreeSet::from(extract(\"test.jsx\",\nr#\"import {Box} from '@devup-ui/core'\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: {
+ Static(
+ ExtractStaticStyle {
+ property: "color",
+ value: "blue",
+ level: 0,
+ selector: Some(
+ Selector(
+ "&:focus",
+ ),
+ ),
+ style_order: None,
+ layer: None,
+ },
+ ),
+ },
+ code: "import \"@devup-ui/core/devup-ui.css\";\n;\n",
+}
diff --git a/libs/extractor/src/stylex.rs b/libs/extractor/src/stylex.rs
index 05e1a930..8aec88a7 100644
--- a/libs/extractor/src/stylex.rs
+++ b/libs/extractor/src/stylex.rs
@@ -3,8 +3,8 @@ use oxc_ast::ast::{Expression, ObjectPropertyKind};
use crate::utils::{get_string_by_literal_expression, get_string_by_property_key};
-/// Which StyleX function a named import refers to
-#[derive(Debug, Clone, PartialEq)]
+/// Which `StyleX` function a named import refers to
+#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StylexFunction {
Create,
Props,
@@ -15,7 +15,7 @@ pub enum StylexFunction {
Include,
}
-/// Check if a call expression is stylex.firstThatWorks() or named firstThatWorks().
+/// Check if a call expression is `stylex.firstThatWorks()` or named `firstThatWorks()`.
pub fn is_first_that_works_call(callee: &Expression) -> bool {
// stylex.firstThatWorks(...)
if let Expression::StaticMemberExpression(member) = callee
@@ -32,7 +32,7 @@ pub fn is_first_that_works_call(callee: &Expression) -> bool {
false
}
-/// Check if a call expression is stylex.include() or named include().
+/// Check if a call expression is `stylex.include()` or named `include()`.
/// This is a static check that does NOT require access to the visitor.
pub fn is_include_call_static(callee: &Expression) -> bool {
if let Expression::StaticMemberExpression(member) = callee
@@ -48,14 +48,14 @@ pub fn is_include_call_static(callee: &Expression) -> bool {
false
}
-/// A reference to a stylex.include(base.member) call found inside stylex.create().
+/// A reference to a stylex.include(base.member) call found inside `stylex.create()`.
#[derive(Debug, Clone)]
pub struct StylexIncludeRef {
pub var_name: String,
pub member_name: String,
}
-/// Check if a call expression is stylex.types.X() or types.X() (type wrapper).
+/// Check if a call expression is `stylex.types.X()` or `types.X()` (type wrapper).
pub fn is_types_call(callee: &Expression) -> bool {
if let Expression::StaticMemberExpression(member) = callee {
// stylex.types.X(...)
@@ -71,7 +71,7 @@ pub fn is_types_call(callee: &Expression) -> bool {
}
/// Convert camelCase CSS property name to kebab-case.
-/// StyleX uses standard CSS properties only — NO devup-ui shorthand expansion.
+/// `StyleX` uses standard CSS properties only — NO devup-ui shorthand expansion.
pub fn normalize_stylex_property(name: &str) -> String {
css::utils::to_kebab_case(name)
}
@@ -79,7 +79,7 @@ pub fn normalize_stylex_property(name: &str) -> String {
/// Intermediate selector parts collected during recursion.
#[derive(Debug, Clone)]
pub enum SelectorPart {
- /// Pseudo-class or pseudo-element, e.g. ":hover", "::placeholder"
+ /// Pseudo-class or pseudo-element, e.g. ":hover", "`::placeholder`"
Pseudo(String),
/// At-rule condition, e.g. @media (max-width: 600px)
AtRule { kind: AtRuleKind, query: String },
@@ -94,16 +94,16 @@ pub struct DecomposedStyle {
pub selector: Option,
}
-/// Information about a dynamic StyleX namespace (arrow function in stylex.create())
+/// Information about a dynamic `StyleX` namespace (arrow function in `stylex.create()`)
#[derive(Debug, Clone)]
pub struct StylexDynamicInfo {
/// Combined class name string for all properties (static + dynamic)
pub class_name: String,
- /// Maps (param_index, css_variable_name) for each dynamic property
+ /// Maps (`param_index`, `css_variable_name`) for each dynamic property
pub css_vars: Vec<(usize, String)>,
}
-/// A StyleX namespace entry — either static or dynamic (arrow function)
+/// A `StyleX` namespace entry — either static or dynamic (arrow function)
#[derive(Debug, Clone)]
pub enum StylexNamespaceValue {
/// Static namespace: just a className string
@@ -112,14 +112,14 @@ pub enum StylexNamespaceValue {
Dynamic(StylexDynamicInfo),
}
-/// Decompose a StyleX value-level condition object into flat (css_value, selector) tuples.
+/// Decompose a `StyleX` value-level condition object into flat (`css_value`, selector) tuples.
///
-/// StyleX allows values to be objects with condition keys:
+/// `StyleX` allows values to be objects with condition keys:
/// ```js
/// { color: { default: 'red', ':hover': 'blue', '@media (max-width:600px)': 'green' } }
/// ```
///
-/// This recursively walks the value tree and returns flat tuples of (value_or_none, selector).
+/// This recursively walks the value tree and returns flat tuples of (`value_or_none`, selector).
/// `None` value means null/no CSS emitted (but tracked for atomic override).
pub fn decompose_value_conditions(
css_property: &str,
@@ -185,7 +185,7 @@ pub fn decompose_value_conditions(
let mut results = vec![];
- for prop in obj.properties.iter() {
+ for prop in &obj.properties {
let ObjectPropertyKind::ObjectProperty(prop) = prop else {
continue;
};
@@ -249,15 +249,14 @@ fn compose_selectors(parts: &[SelectorPart]) -> Option {
Some(format!("&{}", pseudos.join("")))
};
- if at_rules.is_empty() {
- pseudo_str.map(StyleSelector::Selector)
- } else {
- let (kind, query) = at_rules.last().expect("at_rules is non-empty");
+ if let Some((kind, query)) = at_rules.last() {
Some(StyleSelector::At {
kind: *kind,
query: query.to_string(),
selector: pseudo_str,
})
+ } else {
+ pseudo_str.map(StyleSelector::Selector)
}
}
@@ -276,6 +275,7 @@ fn parse_at_rule_key(key: &str) -> Option<(AtRuleKind, String)> {
}
#[cfg(test)]
+#[allow(clippy::expect_used)]
mod tests {
use super::*;
diff --git a/libs/extractor/src/tailwind.rs b/libs/extractor/src/tailwind.rs
index 72b72a50..1779ed5a 100644
--- a/libs/extractor/src/tailwind.rs
+++ b/libs/extractor/src/tailwind.rs
@@ -1,7 +1,7 @@
//! Tailwind CSS class parser for devup-ui extraction
//!
//! This module parses Tailwind CSS class strings and converts them to
-//! ExtractStyleValue objects for integration with the devup-ui extraction system.
+//! `ExtractStyleValue` objects for integration with the devup-ui extraction system.
// The nested if-let pattern is intentional for readability in parsing code.
// Using if-let chains would make the code harder to read and modify.
@@ -98,7 +98,7 @@ pub enum TailwindVariant {
}
impl TailwindVariant {
- /// Convert variant to StyleSelector
+ /// Convert variant to `StyleSelector`
pub fn to_selector(self) -> StyleSelector {
match self {
TailwindVariant::Hover => StyleSelector::Selector("&:hover".to_string()),
@@ -291,7 +291,7 @@ impl TailwindVariant {
}
/// Parsed Tailwind class with all components
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TailwindClass {
/// Responsive level (0=base, 1=sm, 2=md, 3=lg, 4=xl, 5=2xl)
pub responsive: u8,
@@ -306,7 +306,7 @@ pub struct TailwindClass {
}
impl TailwindClass {
- /// Convert to ExtractStaticStyle
+ /// Convert to `ExtractStaticStyle`
pub fn to_static_style(&self) -> ExtractStaticStyle {
// For transform property, negative is already incorporated into the value
// (e.g., translateX(-1rem)), so don't add prefix again
@@ -346,7 +346,7 @@ impl TailwindClass {
} else {
// Combine selectors
selector_str =
- format!("{}{}", selector_str.replace(" &", ""), s.replace("&", ""));
+ format!("{}{}", selector_str.replace(" &", ""), s.replace('&', ""));
if !selector_str.contains(" &") && !selector_str.ends_with(" &") {
selector_str.push_str(" &");
}
@@ -853,8 +853,7 @@ static EASE_SCALE: phf::Map<&'static str, &'static str> = phf_map! {
/// Check if a string contains Tailwind classes
pub fn has_tailwind_classes(class_str: &str) -> bool {
// Simple heuristic: if it looks like a Tailwind class pattern
- let parts: Vec<&str> = class_str.split_whitespace().collect();
- for part in parts {
+ for part in class_str.split_whitespace() {
if is_likely_tailwind_class(part) {
return true;
}
@@ -1188,8 +1187,8 @@ fn is_valid_tailwind_value(value: &str) -> bool {
}
// Numeric values (including decimals like 0.5, 1.5)
- let first_char = value.chars().next().unwrap();
- if first_char.is_ascii_digit() {
+ // Safe: `value.is_empty()` returns early above, so `value` has at least one char.
+ if value.starts_with(|c: char| c.is_ascii_digit()) {
return true;
}
@@ -1217,11 +1216,10 @@ fn is_valid_tailwind_value(value: &str) -> bool {
}
// Fraction values (1/2, 1/3, 2/3, etc.)
- if value.contains('/') {
- let parts: Vec<&str> = value.split('/').collect();
- if parts.len() == 2
- && parts[0].chars().all(|c| c.is_ascii_digit())
- && parts[1].chars().all(|c| c.is_ascii_digit())
+ if let Some((numerator, denominator)) = value.split_once('/') {
+ if !denominator.contains('/')
+ && numerator.chars().all(|c| c.is_ascii_digit())
+ && denominator.chars().all(|c| c.is_ascii_digit())
{
return true;
}
@@ -1230,11 +1228,12 @@ fn is_valid_tailwind_value(value: &str) -> bool {
false
}
-/// Parse a className string into a list of ExtractStyleValue
+/// Parse a className string into a list of `ExtractStyleValue`
pub fn parse_tailwind_to_styles(class_str: &str, filename: Option<&str>) -> Vec {
- let mut styles = Vec::new();
+ let classes = class_str.split_whitespace();
+ let mut styles = Vec::with_capacity(classes.clone().count());
- for class in class_str.split_whitespace() {
+ for class in classes {
if let Some(parsed) = parse_single_class(class) {
let static_style = parsed.to_static_style();
styles.push(ExtractStyleValue::Static(static_style));
@@ -1429,36 +1428,36 @@ fn parse_arbitrary_value(class: &str) -> Option<(String, String)> {
"leading-" => Some(("line-height".to_string(), value)),
"duration-" => Some(("transition-duration".to_string(), value)),
"delay-" => Some(("transition-delay".to_string(), value)),
- "scale-" => Some(("transform".to_string(), format!("scale({})", value))),
- "rotate-" => Some(("transform".to_string(), format!("rotate({})", value))),
- "translate-x-" => Some(("transform".to_string(), format!("translateX({})", value))),
- "translate-y-" => Some(("transform".to_string(), format!("translateY({})", value))),
- "skew-x-" => Some(("transform".to_string(), format!("skewX({})", value))),
- "skew-y-" => Some(("transform".to_string(), format!("skewY({})", value))),
+ "scale-" => Some(("transform".to_string(), format!("scale({value})"))),
+ "rotate-" => Some(("transform".to_string(), format!("rotate({value})"))),
+ "translate-x-" => Some(("transform".to_string(), format!("translateX({value})"))),
+ "translate-y-" => Some(("transform".to_string(), format!("translateY({value})"))),
+ "skew-x-" => Some(("transform".to_string(), format!("skewX({value})"))),
+ "skew-y-" => Some(("transform".to_string(), format!("skewY({value})"))),
"aspect-" => Some(("aspect-ratio".to_string(), value)),
"columns-" => Some(("columns".to_string(), value)),
"grid-cols-" => Some((
"grid-template-columns".to_string(),
- format!("repeat({}, minmax(0, 1fr))", value),
+ format!("repeat({value}, minmax(0, 1fr))"),
)),
"grid-rows-" => Some((
"grid-template-rows".to_string(),
- format!("repeat({}, minmax(0, 1fr))", value),
+ format!("repeat({value}, minmax(0, 1fr))"),
)),
"col-span-" => Some((
"grid-column".to_string(),
- format!("span {} / span {}", value, value),
+ format!("span {value} / span {value}"),
)),
"row-span-" => Some((
"grid-row".to_string(),
- format!("span {} / span {}", value, value),
+ format!("span {value} / span {value}"),
)),
"basis-" => Some(("flex-basis".to_string(), value)),
- "blur-" => Some(("filter".to_string(), format!("blur({})", value))),
- "brightness-" => Some(("filter".to_string(), format!("brightness({})", value))),
- "contrast-" => Some(("filter".to_string(), format!("contrast({})", value))),
- "saturate-" => Some(("filter".to_string(), format!("saturate({})", value))),
- "backdrop-blur-" => Some(("backdrop-filter".to_string(), format!("blur({})", value))),
+ "blur-" => Some(("filter".to_string(), format!("blur({value})"))),
+ "brightness-" => Some(("filter".to_string(), format!("brightness({value})"))),
+ "contrast-" => Some(("filter".to_string(), format!("contrast({value})"))),
+ "saturate-" => Some(("filter".to_string(), format!("saturate({value})"))),
+ "backdrop-blur-" => Some(("backdrop-filter".to_string(), format!("blur({value})"))),
_ => None,
}
}
@@ -1868,7 +1867,7 @@ fn parse_flex_grid_utility(class: &str) -> Option<(String, String)> {
if let Ok(n) = rest.parse::() {
return Some((
"grid-template-columns".to_string(),
- format!("repeat({}, minmax(0, 1fr))", n),
+ format!("repeat({n}, minmax(0, 1fr))"),
));
}
}
@@ -1878,7 +1877,7 @@ fn parse_flex_grid_utility(class: &str) -> Option<(String, String)> {
if let Ok(n) = rest.parse::() {
return Some((
"grid-template-rows".to_string(),
- format!("repeat({}, minmax(0, 1fr))", n),
+ format!("repeat({n}, minmax(0, 1fr))"),
));
}
}
@@ -1886,10 +1885,7 @@ fn parse_flex_grid_utility(class: &str) -> Option<(String, String)> {
// Col span
if let Some(rest) = class.strip_prefix("col-span-") {
if let Ok(n) = rest.parse::() {
- return Some((
- "grid-column".to_string(),
- format!("span {} / span {}", n, n),
- ));
+ return Some(("grid-column".to_string(), format!("span {n} / span {n}")));
}
}
@@ -1906,7 +1902,7 @@ fn parse_flex_grid_utility(class: &str) -> Option<(String, String)> {
// Row span
if let Some(rest) = class.strip_prefix("row-span-") {
if let Ok(n) = rest.parse::() {
- return Some(("grid-row".to_string(), format!("span {} / span {}", n, n)));
+ return Some(("grid-row".to_string(), format!("span {n} / span {n}")));
}
}
@@ -2458,7 +2454,7 @@ fn parse_background_utility(class: &str) -> Option<(String, String)> {
};
return Some((
"background-image".to_string(),
- format!("linear-gradient({}, var(--tw-gradient-stops))", direction),
+ format!("linear-gradient({direction}, var(--tw-gradient-stops))"),
));
}
}
@@ -2765,7 +2761,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
"3xl" => "64px".to_string(),
_ => return None,
};
- return Some(("filter".to_string(), format!("blur({})", value)));
+ return Some(("filter".to_string(), format!("blur({value})")));
}
if class == "blur" {
return Some(("filter".to_string(), "blur(8px)".to_string()));
@@ -2787,7 +2783,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
"200" => "2".to_string(),
_ => return None,
};
- return Some(("filter".to_string(), format!("brightness({})", value)));
+ return Some(("filter".to_string(), format!("brightness({value})")));
}
// Contrast
@@ -2802,7 +2798,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
"200" => "2".to_string(),
_ => return None,
};
- return Some(("filter".to_string(), format!("contrast({})", value)));
+ return Some(("filter".to_string(), format!("contrast({value})")));
}
// Drop shadow
@@ -2845,7 +2841,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
"180" => "180deg".to_string(),
_ => return None,
};
- return Some(("filter".to_string(), format!("hue-rotate({})", value)));
+ return Some(("filter".to_string(), format!("hue-rotate({value})")));
}
// Invert
@@ -2866,7 +2862,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
"200" => "2".to_string(),
_ => return None,
};
- return Some(("filter".to_string(), format!("saturate({})", value)));
+ return Some(("filter".to_string(), format!("saturate({value})")));
}
// Sepia
@@ -2889,7 +2885,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
"3xl" => "64px".to_string(),
_ => return None,
};
- return Some(("backdrop-filter".to_string(), format!("blur({})", value)));
+ return Some(("backdrop-filter".to_string(), format!("blur({value})")));
}
if class == "backdrop-blur" {
return Some(("backdrop-filter".to_string(), "blur(8px)".to_string()));
@@ -2912,7 +2908,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
};
return Some((
"backdrop-filter".to_string(),
- format!("brightness({})", value),
+ format!("brightness({value})"),
));
}
@@ -2927,10 +2923,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
"200" => "2".to_string(),
_ => return None,
};
- return Some((
- "backdrop-filter".to_string(),
- format!("contrast({})", value),
- ));
+ return Some(("backdrop-filter".to_string(), format!("contrast({value})")));
}
if class == "backdrop-grayscale" {
@@ -2949,7 +2942,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
if let Some(rest) = class.strip_prefix("backdrop-opacity-") {
if let Some(&value) = OPACITY_SCALE.get(rest) {
- return Some(("backdrop-filter".to_string(), format!("opacity({})", value)));
+ return Some(("backdrop-filter".to_string(), format!("opacity({value})")));
}
}
@@ -2962,10 +2955,7 @@ fn parse_filter_utility(class: &str) -> Option<(String, String)> {
"200" => "2".to_string(),
_ => return None,
};
- return Some((
- "backdrop-filter".to_string(),
- format!("saturate({})", value),
- ));
+ return Some(("backdrop-filter".to_string(), format!("saturate({value})")));
}
if class == "backdrop-sepia" {
@@ -3048,15 +3038,15 @@ fn parse_transform_utility(class: &str, is_negative: bool) -> Option<(String, St
// Scale
if let Some(rest) = class.strip_prefix("scale-x-") {
let value = parse_scale_value(rest)?;
- return Some(("transform".to_string(), format!("scaleX({})", value)));
+ return Some(("transform".to_string(), format!("scaleX({value})")));
}
if let Some(rest) = class.strip_prefix("scale-y-") {
let value = parse_scale_value(rest)?;
- return Some(("transform".to_string(), format!("scaleY({})", value)));
+ return Some(("transform".to_string(), format!("scaleY({value})")));
}
if let Some(rest) = class.strip_prefix("scale-") {
let value = parse_scale_value(rest)?;
- return Some(("transform".to_string(), format!("scale({})", value)));
+ return Some(("transform".to_string(), format!("scale({value})")));
}
// Rotate
@@ -3076,7 +3066,7 @@ fn parse_transform_utility(class: &str, is_negative: bool) -> Option<(String, St
let neg_prefix = if is_negative { "-" } else { "" };
return Some((
"transform".to_string(),
- format!("rotate({}{})", neg_prefix, value),
+ format!("rotate({neg_prefix}{value})"),
));
}
@@ -3086,7 +3076,7 @@ fn parse_transform_utility(class: &str, is_negative: bool) -> Option<(String, St
let neg_prefix = if is_negative { "-" } else { "" };
return Some((
"transform".to_string(),
- format!("translateX({}{})", neg_prefix, value),
+ format!("translateX({neg_prefix}{value})"),
));
}
}
@@ -3095,7 +3085,7 @@ fn parse_transform_utility(class: &str, is_negative: bool) -> Option<(String, St
let neg_prefix = if is_negative { "-" } else { "" };
return Some((
"transform".to_string(),
- format!("translateY({}{})", neg_prefix, value),
+ format!("translateY({neg_prefix}{value})"),
));
}
}
@@ -3114,7 +3104,7 @@ fn parse_transform_utility(class: &str, is_negative: bool) -> Option<(String, St
let neg_prefix = if is_negative { "-" } else { "" };
return Some((
"transform".to_string(),
- format!("skewX({}{})", neg_prefix, value),
+ format!("skewX({neg_prefix}{value})"),
));
}
if let Some(rest) = class.strip_prefix("skew-y-") {
@@ -3130,7 +3120,7 @@ fn parse_transform_utility(class: &str, is_negative: bool) -> Option<(String, St
let neg_prefix = if is_negative { "-" } else { "" };
return Some((
"transform".to_string(),
- format!("skewY({}{})", neg_prefix, value),
+ format!("skewY({neg_prefix}{value})"),
));
}
@@ -3157,7 +3147,7 @@ fn parse_transform_utility(class: &str, is_negative: bool) -> Option<(String, St
/// Parse scale value (50 -> 0.5, 100 -> 1, 150 -> 1.5)
fn parse_scale_value(s: &str) -> Option {
let n: u32 = s.parse().ok()?;
- Some(n as f64 / 100.0)
+ Some(f64::from(n) / 100.0)
}
/// Parse interactivity utilities (cursor, pointer-events, resize, etc.)
@@ -3342,6 +3332,7 @@ fn parse_accessibility_utility(class: &str) -> Option<(String, String)> {
}
#[cfg(test)]
+#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use css::class_map::reset_class_map;
@@ -4004,7 +3995,7 @@ mod tests {
assert_eq!(kind, css::style_selector::AtRuleKind::Media);
assert_eq!(query, expected_query);
} else {
- panic!("Expected At selector for {:?}", variant);
+ panic!("Expected At selector for {variant:?}");
}
}
diff --git a/libs/extractor/src/util_type.rs b/libs/extractor/src/util_type.rs
index cdfde6e6..316a207a 100644
--- a/libs/extractor/src/util_type.rs
+++ b/libs/extractor/src/util_type.rs
@@ -1,4 +1,4 @@
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Eq)]
pub enum UtilType {
Css,
GlobalCss,
@@ -37,7 +37,7 @@ mod tests {
#[case("globalCss".to_string(), Ok(UtilType::GlobalCss))]
#[case("keyframes".to_string(), Ok(UtilType::Keyframes))]
#[case("unknown".to_string(), Err("unknown".to_string()))]
- #[case("".to_string(), Err("".to_string()))]
+ #[case(String::new(), Err(String::new()))]
fn test_util_type_try_from(#[case] input: String, #[case] expected: Result) {
assert_eq!(UtilType::try_from(input), expected);
}
diff --git a/libs/extractor/src/utils.rs b/libs/extractor/src/utils.rs
index 2b2fe455..163ecf09 100644
--- a/libs/extractor/src/utils.rs
+++ b/libs/extractor/src/utils.rs
@@ -73,7 +73,7 @@ pub(super) enum ParsedStyleOrder<'a> {
impl ParsedStyleOrder<'_> {
/// Convert to Option for backward compatibility (returns None for Conditional)
- pub fn as_static(&self) -> Option {
+ pub const fn as_static(&self) -> Option {
match self {
ParsedStyleOrder::Static(v) => Some(*v),
_ => None,
@@ -334,6 +334,7 @@ pub fn gcd(a: u32, b: u32) -> u32 {
}
#[cfg(test)]
+#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use oxc_allocator::Vec;
use oxc_ast::ast::NumberBase;
@@ -574,14 +575,13 @@ mod tests {
} else {
assert!(
result.is_some(),
- "Expected Some, but got None for input: {:?}",
- input
+ "Expected Some, but got None for input: {input:?}"
);
if let Some(expr) = result {
let code = super::expression_to_code(&expr);
let snapshot_name = format!(
"wrap_array_filter_{}",
- input.join("_").replace("\"", "quote")
+ input.join("_").replace('"', "quote")
);
assert_snapshot!(snapshot_name, code);
}
diff --git a/libs/extractor/src/vanilla_extract.rs b/libs/extractor/src/vanilla_extract.rs
index 8b28aa44..9fcb427c 100644
--- a/libs/extractor/src/vanilla_extract.rs
+++ b/libs/extractor/src/vanilla_extract.rs
@@ -1,6 +1,6 @@
//! Vanilla-extract style file (.css.ts, .css.js) processor
//!
-//! This module uses boa_engine to execute vanilla-extract style files
+//! This module uses `boa_engine` to execute vanilla-extract style files
//! and extract style definitions for processing by the existing extract logic.
#![allow(dead_code)] // Public API fields/functions for future expansion
@@ -19,7 +19,6 @@ use oxc_transformer::{TransformOptions, Transformer};
use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use std::cell::RefCell;
-use std::fmt::Write;
use std::path::Path;
use std::rc::Rc;
@@ -39,65 +38,65 @@ pub struct StyleEntry {
pub json: String,
/// Whether this style is exported
pub exported: bool,
- /// Base class references for composition (placeholder IDs like "__style_0__")
+ /// Base class references for composition (placeholder IDs like "__`style_0`__")
pub bases: SmallVec<[String; 2]>,
}
-/// Entry for createGlobalTheme() - CSS variables scoped to a selector
+/// Entry for `createGlobalTheme()` - CSS variables scoped to a selector
#[derive(Debug, Clone, Default)]
pub struct GlobalThemeEntry {
/// CSS selector (e.g., ":root")
pub selector: String,
- /// CSS variables: Vec<(var_name, value)> e.g. [("--color-brand-0-0", "blue")]
+ /// CSS variables: Vec<(`var_name`, value)> e.g. [("--color-brand-0-0", "blue")]
pub css_vars: SmallVec<[(String, String); 8]>,
- /// Serialized JS object with var() references
+ /// Serialized JS object with `var()` references
pub vars_object_json: String,
/// Whether this is exported
pub exported: bool,
}
-/// Entry for createTheme() - CSS variables scoped to a generated class
+/// Entry for `createTheme()` - CSS variables scoped to a generated class
#[derive(Debug, Clone, Default)]
pub struct ThemeEntry {
- /// CSS variables: Vec<(var_name, value)> e.g. [("--color-brand", "blue")]
+ /// CSS variables: Vec<(`var_name`, value)> e.g. [("--color-brand", "blue")]
pub css_vars: SmallVec<[(String, String); 8]>,
/// Whether this is exported
pub exported: bool,
- /// For single-arg createTheme: the vars object JSON with var() references
+ /// For single-arg createTheme: the vars object JSON with `var()` references
/// Used to generate the second element of the returned array
pub vars_object_json: Option,
/// For single-arg createTheme: the name of the vars variable from [themeClass, vars]
pub vars_name: Option,
- /// The unique generated class name (file_prefix + variable_name)
+ /// The unique generated class name (`file_prefix` + `variable_name`)
pub class_name: String,
}
/// Collected style definitions from vanilla-extract API calls
#[derive(Debug, Default)]
pub struct CollectedStyles {
- /// style() calls: variable_name -> (json, exported)
+ /// `style()` calls: `variable_name` -> (json, exported)
pub styles: FxHashMap,
- /// globalStyle() calls: selector -> style object JSON
+ /// `globalStyle()` calls: selector -> style object JSON
pub global_styles: Vec<(String, String)>,
- /// keyframes() calls: variable_name -> (json, exported)
+ /// `keyframes()` calls: `variable_name` -> (json, exported)
pub keyframes: FxHashMap,
- /// createVar() calls: variable_name -> (CSS variable string, exported)
+ /// `createVar()` calls: `variable_name` -> (CSS variable string, exported)
pub vars: FxHashMap,
- /// fontFace() calls: placeholder_id -> (font_face JSON, font-family name, exported)
+ /// `fontFace()` calls: `placeholder_id` -> (`font_face` JSON, font-family name, exported)
pub font_faces: FxHashMap,
- /// styleVariants() calls: variable_name -> (variants, exported)
+ /// `styleVariants()` calls: `variable_name` -> (variants, exported)
pub style_variants: FxHashMap, bool)>,
- /// createContainer() calls: variable_name -> (container name string, exported)
+ /// `createContainer()` calls: `variable_name` -> (container name string, exported)
pub containers: FxHashMap,
- /// layer() calls: variable_name -> (layer name string, exported)
+ /// `layer()` calls: `variable_name` -> (layer name string, exported)
pub layers: FxHashMap,
- /// createGlobalTheme() calls: variable_name -> GlobalThemeEntry
+ /// `createGlobalTheme()` calls: `variable_name` -> `GlobalThemeEntry`
pub global_themes: FxHashMap,
- /// createTheme() calls: variable_name -> ThemeEntry
+ /// `createTheme()` calls: `variable_name` -> `ThemeEntry`
pub themes: FxHashMap,
- /// Theme vars from array destructuring: vars_name -> (vars_object_json, exported)
+ /// Theme vars from array destructuring: `vars_name` -> (`vars_object_json`, exported)
pub theme_vars: FxHashMap,
- /// Non-style constant exports: variable_name -> value (as code string)
+ /// Non-style constant exports: `variable_name` -> value (as code string)
pub constant_exports: FxHashMap,
}
@@ -166,7 +165,7 @@ fn parse_font_face_json(json: &str) -> Vec<(String, String)> {
.collect()
}
-/// Recursively transform theme contract object to CSS var() references
+/// Recursively transform theme contract object to CSS `var()` references
/// Returns a new JS object with null leaves replaced by var(--path)
fn transform_contract_to_vars(value: &JsValue, ctx: &mut Context, path: &[String]) -> JsValue {
if let Some(obj) = value.as_object() {
@@ -258,7 +257,7 @@ fn extract_theme_vars(
}
}
-/// Recursively transform theme object to CSS var() references
+/// Recursively transform theme object to CSS `var()` references
/// Returns a new JS object with the same structure but leaf values replaced with var(--path)
fn transform_theme_to_vars(
value: &JsValue,
@@ -330,7 +329,7 @@ fn transform_theme_to_vars(
}
}
-/// Convert JsValue to JSON string using JSON.stringify
+/// Convert `JsValue` to JSON string using JSON.stringify
fn js_value_to_json(value: &JsValue, context: &mut Context) -> String {
// Use JSON.stringify to convert the value
let json_obj = context.intrinsics().objects().json();
@@ -377,7 +376,7 @@ pub fn execute_vanilla_extract(
// Execute the code
context
.eval(Source::from_bytes(js_code.as_bytes()))
- .map_err(|e| format!("JS execution error: {}", e))?;
+ .map_err(|e| format!("JS execution error: {e}"))?;
// Map placeholder IDs back to original variable names
let mut result = std::mem::take(&mut collector.borrow_mut().styles);
@@ -398,7 +397,7 @@ enum VarInfo {
}
/// Extract all variable names and their info from the original code
-/// Returns (style_api_vars, exported_constants)
+/// Returns (`style_api_vars`, `exported_constants`)
fn extract_var_names(code: &str, _package: &str) -> Vec<(String, VarInfo)> {
let allocator = Allocator::default();
let source_type = SourceType::ts();
@@ -527,7 +526,7 @@ fn remap_style_names(
) {
// Generate a file-based prefix for unique class names
// e.g., file_num 0 -> "f0"
- let file_prefix = format!("f{}", file_num);
+ let file_prefix = format!("f{file_num}");
// Build mapping from placeholder ID to original name
// The order of style() calls matches the order of variable declarations
let mut placeholder_to_name: FxHashMap = FxHashMap::default();
@@ -562,7 +561,7 @@ fn remap_style_names(
match info {
VarInfo::StyleApi { exported } => {
// First check if this is a fontFace (uses __font_N__ placeholder)
- let font_placeholder = format!("__font_{}__", font_idx);
+ let font_placeholder = format!("__font_{font_idx}__");
if let Some((json, font_family, _)) = old_font_faces.remove(&font_placeholder) {
font_placeholder_to_name.insert(font_placeholder, name.clone());
new_font_faces.insert(name.clone(), (json, font_family, *exported));
@@ -571,7 +570,7 @@ fn remap_style_names(
}
// Check if this is a createGlobalTheme (uses __global_theme_N__ placeholder)
- let global_theme_placeholder = format!("__global_theme_{}__", global_theme_idx);
+ let global_theme_placeholder = format!("__global_theme_{global_theme_idx}__");
if let Some(mut entry) = old_global_themes.remove(&global_theme_placeholder) {
entry.exported = *exported;
new_global_themes.insert(name.clone(), entry);
@@ -579,7 +578,7 @@ fn remap_style_names(
continue;
}
- let placeholder = format!("__style_{}__", style_idx);
+ let placeholder = format!("__style_{style_idx}__");
placeholder_to_name.insert(placeholder.clone(), name.clone());
if let Some(mut entry) = old_styles.remove(&placeholder) {
@@ -609,7 +608,7 @@ fn remap_style_names(
}
// Generate unique class name: file_prefix + variable_name
- let class_name = format!("{}_{}", file_prefix, name);
+ let class_name = format!("{file_prefix}_{name}");
// Add CSS variables to global_styles with class selector
if !entry.css_vars.is_empty() {
@@ -618,13 +617,13 @@ fn remap_style_names(
entry
.css_vars
.iter()
- .map(|(var_name, value)| format!("\"{}\": \"{}\"", var_name, value))
+ .map(|(var_name, value)| format!("\"{var_name}\": \"{value}\""))
.collect::>()
.join(", ")
);
collected
.global_styles
- .push((format!(".{}", class_name), vars_json));
+ .push((format!(".{class_name}"), vars_json));
}
entry.exported = *exported;
@@ -688,23 +687,10 @@ fn remap_style_names(
})
.collect();
- // Replace __font_N__ placeholders in style JSON with font-family names
+ // Replace placeholders in a single pass per style JSON.
+ // This is needed for font-family references and selectors like `${parent}:hover &`.
for entry in new_styles.values_mut() {
- for (placeholder, font_family) in &font_family_map {
- if entry.json.contains(placeholder) {
- entry.json = entry.json.replace(placeholder, font_family);
- }
- }
- }
-
- // Replace __style_N__ placeholders in style JSON with variable names
- // This is needed for selectors that reference other styles like `${parent}:hover &`
- for entry in new_styles.values_mut() {
- for (placeholder, var_name) in &placeholder_to_name {
- if entry.json.contains(placeholder) {
- entry.json = entry.json.replace(placeholder, var_name);
- }
- }
+ replace_placeholders_in_json(&mut entry.json, &font_family_map, &placeholder_to_name);
}
collected.styles = new_styles;
@@ -718,6 +704,45 @@ fn remap_style_names(
collected.themes = new_themes;
}
+fn replace_placeholders_in_json(
+ json: &mut String,
+ font_family_map: &FxHashMap<&str, &str>,
+ placeholder_to_name: &FxHashMap,
+) {
+ let source = json.as_str();
+ let mut search_start = 0;
+ let mut last_copied = 0;
+ let mut output = None::;
+
+ while let Some(relative_start) = source[search_start..].find("__") {
+ let start = search_start + relative_start;
+ let Some(relative_end) = source[start + 2..].find("__") else {
+ break;
+ };
+ let end = start + 2 + relative_end + 2;
+ let placeholder = &source[start..end];
+ let replacement = font_family_map
+ .get(placeholder)
+ .copied()
+ .or_else(|| placeholder_to_name.get(placeholder).map(String::as_str));
+
+ if let Some(replacement) = replacement {
+ let output = output.get_or_insert_with(|| String::with_capacity(source.len()));
+ output.push_str(&source[last_copied..start]);
+ output.push_str(replacement);
+ last_copied = end;
+ search_start = end;
+ } else {
+ search_start = start + 2;
+ }
+ }
+
+ if let Some(mut output) = output {
+ output.push_str(&source[last_copied..]);
+ *json = output;
+ }
+}
+
/// Convert TypeScript to JavaScript using Oxc Transformer and replace imports
fn preprocess_typescript(code: &str, package: &str) -> String {
let allocator = Allocator::default();
@@ -737,21 +762,19 @@ fn preprocess_typescript(code: &str, package: &str) -> String {
let _ = Transformer::new(&allocator, path, &options).build_with_scoping(scoping, &mut program);
// Generate JavaScript
- let mut js_code = Codegen::new().build(&program).code;
+ let js_code = Codegen::new().build(&program).code;
// Replace import from package with our mock object destructuring
// e.g., import { style } from '@devup-ui/react' -> const { style } = __vanilla_extract__;
// Note: Import aliases (like @vanilla-extract/css) are already transformed by import_alias_visit
- let import_patterns = [
- format!("from \"{}\"", package),
- format!("from '{}'", package),
- ];
+ let import_patterns = [format!("from \"{package}\""), format!("from '{package}'")];
// Process all import patterns (multiple imports may exist)
- let lines: Vec<&str> = js_code.lines().collect();
- let mut new_lines = Vec::with_capacity(lines.len());
-
- for line in lines {
+ let mut transformed = String::with_capacity(js_code.len());
+ for (idx, line) in js_code.lines().enumerate() {
+ if idx > 0 {
+ transformed.push('\n');
+ }
let mut matched = false;
for pattern in &import_patterns {
if line.contains(pattern) {
@@ -760,25 +783,33 @@ fn preprocess_typescript(code: &str, package: &str) -> String {
&& let Some(end) = line.find('}')
{
let imports = &line[start + 1..end];
- new_lines.push(format!("const {{{}}} = __vanilla_extract__;", imports));
+ transformed.push_str("const {");
+ transformed.push_str(imports);
+ transformed.push_str("} = __vanilla_extract__;");
matched = true;
break;
}
}
}
if !matched {
- new_lines.push(line.to_string());
+ transformed.push_str(strip_export_keyword(line));
}
}
- js_code = new_lines.join("\n");
-
- // Remove 'export' keyword (boa doesn't support ES modules)
- js_code = js_code.replace("export const ", "const ");
- js_code = js_code.replace("export let ", "let ");
- js_code = js_code.replace("export var ", "var ");
- js_code = js_code.replace("export function ", "function ");
+ transformed
+}
- js_code
+fn strip_export_keyword(line: &str) -> &str {
+ line.strip_prefix("export ").map_or(line, |rest| {
+ if rest.starts_with("const ")
+ || rest.starts_with("let ")
+ || rest.starts_with("var ")
+ || rest.starts_with("function ")
+ {
+ rest
+ } else {
+ line
+ }
+ })
}
/// Register vanilla-extract mock APIs in the JS context
@@ -905,7 +936,7 @@ fn register_vanilla_extract_apis(
.borrow_mut()
.styles
.vars
- .insert(id.clone(), (var_name.clone(), false));
+ .insert(id, (var_name.clone(), false));
// Return just the CSS custom property name, without var() wrapper
Ok(JsValue::from(js_string!(var_name)))
})
@@ -932,7 +963,7 @@ fn register_vanilla_extract_apis(
.borrow_mut()
.styles
.font_faces
- .insert(id.clone(), (json, font_family.clone(), false));
+ .insert(id.clone(), (json, font_family, false));
// Return the placeholder ID - will be replaced in code generation
Ok(JsValue::from(js_string!(id)))
@@ -972,7 +1003,7 @@ fn register_vanilla_extract_apis(
// var_value is now just "--var-0" (CSS custom property name)
// Return var(--var-0, fallback)
- let result = format!("var({}, {})", var_value, fallback);
+ let result = format!("var({var_value}, {fallback})");
Ok(JsValue::from(js_string!(result)))
})
};
@@ -1072,7 +1103,7 @@ fn register_vanilla_extract_apis(
.borrow_mut()
.styles
.layers
- .insert(id.clone(), (name.clone(), false));
+ .insert(id, (name.clone(), false));
Ok(JsValue::from(js_string!(name)))
})
};
@@ -1090,21 +1121,20 @@ fn register_vanilla_extract_apis(
.borrow_mut()
.styles
.containers
- .insert(id.clone(), (container_name.clone(), false));
+ .insert(id, (container_name.clone(), false));
Ok(JsValue::from(js_string!(container_name)))
})
};
// createGlobalTheme() function
- let collector_global_theme = collector.clone();
+ let collector_global_theme = collector;
let create_global_theme_fn = unsafe {
NativeFunction::from_closure(move |_this, args, ctx| {
let placeholder_id = next_global_theme_id(&collector_global_theme);
let selector = args
.get_or_undefined(0)
.to_string(ctx)
- .map(|s| s.to_std_string_escaped())
- .unwrap_or_else(|_| ":root".to_string());
+ .map_or_else(|_| ":root".to_string(), |s| s.to_std_string_escaped());
let theme_obj = args.get_or_undefined(1);
// Collect CSS variables and build new object with var() references
@@ -1165,7 +1195,7 @@ fn register_vanilla_extract_apis(
// Register as global __vanilla_extract__
context
.register_global_property(js_string!("__vanilla_extract__"), ve_obj, Attribute::all())
- .map_err(|e| format!("Failed to register __vanilla_extract__: {}", e))?;
+ .map_err(|e| format!("Failed to register __vanilla_extract__: {e}"))?;
Ok(())
}
@@ -1174,15 +1204,15 @@ fn register_vanilla_extract_apis(
/// Returns a set of style names that need to be extracted first
pub fn find_selector_references(collected: &CollectedStyles) -> FxHashSet {
let mut referenced = rustc_hash::FxHashSet::default();
- let style_names: FxHashSet<&str> = collected.styles.keys().map(|s| s.as_str()).collect();
+ let style_names: FxHashSet<&str> = collected.styles.keys().map(String::as_str).collect();
for entry in collected.styles.values() {
// Check if this style's JSON contains references to other style names
for style_name in &style_names {
// Look for patterns like "stylename:" or "stylename " in selectors
// The JSON has selectors like {"selectors":{"parent:hover &":{...}}}
- if entry.json.contains(&format!("\"{}:", style_name))
- || entry.json.contains(&format!("\"{} ", style_name))
+ if entry.json.contains(&format!("\"{style_name}:"))
+ || entry.json.contains(&format!("\"{style_name} "))
{
referenced.insert(style_name.to_string());
}
@@ -1201,7 +1231,9 @@ pub fn collected_styles_to_code_partial(
let mut output = String::new();
if !style_names.is_empty() {
- write!(output, "import {{ css }} from '{}'", package).unwrap();
+ output.push_str("import { css } from '");
+ output.push_str(package);
+ output.push('\'');
}
// Generate only the specified styles
@@ -1217,7 +1249,11 @@ pub fn collected_styles_to_code_partial(
if !output.is_empty() {
output.push('\n');
}
- write!(output, "const {} = css({})", name, entry.json).unwrap();
+ output.push_str("const ");
+ output.push_str(name);
+ output.push_str(" = css(");
+ output.push_str(&entry.json);
+ output.push(')');
}
output
@@ -1269,10 +1305,10 @@ pub fn collected_styles_to_code_with_classes(
.iter()
.map(|(style_name, class_name)| {
(
- format!("\"{}:", style_name),
- format!("\"{}:", class_name),
- format!("\"{} ", style_name),
- format!("\"{} ", class_name),
+ format!("\"{style_name}:"),
+ format!("\"{class_name}:"),
+ format!("\"{style_name} "),
+ format!("\"{class_name} "),
)
})
.collect();
@@ -1295,7 +1331,7 @@ pub fn collected_styles_to_code_with_classes(
}
if entry.bases.is_empty() {
- code_parts.push(format!("{}const {} = css({})", prefix, name, json));
+ code_parts.push(format!("{prefix}const {name} = css({json})"));
} else {
// Composition: merge all base styles
let mut merged_parts = Vec::new();
@@ -1320,7 +1356,7 @@ pub fn collected_styles_to_code_with_classes(
merged_parts.push(own_inner.to_string());
}
let merged_json = format!("{{{}}}", merged_parts.join(","));
- code_parts.push(format!("{}const {} = css({})", prefix, name, merged_json));
+ code_parts.push(format!("{prefix}const {name} = css({merged_json})"));
}
}
@@ -1364,7 +1400,7 @@ fn append_non_style_code(
) {
// Generate globalCss calls
for (selector, json) in &collected.global_styles {
- code_parts.push(format!("globalCss({{ \"{}\": {} }})", selector, json));
+ code_parts.push(format!("globalCss({{ \"{selector}\": {json} }})"));
}
// Generate @font-face rules
@@ -1374,18 +1410,14 @@ fn append_non_style_code(
let props = parse_font_face_json(json);
let props_str = props
.iter()
- .map(|(k, v)| format!("{}: {}", k, v))
+ .map(|(k, v)| format!("{k}: {v}"))
.collect::>()
.join(", ");
let code = if props_str.is_empty() {
- format!(
- "globalCss({{ fontFaces: [{{ fontFamily: \"{}\" }}] }})",
- font_family
- )
+ format!("globalCss({{ fontFaces: [{{ fontFamily: \"{font_family}\" }}] }})")
} else {
format!(
- "globalCss({{ fontFaces: [{{ fontFamily: \"{}\", {} }}] }})",
- font_family, props_str
+ "globalCss({{ fontFaces: [{{ fontFamily: \"{font_family}\", {props_str} }}] }})"
)
};
code_parts.push(code);
@@ -1399,7 +1431,7 @@ fn append_non_style_code(
let vars_str = entry
.css_vars
.iter()
- .map(|(var_name, value)| format!("\"{}\": \"{}\"", var_name, value))
+ .map(|(var_name, value)| format!("\"{var_name}\": \"{value}\""))
.collect::>()
.join(", ");
code_parts.push(format!(
@@ -1458,7 +1490,7 @@ fn append_non_style_code(
} else {
format!("css({})", variant.styles_json)
};
- object_parts.push(format!(" {}: {}", variant_key, value));
+ object_parts.push(format!(" {variant_key}: {value}"));
}
let prefix = if *exported { "export " } else { "" };
code_parts.push(format!(
@@ -1474,7 +1506,7 @@ fn append_non_style_code(
vars.sort_by_key(|(name, _)| *name);
for (name, (value, exported)) in vars {
let prefix = if *exported { "export " } else { "" };
- code_parts.push(format!("{}const {} = \"{}\"", prefix, name, value));
+ code_parts.push(format!("{prefix}const {name} = \"{value}\""));
}
// Generate fontFace declarations
@@ -1482,7 +1514,7 @@ fn append_non_style_code(
font_faces.sort_by_key(|(name, _)| *name);
for (name, (_, font_family, exported)) in font_faces {
let prefix = if *exported { "export " } else { "" };
- code_parts.push(format!("{}const {} = \"{}\"", prefix, name, font_family));
+ code_parts.push(format!("{prefix}const {name} = \"{font_family}\""));
}
// Generate createContainer declarations
@@ -1490,7 +1522,7 @@ fn append_non_style_code(
containers.sort_by_key(|(name, _)| *name);
for (name, (value, exported)) in containers {
let prefix = if *exported { "export " } else { "" };
- code_parts.push(format!("{}const {} = \"{}\"", prefix, name, value));
+ code_parts.push(format!("{prefix}const {name} = \"{value}\""));
}
// Generate layer declarations
@@ -1498,7 +1530,7 @@ fn append_non_style_code(
layers.sort_by_key(|(name, _)| *name);
for (name, (value, exported)) in layers {
let prefix = if *exported { "export " } else { "" };
- code_parts.push(format!("{}const {} = \"{}\"", prefix, name, value));
+ code_parts.push(format!("{prefix}const {name} = \"{value}\""));
}
// Generate createGlobalTheme vars object declarations
@@ -1514,7 +1546,7 @@ fn append_non_style_code(
let mut constants: Vec<_> = collected.constant_exports.iter().collect();
constants.sort_by_key(|(name, _)| *name);
for (name, value) in constants {
- code_parts.push(format!("export const {} = {}", name, value));
+ code_parts.push(format!("export const {name} = {value}"));
}
}
@@ -1594,7 +1626,7 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
}
let merged_json = format!("{{{}}}", merged_parts.join(","));
- code_parts.push(format!("{}const {} = css({})", prefix, name, merged_json));
+ code_parts.push(format!("{prefix}const {name} = css({merged_json})"));
}
}
@@ -1627,7 +1659,7 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
// Generate globalCss calls
for (selector, json) in &collected.global_styles {
- code_parts.push(format!("globalCss({{ \"{}\": {} }})", selector, json));
+ code_parts.push(format!("globalCss({{ \"{selector}\": {json} }})"));
}
// Generate @font-face rules via globalCss fontFaces (sorted for deterministic output)
@@ -1640,20 +1672,16 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
let props = parse_font_face_json(json);
let props_str = props
.iter()
- .map(|(k, v)| format!("{}: {}", k, v))
+ .map(|(k, v)| format!("{k}: {v}"))
.collect::>()
.join(", ");
// Generate clean single-line globalCss call
let code = if props_str.is_empty() {
- format!(
- "globalCss({{ fontFaces: [{{ fontFamily: \"{}\" }}] }})",
- font_family
- )
+ format!("globalCss({{ fontFaces: [{{ fontFamily: \"{font_family}\" }}] }})")
} else {
format!(
- "globalCss({{ fontFaces: [{{ fontFamily: \"{}\", {} }}] }})",
- font_family, props_str
+ "globalCss({{ fontFaces: [{{ fontFamily: \"{font_family}\", {props_str} }}] }})"
)
};
code_parts.push(code);
@@ -1668,7 +1696,7 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
let vars_str = entry
.css_vars
.iter()
- .map(|(var_name, value)| format!("\"{}\": \"{}\"", var_name, value))
+ .map(|(var_name, value)| format!("\"{var_name}\": \"{value}\""))
.collect::>()
.join(", ");
code_parts.push(format!(
@@ -1731,7 +1759,7 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
// No composition, just the styles
format!("css({})", variant.styles_json)
};
- object_parts.push(format!(" {}: {}", variant_key, value));
+ object_parts.push(format!(" {variant_key}: {value}"));
}
let prefix = if *exported { "export " } else { "" };
@@ -1748,7 +1776,7 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
vars.sort_by_key(|(name, _)| *name);
for (name, (value, exported)) in vars {
let prefix = if *exported { "export " } else { "" };
- code_parts.push(format!("{}const {} = \"{}\"", prefix, name, value));
+ code_parts.push(format!("{prefix}const {name} = \"{value}\""));
}
// Generate fontFace declarations (sorted for deterministic output)
@@ -1757,7 +1785,7 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
font_faces.sort_by_key(|(name, _)| *name);
for (name, (_, font_family, exported)) in font_faces {
let prefix = if *exported { "export " } else { "" };
- code_parts.push(format!("{}const {} = \"{}\"", prefix, name, font_family));
+ code_parts.push(format!("{prefix}const {name} = \"{font_family}\""));
}
// Generate createContainer declarations (sorted for deterministic output)
@@ -1765,7 +1793,7 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
containers.sort_by_key(|(name, _)| *name);
for (name, (value, exported)) in containers {
let prefix = if *exported { "export " } else { "" };
- code_parts.push(format!("{}const {} = \"{}\"", prefix, name, value));
+ code_parts.push(format!("{prefix}const {name} = \"{value}\""));
}
// Generate layer declarations (sorted for deterministic output)
@@ -1773,7 +1801,7 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
layers.sort_by_key(|(name, _)| *name);
for (name, (value, exported)) in layers {
let prefix = if *exported { "export " } else { "" };
- code_parts.push(format!("{}const {} = \"{}\"", prefix, name, value));
+ code_parts.push(format!("{prefix}const {name} = \"{value}\""));
}
// Generate createGlobalTheme vars object declarations (sorted for deterministic output)
@@ -1789,7 +1817,7 @@ pub fn collected_styles_to_code(collected: &CollectedStyles, package: &str) -> S
let mut constants: Vec<_> = collected.constant_exports.iter().collect();
constants.sort_by_key(|(name, _)| *name);
for (name, value) in constants {
- code_parts.push(format!("export const {} = {}", name, value));
+ code_parts.push(format!("export const {name} = {value}"));
}
code_parts.join("\n")
@@ -1880,6 +1908,7 @@ fn parse_single_variant(value: &JsValue, context: &mut Context) -> StyleVariant
}
#[cfg(test)]
+#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use smallvec::smallvec;
@@ -1902,13 +1931,11 @@ export const container = style({ background: "red" })"#;
// The result should have destructuring from __vanilla_extract__ and no export keyword
assert!(
result.contains("__vanilla_extract__"),
- "Expected __vanilla_extract__ but got: {}",
- result
+ "Expected __vanilla_extract__ but got: {result}"
);
assert!(
!result.contains("export const"),
- "Should not contain 'export const': {}",
- result
+ "Should not contain 'export const': {result}"
);
}
@@ -1923,8 +1950,7 @@ export const container = style({ background: "red" })"#;
// TypeScript interface should be stripped
assert!(
!result.contains("interface"),
- "Should not contain interface: {}",
- result
+ "Should not contain interface: {result}"
);
}
@@ -1945,8 +1971,7 @@ export const button = style({ color: "blue" })"#;
let result = execute_vanilla_extract(code, "@devup-ui/react", "test.css.ts").unwrap();
assert!(
result.styles.len() >= 2,
- "Expected at least 2 styles but got: {:?}",
- result
+ "Expected at least 2 styles but got: {result:?}"
);
}
@@ -1959,20 +1984,18 @@ export const container = style({ background: "red", padding: 16 })"#;
let generated = super::collected_styles_to_code(&collected, "@devup-ui/react");
assert!(
!generated.is_empty(),
- "Expected non-empty generated code. Collected: {:?}",
- collected
+ "Expected non-empty generated code. Collected: {collected:?}"
);
assert!(
generated.contains("css("),
- "Expected css() call in generated code: {}",
- generated
+ "Expected css() call in generated code: {generated}"
);
}
#[test]
fn test_full_flow_multiline() {
// Test exactly what lib.rs extract function does (with already-transformed imports)
- let code = r#"import { style } from '@devup-ui/react'
+ let code = r"import { style } from '@devup-ui/react'
export const hello = style({
cursor: 'pointer',
fontSize: 32,
@@ -1982,7 +2005,7 @@ export const hello = style({
export const text = style({
color: 'var(--text)',
})
-"#;
+";
let package = "@devup-ui/react";
let filename = "styles.css.ts";
@@ -1997,19 +2020,17 @@ export const text = style({
Ok(collected) => {
assert!(
!collected.styles.is_empty(),
- "Styles should not be empty. Collected: {:?}",
- collected
+ "Styles should not be empty. Collected: {collected:?}"
);
let generated = super::collected_styles_to_code(&collected, package);
assert!(
!generated.is_empty(),
- "Generated code should not be empty. Generated: {}",
- generated
+ "Generated code should not be empty. Generated: {generated}"
);
- println!("Generated code:\n{}", generated);
+ println!("Generated code:\n{generated}");
}
Err(e) => {
- panic!("execute_vanilla_extract failed: {}", e);
+ panic!("execute_vanilla_extract failed: {e}");
}
}
}
@@ -2066,9 +2087,9 @@ export const button = style({ background: primaryColor, padding: spacing })"#;
#[test]
fn test_execute_vanilla_extract_with_computed_value() {
// Test computed values
- let code = r#"import { style } from '@devup-ui/react'
+ let code = r"import { style } from '@devup-ui/react'
const base = 8;
-export const box = style({ padding: base * 2, margin: base / 2 })"#;
+export const box = style({ padding: base * 2, margin: base / 2 })";
let result = execute_vanilla_extract(code, "@devup-ui/react", "test.css.ts").unwrap();
assert!(result.styles.contains_key("box"));
let entry = &result.styles["box"];
@@ -2078,7 +2099,7 @@ export const box = style({ padding: base * 2, margin: base / 2 })"#;
entry.json
);
assert!(
- entry.json.contains("4"),
+ entry.json.contains('4'),
"Expected margin 4 in JSON: {}",
entry.json
);
@@ -2110,12 +2131,12 @@ export const extended = style({ ...baseStyle, background: "red" })"#;
assert!(result.styles.contains_key("extended"));
let entry = &result.styles["extended"];
assert!(
- entry.json.contains("8"),
+ entry.json.contains('8'),
"Expected padding 8 in JSON: {}",
entry.json
);
assert!(
- entry.json.contains("4"),
+ entry.json.contains('4'),
"Expected margin 4 in JSON: {}",
entry.json
);
@@ -2129,7 +2150,7 @@ export const extended = style({ ...baseStyle, background: "red" })"#;
#[test]
fn test_execute_vanilla_extract_create_theme_contract() {
// Test createThemeContract + createTheme
- let code = r#"import { createThemeContract, createTheme } from '@devup-ui/react'
+ let code = r"import { createThemeContract, createTheme } from '@devup-ui/react'
const vars = createThemeContract({
color: {
brand: null,
@@ -2141,7 +2162,7 @@ export const lightTheme = createTheme(vars, {
brand: 'blue',
text: 'black'
}
-})"#;
+})";
let result = execute_vanilla_extract(code, "@devup-ui/react", "test.css.ts").unwrap();
// Check that themes were collected
@@ -2160,8 +2181,7 @@ export const lightTheme = createTheme(vars, {
let theme_entry = &result.themes["lightTheme"];
assert!(
!theme_entry.css_vars.is_empty(),
- "Expected CSS vars in theme: {:?}",
- theme_entry
+ "Expected CSS vars in theme: {theme_entry:?}"
);
// Check that CSS vars are correct
@@ -2170,8 +2190,7 @@ export const lightTheme = createTheme(vars, {
css_vars
.iter()
.any(|(name, val)| name == "--color-brand" && val == "blue"),
- "Expected --color-brand: blue in {:?}",
- css_vars
+ "Expected --color-brand: blue in {css_vars:?}"
);
}
@@ -2885,11 +2904,11 @@ export const lightTheme = createTheme(vars, {
#[test]
fn test_extract_var_names_non_style_api() {
// Test extract_var_names with non-style-api expressions
- let code = r#"import { style } from '@devup-ui/react'
+ let code = r"import { style } from '@devup-ui/react'
export const CONSTANT = 42;
export const OBJECT = { key: 'value' };
export const computed = 1 + 2;
-"#;
+";
let vars = super::extract_var_names(code, "@devup-ui/react");
// Should have constants
assert!(vars.iter().any(|(name, _)| name == "CONSTANT"));
@@ -2899,8 +2918,8 @@ export const computed = 1 + 2;
#[test]
fn test_preprocess_typescript_single_quotes() {
// Test preprocess with single quotes in import
- let code = r#"import { style } from '@devup-ui/react'
-export const box = style({ padding: 8 })"#;
+ let code = r"import { style } from '@devup-ui/react'
+export const box = style({ padding: 8 })";
let result = super::preprocess_typescript(code, "@devup-ui/react");
assert!(result.contains("__vanilla_extract__"));
assert!(!result.contains("export const"));
@@ -2967,9 +2986,7 @@ export const box = style({ padding: 8 })"#;
);
let class_map: rustc_hash::FxHashMap =
- [("parent".to_string(), "a".to_string())]
- .into_iter()
- .collect();
+ std::iter::once(("parent".to_string(), "a".to_string())).collect();
let code =
super::collected_styles_to_code_with_classes(&collected, "@devup-ui/react", &class_map);
assert!(code.contains("a:hover"));
@@ -3322,7 +3339,7 @@ export const box = style({ padding: 8 })"#;
collected.styles.insert(
"box".to_string(),
StyleEntry {
- json: r#"{}"#.to_string(),
+ json: r"{}".to_string(),
exported: true,
bases: SmallVec::new(),
},
@@ -3388,7 +3405,7 @@ export const box = style({ padding: 8 })"#;
);
let class_map: rustc_hash::FxHashMap =
- [("box".to_string(), "a".to_string())].into_iter().collect();
+ std::iter::once(("box".to_string(), "a".to_string())).collect();
let code =
super::collected_styles_to_code_with_classes(&collected, "@devup-ui/react", &class_map);
@@ -3404,7 +3421,7 @@ export const box = style({ padding: 8 })"#;
collected.styles.insert(
"box".to_string(),
StyleEntry {
- json: r#"{}"#.to_string(),
+ json: r"{}".to_string(),
exported: true,
bases: SmallVec::new(),
},
@@ -3533,12 +3550,12 @@ export const box = style({ padding: 8 })"#;
let mut collected = CollectedStyles::default();
collected.font_faces.insert(
"customFont".to_string(),
- (r#"{}"#.to_string(), "__devup_font_custom".to_string(), true),
+ (r"{}".to_string(), "__devup_font_custom".to_string(), true),
);
collected.font_faces.insert(
"internalFont".to_string(),
(
- r#"{}"#.to_string(),
+ r"{}".to_string(),
"__devup_font_internal".to_string(),
false,
),
@@ -3605,11 +3622,11 @@ export const box = style({ padding: 8 })"#;
fn test_style_with_non_object_argument() {
// Test style() with primitive value (covers line 749)
// This is tested through execute_vanilla_extract with edge case input
- let code = r#"
+ let code = r"
import { style } from '@devup-ui/react'
// This should still work - style with null or undefined would be an edge case
export const empty = style({})
-"#;
+";
let result = super::execute_vanilla_extract(code, "@devup-ui/react", "test.css.ts");
assert!(result.is_ok());
}
@@ -3617,10 +3634,10 @@ export const empty = style({})
#[test]
fn test_style_with_object_no_length() {
// Test style() with regular object (covers line 745)
- let code = r#"
+ let code = r"
import { style } from '@devup-ui/react'
export const box = style({ padding: 8, margin: 4 })
-"#;
+";
let result = super::execute_vanilla_extract(code, "@devup-ui/react", "test.css.ts");
assert!(result.is_ok());
let collected = result.unwrap();
diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs
index 157b13e9..c9f61007 100644
--- a/libs/extractor/src/visit.rs
+++ b/libs/extractor/src/visit.rs
@@ -59,22 +59,22 @@ pub struct DevupVisitor<'a> {
pub css_files: Vec,
pub styles: FxHashSet,
styled_import: Option,
- /// Tracked StyleX default/namespace import name (e.g., `stylex` from `import stylex from '...'`)
+ /// Tracked `StyleX` default/namespace import name (e.g., `stylex` from `import stylex from '...'`)
stylex_import: Option,
- /// Tracked StyleX named imports (e.g., `create` from `import { create } from '...'`)
+ /// Tracked `StyleX` named imports (e.g., `create` from `import { create } from '...'`)
stylex_named_imports: FxHashMap,
- /// Pending StyleX namespace map from the most recent stylex.create() call.
- /// Set in visit_expression, consumed in visit_variable_declarator.
+ /// Pending `StyleX` namespace map from the most recent `stylex.create()` call.
+ /// Set in `visit_expression`, consumed in `visit_variable_declarator`.
stylex_pending_create: Option>,
- /// Maps variable names to their namespace→className mappings from stylex.create().
+ /// Maps variable names to their namespace→className mappings from `stylex.create()`.
/// e.g., "styles" → { "base" → "a b", "active" → "c" }
stylex_namespaces: FxHashMap>,
- /// Pending keyframe animation name from most recent stylex.keyframes() call.
+ /// Pending keyframe animation name from most recent `stylex.keyframes()` call.
stylex_pending_keyframe_name: Option,
/// Maps variable names to their keyframe animation names.
/// e.g., "fadeIn" → "a-a"
stylex_keyframe_names: FxHashMap,
- /// Pending JSXFragment children from dynamic `as` prop resolution.
+ /// Pending `JSXFragment` children from dynamic `as` prop resolution.
/// Set in `visit_jsx_element`, consumed in `visit_expression` to replace
/// `Expression::JSXElement` with `Expression::JSXFragment`.
pending_fragment_children: Option>>,
@@ -126,7 +126,10 @@ impl<'a> DevupVisitor<'a> {
}
// Check named import call: create(...)
if let Expression::Identifier(ident) = callee
- && let Some(StylexFunction::Create) = self.stylex_named_imports.get(ident.name.as_str())
+ && matches!(
+ self.stylex_named_imports.get(ident.name.as_str()),
+ Some(StylexFunction::Create)
+ )
{
return true;
}
@@ -146,14 +149,17 @@ impl<'a> DevupVisitor<'a> {
}
// Check named import call: props(...)
if let Expression::Identifier(ident) = callee
- && let Some(StylexFunction::Props) = self.stylex_named_imports.get(ident.name.as_str())
+ && matches!(
+ self.stylex_named_imports.get(ident.name.as_str()),
+ Some(StylexFunction::Props)
+ )
{
return true;
}
false
}
- /// Check if a callee is stylex.keyframes() or named keyframes() call.
+ /// Check if a callee is `stylex.keyframes()` or named `keyframes()` call.
fn is_stylex_keyframes_call(&self, callee: &Expression) -> bool {
if let Some(stylex_name) = &self.stylex_import
&& let Expression::StaticMemberExpression(member) = callee
@@ -164,25 +170,27 @@ impl<'a> DevupVisitor<'a> {
return true;
}
if let Expression::Identifier(ident) = callee
- && let Some(StylexFunction::Keyframes) =
- self.stylex_named_imports.get(ident.name.as_str())
+ && matches!(
+ self.stylex_named_imports.get(ident.name.as_str()),
+ Some(StylexFunction::Keyframes)
+ )
{
return true;
}
false
}
- /// Resolve stylex.props() arguments to className expressions and style properties.
- /// Returns (class_exprs, style_props) where style_props are CSS variable assignments
+ /// Resolve `stylex.props()` arguments to className expressions and style properties.
+ /// Returns (`class_exprs`, `style_props`) where `style_props` are CSS variable assignments
/// from dynamic namespace calls like `styles.bar(h)`.
fn resolve_stylex_props_args(
&self,
- arguments: &mut oxc_allocator::Vec<'a, Argument<'a>>,
+ arguments: &oxc_allocator::Vec<'a, Argument<'a>>,
) -> (Vec>, Vec>) {
let mut class_exprs: Vec> = vec![];
let mut style_props: Vec> = vec![];
- for arg in arguments.iter() {
+ for arg in arguments {
let expr = arg.to_expression();
// Check for dynamic namespace call first: styles.bar(h)
if let Expression::CallExpression(call) = expr
@@ -200,7 +208,7 @@ impl<'a> DevupVisitor<'a> {
(class_exprs, style_props)
}
- /// Resolve a dynamic namespace call like `styles.bar(h)` to (className, style_props).
+ /// Resolve a dynamic namespace call like `styles.bar(h)` to (className, `style_props`).
fn resolve_stylex_dynamic_call(
&self,
call: &CallExpression<'a>,
@@ -241,7 +249,7 @@ impl<'a> DevupVisitor<'a> {
}
}
- /// Resolve a single stylex.props() argument to a className expression.
+ /// Resolve a single `stylex.props()` argument to a className expression.
fn resolve_stylex_arg(&self, expr: &Expression<'a>) -> Option> {
match expr {
// styles.base → StaticMemberExpression
@@ -313,7 +321,6 @@ impl<'a> DevupVisitor<'a> {
}
// false, null, undefined, 0, "" → falsy, skip
Expression::BooleanLiteral(b) if !b.value => None,
- Expression::NullLiteral(_) => None,
Expression::NumericLiteral(n) if n.value == 0.0 => None,
Expression::StringLiteral(s) if s.value.is_empty() => None,
// Anything else we can't resolve → skip
@@ -456,7 +463,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
if class_name_str.is_empty() {
class_name_str = included_class;
} else {
- class_name_str = format!("{} {}", included_class, class_name_str);
+ class_name_str = format!("{included_class} {class_name_str}");
}
}
}
@@ -524,7 +531,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
if let Expression::CallExpression(call) = it
&& self.is_stylex_props_call(&call.callee)
{
- let (class_exprs, style_props) = self.resolve_stylex_props_args(&mut call.arguments);
+ let (class_exprs, style_props) = self.resolve_stylex_props_args(&call.arguments);
// Build className expression using existing merge utility
let class_name_expr = merge_expression_for_class_name(&self.ast, class_exprs)
@@ -576,19 +583,9 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
if let Some(util_import_key) = util_import_key
&& let Some(util_type) = self.util_imports.get(&util_import_key)
{
- if call.arguments.len() != 1 {
- *it = match util_type.as_ref() {
- UtilType::Css | UtilType::Keyframes => {
- self.ast
- .expression_string_literal(SPAN, self.ast.str(""), None)
- }
- UtilType::GlobalCss => {
- self.ast.expression_identifier(SPAN, self.ast.str(""))
- }
- };
- } else {
+ if call.arguments.len() == 1 {
let r = util_type.as_ref();
- *it = if let UtilType::Css = r {
+ *it = if matches!(r, UtilType::Css) {
let ExtractResult {
mut styles,
style_order,
@@ -628,7 +625,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
.expression_string_literal(SPAN, self.ast.str(""), None)
}
}
- } else if let UtilType::Keyframes = r {
+ } else if matches!(r, UtilType::Keyframes) {
let KeyframesExtractResult { keyframes } =
extract_keyframes_from_expression(
&self.ast,
@@ -668,6 +665,16 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
}));
self.ast.expression_identifier(SPAN, self.ast.str(""))
}
+ } else {
+ *it = match util_type.as_ref() {
+ UtilType::Css | UtilType::Keyframes => {
+ self.ast
+ .expression_string_literal(SPAN, self.ast.str(""), None)
+ }
+ UtilType::GlobalCss => {
+ self.ast.expression_identifier(SPAN, self.ast.str(""))
+ }
+ };
}
}
} else if let Expression::TaggedTemplateExpression(tag) = it
@@ -676,13 +683,13 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
{
let css_str = {
let mut s = String::new();
- for quasi in tag.quasi.quasis.iter() {
+ for quasi in &tag.quasi.quasis {
s.push_str(quasi.value.raw.as_str());
}
s
};
let r = css_type.as_ref();
- *it = if let UtilType::Css = r {
+ *it = if matches!(r, UtilType::Css) {
let styles = css_to_style_literal(&tag.quasi, 0, &None);
let class_name = gen_class_names(
&self.ast,
@@ -694,7 +701,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
self.split_filename.as_deref(),
);
- self.styles.extend(styles.into_iter().map(|ex| ex.into()));
+ self.styles.extend(styles.into_iter().map(Into::into));
if let Some(cls) = class_name {
cls
} else {
@@ -702,7 +709,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
.expression_string_literal(SPAN, self.ast.str(""), None)
}
// already set style order
- } else if let UtilType::Keyframes = r {
+ } else if matches!(r, UtilType::Keyframes) {
let keyframes = ExtractKeyframes {
keyframes: keyframes_to_keyframes_style(&css_str),
};
@@ -864,7 +871,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
s.set_style_order(*order);
}
s
- }))
+ }));
});
alt_props_styles.iter().rev().for_each(|style| {
self.styles.extend(style.extract().into_iter().map(|mut s| {
@@ -872,7 +879,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
s.set_style_order(*order);
}
s
- }))
+ }));
});
} else {
let style_order = parsed_style_order.as_static();
@@ -882,7 +889,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
s.set_style_order(order);
});
s
- }))
+ }));
});
if let Expression::ObjectExpression(obj) = it.arguments[1].to_expression_mut() {
@@ -1103,10 +1110,10 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
if !duplicate_set.contains(&name) {
duplicate_set.insert(name.clone());
if property_name == "styleOrder" {
- parsed_style_order = jsx_expression_to_style_order(
- attr.value.as_ref().unwrap(),
- self.ast.allocator,
- );
+ if let Some(value) = attr.value.as_ref() {
+ parsed_style_order =
+ jsx_expression_to_style_order(value, self.ast.allocator);
+ }
} else if property_name == "props" {
if let Some(value) = attr.value.as_ref()
&& let JSXAttributeValue::ExpressionContainer(expr) = value
@@ -1139,10 +1146,10 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
&None,
LiteralHandling::ExpandResponsiveThemeToken,
);
- if !styles.is_empty() {
- props_styles.extend(styles.into_iter().rev());
- } else {
+ if styles.is_empty() {
attrs.insert(i, attr);
+ } else {
+ props_styles.extend(styles.into_iter().rev());
}
} else {
attrs.insert(i, attr);
@@ -1190,7 +1197,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
s.set_style_order(*order);
}
s
- }))
+ }));
});
alt_props_styles.iter().rev().for_each(|style| {
self.styles.extend(style.extract().into_iter().map(|mut s| {
@@ -1198,7 +1205,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
s.set_style_order(*order);
}
s
- }))
+ }));
});
} else {
let style_order = parsed_style_order.as_static();
@@ -1244,7 +1251,7 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
elem.opening_element.name = ident.clone_in(self.ast.allocator);
if let Some(el) = &mut elem.closing_element {
- el.name = ident
+ el.name = ident;
}
}
}
diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml
index a1774513..fb96383e 100644
--- a/libs/sheet/Cargo.toml
+++ b/libs/sheet/Cargo.toml
@@ -2,6 +2,14 @@
name = "sheet"
version = "0.1.0"
edition = "2024"
+description = "CSS sheet generation and theme system for Devup UI"
+license = "Apache-2.0"
+repository = "https://github.com/dev-five-git/devup-ui"
+keywords = ["css", "theme", "stylesheet", "react", "devup-ui"]
+categories = ["development-tools", "wasm", "web-programming"]
+
+[lints]
+workspace = true
[dependencies]
css = { path = "../css" }
diff --git a/libs/sheet/benches/my_benchmark.rs b/libs/sheet/benches/my_benchmark.rs
index a88d66ba..c4dc6617 100644
--- a/libs/sheet/benches/my_benchmark.rs
+++ b/libs/sheet/benches/my_benchmark.rs
@@ -1,12 +1,17 @@
use criterion::{Criterion, criterion_group, criterion_main};
use regex_lite::Regex;
+use sheet::StyleSheet;
+use sheet::theme::{ColorTheme, Theme, Typography};
use std::hint::black_box;
use std::sync::LazyLock;
-static VAR_RE: LazyLock = LazyLock::new(|| Regex::new(r"\$\w+").unwrap());
+static VAR_RE: LazyLock = LazyLock::new(|| {
+ Regex::new(r"\$\w+")
+ .unwrap_or_else(|err| panic!("invalid built-in regex pattern `\\$\\w+`: {err}"))
+});
fn convert_theme_variable_value_a(value: &str) -> String {
- if value.contains("$") {
+ if value.contains('$') {
VAR_RE
.replace_all(value, |caps: ®ex_lite::Captures| {
format!("var(--{})", &caps[0][1..])
@@ -25,6 +30,81 @@ fn convert_theme_variable_value_b(value: &str) -> String {
.to_string()
}
+fn make_large_theme() -> Theme {
+ let mut theme = Theme::default();
+ let mut default_colors = ColorTheme::default();
+ let mut dark_colors = ColorTheme::default();
+
+ for idx in 0..80 {
+ let name = format!("color.{idx}");
+ default_colors.add_color(&name, &format!("#{idx:02x}{idx:02x}{idx:02x}"));
+ dark_colors.add_color(
+ &name,
+ &format!("#{:02x}{:02x}{:02x}", 255 - idx, 255 - idx, 255 - idx),
+ );
+ }
+ theme.add_color_theme("default", default_colors);
+ theme.add_color_theme("dark", dark_colors);
+
+ for idx in 0..80 {
+ theme.add_length(
+ "default",
+ &format!("space{idx}"),
+ vec![
+ Some(format!("{}px", idx + 1)),
+ Some(format!("{}px", idx + 2)),
+ None,
+ Some(format!("{}px", idx + 4)),
+ ],
+ );
+ theme.add_shadow(
+ "default",
+ &format!("shadow{idx}"),
+ vec![
+ Some(format!("0 {}px {}px #0003", idx + 1, idx + 2)),
+ None,
+ Some(format!("0 {}px {}px #0004", idx + 2, idx + 4)),
+ ],
+ );
+ }
+
+ for idx in 0..40 {
+ theme.add_typography(
+ &format!("type{idx}"),
+ vec![
+ Some(Typography::new(
+ Some("Inter".to_string()),
+ Some(format!("{}px", 12 + idx)),
+ Some("600".to_string()),
+ Some("1.4".to_string()),
+ None,
+ )),
+ Some(Typography::new(
+ Some("Inter".to_string()),
+ Some(format!("{}px", 14 + idx)),
+ Some("700".to_string()),
+ Some("1.5".to_string()),
+ Some("0.01em".to_string()),
+ )),
+ ],
+ );
+ }
+
+ theme
+}
+
+fn make_large_sheet() -> StyleSheet {
+ let mut sheet = StyleSheet::default();
+ sheet.set_theme(make_large_theme());
+ for idx in 0..300 {
+ let class_name = format!("c{idx}");
+ let property = if idx % 2 == 0 { "color" } else { "background" };
+ let value = if idx % 3 == 0 { "$color.1" } else { "red" };
+ sheet.add_property(&class_name, property, 0, value, None, None, Some("app.tsx"));
+ }
+ sheet
+}
+
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("convert_theme_variable_value_a", |b| {
b.iter(|| {
@@ -32,7 +112,7 @@ fn criterion_benchmark(c: &mut Criterion) {
convert_theme_variable_value_a(black_box("red"));
convert_theme_variable_value_a(black_box("solid 2px red"));
convert_theme_variable_value_a(black_box("solid 2px $primary"));
- })
+ });
});
c.bench_function("convert_theme_variable_value_b", |b| {
@@ -41,7 +121,17 @@ fn criterion_benchmark(c: &mut Criterion) {
convert_theme_variable_value_b(black_box("red"));
convert_theme_variable_value_b(black_box("solid 2px red"));
convert_theme_variable_value_b(black_box("solid 2px $primary"));
- })
+ });
+ });
+
+ c.bench_function("theme_to_css_large", |b| {
+ let theme = make_large_theme();
+ b.iter(|| black_box(theme.to_css()));
+ });
+
+ c.bench_function("sheet_create_css_large", |b| {
+ let sheet = make_large_sheet();
+ b.iter(|| black_box(sheet.create_css(Some("app.tsx"), false)));
});
}
diff --git a/libs/sheet/src/lib.rs b/libs/sheet/src/lib.rs
index 746ed990..8a7cefec 100644
--- a/libs/sheet/src/lib.rs
+++ b/libs/sheet/src/lib.rs
@@ -17,11 +17,13 @@ use serde::{Deserialize, Deserializer, Serialize};
use std::borrow::Cow;
use std::cmp::Ordering::Equal;
use std::collections::{BTreeMap, BTreeSet};
-use std::fmt::Write;
use std::sync::LazyLock;
-trait ExtractStyle {
- fn extract(&self) -> String;
+macro_rules! push_fmt {
+ ($target:expr, $($arg:tt)*) => {{
+ // `std::fmt::Write::write_fmt` on `&mut String` is infallible; discard result.
+ let _ = std::fmt::Write::write_fmt($target, format_args!($($arg)*));
+ }};
}
#[derive(Debug, Hash, Eq, PartialEq, Deserialize, Serialize, Clone)]
@@ -35,7 +37,7 @@ pub struct StyleSheetProperty {
pub value: String,
#[serde(rename = "s")]
pub selector: Option,
- /// CSS layer name (from vanilla-extract layer())
+ /// CSS layer name (from vanilla-extract `layer()`)
#[serde(rename = "l", skip_serializing_if = "Option::is_none")]
pub layer: Option,
}
@@ -72,20 +74,25 @@ impl Ord for StyleSheetProperty {
}
}
-impl ExtractStyle for StyleSheetProperty {
- fn extract(&self) -> String {
- format!(
- "{}{{{}:{}}}",
- merge_selector(&self.class_name, self.selector.as_ref()),
- self.property,
- convert_theme_variable_value(&self.value)
- )
+impl StyleSheetProperty {
+ fn write_extract(&self, css: &mut String) {
+ css.push_str(&merge_selector(&self.class_name, self.selector.as_ref()));
+ css.push('{');
+ css.push_str(&self.property);
+ css.push(':');
+ css.push_str(&convert_theme_variable_value(&self.value));
+ css.push('}');
}
}
-static VAR_RE: LazyLock = LazyLock::new(|| Regex::new(r"\$[\w.]+").unwrap());
+fn compile_regex(pattern: &str) -> Regex {
+ Regex::new(pattern)
+ .unwrap_or_else(|err| panic!("invalid built-in regex pattern `{pattern}`: {err}"))
+}
+
+static VAR_RE: LazyLock = LazyLock::new(|| compile_regex(r"\$[\w.]+"));
static INTERFACE_KEY_RE: LazyLock =
- LazyLock::new(|| Regex::new(r"^[a-zA-Z_$][a-zA-Z0-9_$]*$").unwrap());
+ LazyLock::new(|| compile_regex(r"^[a-zA-Z_$][a-zA-Z0-9_$]*$"));
/// Cached header string — computed once from compile-time included package.json
static HEADER: LazyLock = LazyLock::new(|| {
@@ -94,12 +101,10 @@ static HEADER: LazyLock = LazyLock::new(|| {
version = include_str!("../../../bindings/devup-ui-wasm/package.json")
.lines()
.find(|line| line.contains("\"version\""))
- .unwrap()
- .split(":")
- .nth(1)
- .unwrap()
+ .and_then(|line| line.split(':').nth(1))
+ .unwrap_or("\"unknown\"")
.trim()
- .replace("\"", ""),
+ .replace('"', ""),
)
});
@@ -107,7 +112,7 @@ fn convert_interface_key(key: &str) -> String {
if INTERFACE_KEY_RE.is_match(key) {
key.to_string()
} else {
- format!("[`{}`]", key.replace("`", "\\`"))
+ format!("[`{}`]", key.replace('`', "\\`"))
}
}
@@ -130,12 +135,6 @@ pub struct StyleSheetCss {
pub css: String,
}
-impl ExtractStyle for StyleSheetCss {
- fn extract(&self) -> String {
- self.css.clone()
- }
-}
-
type PropertyMap = BTreeMap>>;
type KeyframesMap = BTreeMap>>>;
@@ -153,9 +152,9 @@ where
{
let mut tmp_map: PropertyMap = BTreeMap::new();
- for (key, value) in value.into_iter() {
+ for (key, value) in value {
let mut inner_tmp_map = BTreeMap::new();
- for (key, value) in value.into_iter() {
+ for (key, value) in value {
inner_tmp_map.insert(key.parse().map_err(Error::custom)?, value);
}
tmp_map.insert(key.parse().map_err(Error::custom)?, inner_tmp_map);
@@ -237,7 +236,7 @@ impl StyleSheet {
property: property.to_string(),
value: value.to_string(),
selector: selector.cloned(),
- layer: layer.map(|s| s.to_string()),
+ layer: layer.map(ToString::to_string),
})
}
@@ -341,7 +340,7 @@ impl StyleSheet {
) -> (bool, bool) {
let mut collected = false;
let mut updated_base_style = false;
- for style in styles.iter() {
+ for style in styles {
match style {
ExtractStyleValue::Static(st) => {
let resolved_value =
@@ -351,13 +350,11 @@ impl StyleSheet {
"box-shadow" => self
.theme
.get_default_shadow_value(token)
- .map(str::to_string)
- .unwrap_or_else(|| st.value().to_string()),
+ .map_or_else(|| st.value().to_string(), str::to_string),
_ => self
.theme
.get_default_length_value(token)
- .map(str::to_string)
- .unwrap_or_else(|| st.value().to_string()),
+ .map_or_else(|| st.value().to_string(), str::to_string),
}
} else {
st.value().to_string()
@@ -375,10 +372,10 @@ impl StyleSheet {
Some(&resolved_value),
selector.as_deref(),
st.style_order(),
- if !single_css { Some(filename) } else { None },
+ if single_css { None } else { Some(filename) },
)
} else {
- match st.extract(if !single_css { Some(filename) } else { None }) {
+ match st.extract(if single_css { None } else { Some(filename) }) {
StyleProperty::ClassName(cls)
| StyleProperty::Variable {
class_name: cls, ..
@@ -393,7 +390,7 @@ impl StyleSheet {
&resolved_value,
st.selector(),
st.style_order(),
- if !single_css { Some(filename) } else { None },
+ if single_css { None } else { Some(filename) },
st.layer(),
) {
collected = true;
@@ -407,19 +404,19 @@ impl StyleSheet {
class_name,
variable_name,
..
- }) = style.extract(if !single_css { Some(filename) } else { None })
+ }) = style.extract(if single_css { None } else { Some(filename) })
&& self.add_property(
&class_name,
dy.property(),
dy.level(),
&if dy.important() {
- format!("var({}) !important", variable_name)
+ format!("var({variable_name}) !important")
} else {
- format!("var({})", variable_name)
+ format!("var({variable_name})")
},
dy.selector(),
dy.style_order(),
- if !single_css { Some(filename) } else { None },
+ if single_css { None } else { Some(filename) },
)
{
collected = true;
@@ -432,7 +429,7 @@ impl StyleSheet {
ExtractStyleValue::Keyframes(keyframes) => {
if self.add_keyframes(
&keyframes
- .extract(if !single_css { Some(filename) } else { None })
+ .extract(if single_css { None } else { Some(filename) })
.to_string(),
keyframes
.keyframes
@@ -452,7 +449,7 @@ impl StyleSheet {
)
})
.collect(),
- if !single_css { Some(filename) } else { None },
+ if single_css { None } else { Some(filename) },
) {
collected = true;
}
@@ -475,6 +472,7 @@ impl StyleSheet {
(collected, updated_base_style)
}
+ #[must_use]
pub fn create_interface(
&self,
package_name: &str,
@@ -512,16 +510,26 @@ impl StyleSheet {
String::new()
} else {
let dollar_keys = |keys: BTreeSet| {
- keys.into_iter()
- .map(|key| format!("{}:null", convert_interface_key(&format!("${key}"))))
- .collect::>()
- .join(";")
+ let mut contents = String::new();
+ for key in keys {
+ if !contents.is_empty() {
+ contents.push(';');
+ }
+ contents.push_str(&convert_interface_key(&format!("${key}")));
+ contents.push_str(":null");
+ }
+ contents
};
let plain_keys = |keys: BTreeSet| {
- keys.into_iter()
- .map(|key| format!("{}:null", convert_interface_key(&key)))
- .collect::>()
- .join(";")
+ let mut contents = String::new();
+ for key in keys {
+ if !contents.is_empty() {
+ contents.push(';');
+ }
+ contents.push_str(&convert_interface_key(&key));
+ contents.push_str(":null");
+ }
+ contents
};
format!(
"import \"{}\";declare module \"{}\"{{interface {}{{{}}}interface {}{{{}}}interface {}{{{}}}interface {}{{{}}}interface {}{{{}}}}}",
@@ -550,9 +558,9 @@ impl StyleSheet {
layered_styles: &mut BTreeMap>, // layer -> Vec<(selector, property, value)>
) -> String {
// Estimate ~64 bytes per property for pre-allocation
- let prop_count: usize = map.values().map(|s| s.len()).sum();
+ let prop_count: usize = map.values().map(FxHashSet::len).sum();
let mut current_css = String::with_capacity(prop_count * 64);
- for (level, props) in map.iter() {
+ for (level, props) in map {
let (mut global_props, rest): (Vec<_>, Vec<_>) = props
.iter()
.partition(|prop| matches!(prop.selector, Some(StyleSelector::Global(_, _))));
@@ -583,8 +591,10 @@ impl StyleSheet {
.iter()
.enumerate()
.find(|(idx, _)| (*idx as u8) == *level)
- .map(|(_, bp)| *bp)
- .unwrap_or_else(|| self.theme.breakpoints.last().cloned().unwrap_or(0)),
+ .map_or_else(
+ || self.theme.breakpoints.last().copied().unwrap_or(0),
+ |(_, bp)| *bp,
+ ),
)
};
@@ -616,7 +626,7 @@ impl StyleSheet {
}
}
if let Some(break_point) = break_point {
- write!(current_css, "@media(min-width:{break_point}px){{").unwrap();
+ push_fmt!(&mut current_css, "@media(min-width:{break_point}px){{");
}
for (selector, props) in selector_map {
current_css.push_str(selector);
@@ -641,10 +651,10 @@ impl StyleSheet {
if !sorted_props.is_empty() {
if let Some(break_point) = break_point {
- write!(current_css, "@media(min-width:{break_point}px){{").unwrap();
+ push_fmt!(&mut current_css, "@media(min-width:{break_point}px){{");
}
for prop in sorted_props {
- current_css.push_str(&prop.extract());
+ prop.write_extract(&mut current_css);
}
if break_point.is_some() {
current_css.push('}');
@@ -654,50 +664,46 @@ impl StyleSheet {
if let Some(break_point) = break_point {
match kind {
AtRuleKind::Media => {
- write!(
- current_css,
+ push_fmt!(
+ &mut current_css,
"@media(min-width:{break_point}px)and {query}{{"
- )
- .unwrap();
+ );
}
AtRuleKind::Supports => {
- write!(
- current_css,
+ push_fmt!(
+ &mut current_css,
"@media(min-width:{break_point}px){{@supports{query}{{"
- )
- .unwrap();
+ );
}
AtRuleKind::Container => {
- write!(
- current_css,
+ push_fmt!(
+ &mut current_css,
"@media(min-width:{break_point}px){{@container{query}{{"
- )
- .unwrap();
+ );
}
AtRuleKind::Layer => {
- write!(
- current_css,
+ push_fmt!(
+ &mut current_css,
"@media(min-width:{break_point}px){{@layer {query}{{"
- )
- .unwrap();
+ );
}
}
for prop in props {
- current_css.push_str(&prop.extract());
+ prop.write_extract(&mut current_css);
}
match kind {
AtRuleKind::Media => current_css.push('}'),
_ => current_css.push_str("}}"),
}
} else {
- write!(current_css, "@{kind}").unwrap();
+ push_fmt!(&mut current_css, "@{kind}");
if query.starts_with('(') {
- write!(current_css, "{query}{{").unwrap();
+ push_fmt!(&mut current_css, "{query}{{");
} else {
- write!(current_css, " {query}{{").unwrap();
+ push_fmt!(&mut current_css, " {query}{{");
}
for prop in props {
- current_css.push_str(&prop.extract());
+ prop.write_extract(&mut current_css);
}
current_css.push('}');
}
@@ -707,18 +713,19 @@ impl StyleSheet {
}
#[inline]
- fn create_header(&self) -> &'static str {
+ fn create_header() -> &'static str {
&HEADER
}
+ #[must_use]
pub fn create_css(&self, filename: Option<&str>, import_main_css: bool) -> String {
let mut css = String::with_capacity(4096);
- css.push_str(self.create_header());
+ css.push_str(Self::create_header());
for import in self.imports.values().flatten() {
if import.starts_with('"') {
- write!(css, "@import {import};").unwrap();
+ push_fmt!(&mut css, "@import {import};");
} else {
- write!(css, "@import \"{import}\";").unwrap();
+ push_fmt!(&mut css, "@import \"{import}\";");
}
}
@@ -748,11 +755,12 @@ impl StyleSheet {
let has_orders = !style_orders.is_empty();
if has_base || has_theme || has_orders {
css.push_str("@layer ");
- let mut first = true;
- if has_base {
+ let mut first = if has_base {
css.push('b');
- first = false;
- }
+ false
+ } else {
+ true
+ };
if has_theme {
if !first {
css.push(',');
@@ -765,31 +773,31 @@ impl StyleSheet {
css.push(',');
}
first = false;
- write!(css, "o{v}").unwrap();
+ push_fmt!(&mut css, "o{v}");
}
css.push(';');
}
if !theme_css.is_empty() {
- write!(css, "@layer t{{{theme_css}}}").unwrap();
+ push_fmt!(&mut css, "@layer t{{{theme_css}}}");
}
- for (_, font_faces) in self.font_faces.iter() {
- for font_face in font_faces.iter() {
+ for font_faces in self.font_faces.values() {
+ for font_face in font_faces {
css.push_str("@font-face{");
let mut first = true;
- for (key, value) in font_face.iter() {
+ for (key, value) in font_face {
if !first {
css.push(';');
}
first = false;
- write!(css, "{key}:{value}").unwrap();
+ push_fmt!(&mut css, "{key}:{value}");
}
css.push('}');
}
}
// global css
- for (_, _css) in self.css.iter() {
- for _css in _css.iter() {
+ for _css in self.css.values() {
+ for _css in _css {
css.push_str(&_css.css);
}
}
@@ -799,7 +807,7 @@ impl StyleSheet {
BTreeMap::new();
let base_css = self.create_style_with_layers(&base_styles, &mut layered_styles);
if !base_css.is_empty() {
- write!(css, "@layer b{{{base_css}}}").unwrap();
+ push_fmt!(&mut css, "@layer b{{{base_css}}}");
}
// Generate @layer declarations and wrapped styles for custom layers
@@ -827,7 +835,7 @@ impl StyleSheet {
.push((property, value));
}
- write!(css, "@layer {layer_name}{{").unwrap();
+ push_fmt!(&mut css, "@layer {layer_name}{{");
for (selector, props) in selector_map {
css.push_str(&selector);
css.push('{');
@@ -837,7 +845,7 @@ impl StyleSheet {
css.push(';');
}
first = false;
- write!(css, "{p}:{v}").unwrap();
+ push_fmt!(&mut css, "{p}:{v}");
}
css.push('}');
}
@@ -854,16 +862,16 @@ impl StyleSheet {
if let Some(keyframes) = self.keyframes.get(filename.unwrap_or_default()) {
for (name, map) in keyframes {
- write!(css, "@keyframes {name}{{").unwrap();
- for (key, props) in map.iter() {
- write!(css, "{key}{{").unwrap();
+ push_fmt!(&mut css, "@keyframes {name}{{");
+ for (key, props) in map {
+ push_fmt!(&mut css, "{key}{{");
let mut first = true;
- for (k, v) in props.iter() {
+ for (k, v) in props {
if !first {
css.push(';');
}
first = false;
- write!(css, "{k}:{v}").unwrap();
+ push_fmt!(&mut css, "{k}:{v}");
}
css.push('}');
}
@@ -873,7 +881,7 @@ impl StyleSheet {
// order
if let Some(maps) = self.properties.get(filename.unwrap_or_default()) {
- for (style_order, map) in maps.iter() {
+ for (style_order, map) in maps {
if *style_order == 0 {
// base style was created in global css
continue;
@@ -885,7 +893,7 @@ impl StyleSheet {
if *style_order == 255 {
css.push_str(¤t_css);
} else {
- write!(css, "@layer o{style_order}{{{current_css}}}").unwrap();
+ push_fmt!(&mut css, "@layer o{style_order}{{{current_css}}}");
}
}
}
@@ -895,6 +903,7 @@ impl StyleSheet {
}
#[cfg(test)]
+#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use crate::theme::{ColorTheme, Typography};
@@ -2030,6 +2039,37 @@ mod tests {
"ShadowsInterface",
"ThemeInterface"
));
+
+ // Multiple typography keys + multiple color themes exercise the
+ // `plain_keys` semicolon separator (joins 2+ entries).
+ let mut sheet = StyleSheet::default();
+ let mut theme = Theme::default();
+ let mut light_theme = ColorTheme::default();
+ light_theme.add_color("primary", "#000");
+ let mut dark_theme = ColorTheme::default();
+ dark_theme.add_color("primary", "#fff");
+ theme.add_color_theme("default", light_theme);
+ theme.add_color_theme("dark", dark_theme);
+ let make_typography = || {
+ Typography::new(
+ Some("Arial".to_string()),
+ Some("16px".to_string()),
+ Some("400".to_string()),
+ Some("1.5".to_string()),
+ Some("0.5".to_string()),
+ )
+ };
+ theme.add_typography("heading", vec![Some(make_typography())]);
+ theme.add_typography("body", vec![Some(make_typography())]);
+ sheet.set_theme(theme);
+ assert_debug_snapshot!(sheet.create_interface(
+ "package",
+ "ColorInterface",
+ "TypographyInterface",
+ "LengthInterface",
+ "ShadowsInterface",
+ "ThemeInterface"
+ ));
}
#[test]
@@ -2037,27 +2077,11 @@ mod tests {
let mut sheet = StyleSheet::default();
let mut keyframes: BTreeMap> = BTreeMap::new();
- let mut from_props = BTreeSet::new();
- from_props.insert(StyleSheetProperty {
- class_name: String::from("test"),
- property: String::from("opacity"),
- value: String::from("0"),
- selector: None,
- layer: None,
- });
keyframes.insert(
String::from("from"),
vec![(String::from("opacity"), String::from("0"))],
);
- let mut to_props = BTreeSet::new();
- to_props.insert(StyleSheetProperty {
- class_name: String::from("test"),
- property: String::from("opacity"),
- value: String::from("1"),
- selector: None,
- layer: None,
- });
keyframes.insert(
String::from("to"),
vec![(String::from("opacity"), String::from("1"))],
@@ -2068,27 +2092,11 @@ mod tests {
assert_debug_snapshot!(past.split("*/").nth(1).unwrap());
let mut keyframes: BTreeMap> = BTreeMap::new();
- let mut from_props = BTreeSet::new();
- from_props.insert(StyleSheetProperty {
- class_name: String::from("test"),
- property: String::from("opacity"),
- value: String::from("0"),
- selector: None,
- layer: None,
- });
keyframes.insert(
String::from("from"),
vec![(String::from("opacity"), String::from("0"))],
);
- let mut to_props = BTreeSet::new();
- to_props.insert(StyleSheetProperty {
- class_name: String::from("test"),
- property: String::from("opacity"),
- value: String::from("1"),
- selector: None,
- layer: None,
- });
keyframes.insert(
String::from("to"),
vec![(String::from("opacity"), String::from("1"))],
@@ -2297,14 +2305,30 @@ mod tests {
}
#[test]
- fn test_stylesheet_css_extract() {
+ fn test_stylesheet_css_struct() {
let css_entry = StyleSheetCss {
css: "div{display:flex}".to_string(),
};
- assert_eq!(css_entry.extract(), "div{display:flex}");
+ assert_eq!(css_entry.css, "div{display:flex}");
let empty = StyleSheetCss { css: String::new() };
- assert_eq!(empty.extract(), "");
+ assert_eq!(empty.css, "");
+ }
+
+ #[test]
+ fn test_stylesheet_property_ord_no_selectors() {
+ // Both sides without selectors: branches on property then value.
+ let make = |property: &str, value: &str| StyleSheetProperty {
+ class_name: "a".to_string(),
+ property: property.to_string(),
+ value: value.to_string(),
+ selector: None,
+ layer: None,
+ };
+ assert_eq!(make("color", "red").cmp(&make("color", "red")), Equal);
+ assert!(make("color", "red") < make("color", "white"));
+ assert!(make("color", "red") < make("display", "block"));
+ assert!(make("display", "block") > make("color", "white"));
}
#[test]
diff --git a/libs/sheet/src/snapshots/sheet__tests__get_theme_interface-5.snap b/libs/sheet/src/snapshots/sheet__tests__get_theme_interface-5.snap
new file mode 100644
index 00000000..2c5ac937
--- /dev/null
+++ b/libs/sheet/src/snapshots/sheet__tests__get_theme_interface-5.snap
@@ -0,0 +1,5 @@
+---
+source: libs/sheet/src/lib.rs
+expression: "sheet.create_interface(\"package\", \"ColorInterface\", \"TypographyInterface\",\n\"LengthInterface\", \"ShadowsInterface\", \"ThemeInterface\")"
+---
+"import \"package\";declare module \"package\"{interface ColorInterface{$primary:null}interface TypographyInterface{body:null;heading:null}interface LengthInterface{}interface ShadowsInterface{}interface ThemeInterface{dark:null;default:null}}"
diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs
index 2b878922..3d8608a2 100644
--- a/libs/sheet/src/theme.rs
+++ b/libs/sheet/src/theme.rs
@@ -2,9 +2,9 @@ use css::optimize_value::optimize_value;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
-use std::fmt::Write;
+use std::fmt::Write as _;
-/// ColorEntry stores both the original key (for TypeScript interface) and CSS key (for CSS variables)
+/// `ColorEntry` stores both the original key (for TypeScript interface) and CSS key (for CSS variables)
#[derive(Debug, Clone, Serialize)]
pub struct ColorEntry {
/// Original key with dots for TypeScript interface (e.g., "gray.100")
@@ -15,20 +15,20 @@ pub struct ColorEntry {
pub value: String,
}
-/// ColorTheme stores flattened color entries
+/// `ColorTheme` stores flattened color entries
/// Supports:
-/// - Simple: `primary: "#000"` -> interface_key: "primary", css_key: "primary"
-/// - Dot notation: `"primary.100": "#000"` -> interface_key: "primary.100", css_key: "primary-100"
-/// - Nested object: `hello: { 100: "#000" }` -> interface_key: "hello.100", css_key: "hello-100"
-/// - Deep nested: `gray: { light: { 100: "#000" } }` -> interface_key: "gray.light.100", css_key: "gray-light-100"
+/// - Simple: `primary: "#000"` -> `interface_key`: "primary", `css_key`: "primary"
+/// - Dot notation: `"primary.100": "#000"` -> `interface_key`: "primary.100", `css_key`: "primary-100"
+/// - Nested object: `hello: { 100: "#000" }` -> `interface_key`: "hello.100", `css_key`: "hello-100"
+/// - Deep nested: `gray: { light: { 100: "#000" } }` -> `interface_key`: "gray.light.100", `css_key`: "gray-light-100"
#[derive(Default, Serialize, Debug)]
pub struct ColorTheme {
- /// Map from css_key to ColorEntry for quick lookup
+ /// Map from `css_key` to `ColorEntry` for quick lookup
entries: HashMap,
}
-/// Recursively flatten a JSON value into ColorEntry list
-/// interface_prefix uses dots, css_prefix uses dashes
+/// Recursively flatten a JSON value into `ColorEntry` list
+/// `interface_prefix` uses dots, `css_prefix` uses dashes
fn flatten_color_value(
interface_prefix: &str,
css_prefix: &str,
@@ -52,7 +52,7 @@ fn flatten_color_value(
let new_interface_prefix = if interface_prefix.is_empty() {
key.clone()
} else {
- format!("{}.{}", interface_prefix, key)
+ format!("{interface_prefix}.{key}")
};
let new_css_prefix = if css_prefix.is_empty() {
key.replace('.', "-")
@@ -64,8 +64,7 @@ fn flatten_color_value(
Ok(())
}
_ => Err(format!(
- "color value for key '{}' must be a string or an object, got {:?}",
- interface_prefix, value
+ "color value for key '{interface_prefix}' must be a string or an object, got {value:?}"
)),
}
}
@@ -112,17 +111,19 @@ impl ColorTheme {
self.entries.keys()
}
- /// Get iterator over (css_key, value) pairs for CSS generation
+ /// Get iterator over (`css_key`, value) pairs for CSS generation
pub fn css_entries(&self) -> impl Iterator- {
self.entries.iter().map(|(k, e)| (k, &e.value))
}
/// Get value by CSS key
+ #[must_use]
pub fn get(&self, css_key: &str) -> Option<&String> {
self.entries.get(css_key).map(|e| &e.value)
}
/// Check if CSS key exists
+ #[must_use]
pub fn contains_key(&self, css_key: &str) -> bool {
self.entries.contains_key(css_key)
}
@@ -159,7 +160,8 @@ pub struct Typography {
pub letter_spacing: Option,
}
impl Typography {
- pub fn new(
+ #[must_use]
+ pub const fn new(
font_family: Option,
font_size: Option,
font_weight: Option,
@@ -198,12 +200,12 @@ fn deserialize_typo_prop(value: &Value) -> Result>, String> {
Value::Null => result.push(None),
Value::String(s) => result.push(Some(s.clone())),
Value::Number(n) => result.push(Some(n.to_string())),
- _ => return Err(format!("Invalid typography property value: {:?}", item)),
+ _ => return Err(format!("Invalid typography property value: {item:?}")),
}
}
Ok(result)
}
- _ => Err(format!("Invalid typography property value: {:?}", value)),
+ _ => Err(format!("Invalid typography property value: {value:?}")),
}
}
@@ -327,8 +329,7 @@ impl<'de> Deserialize<'de> for Typographies {
Ok(Self(result))
}
_ => Err(D::Error::custom(format!(
- "Typography must be an object or array, got: {:?}",
- value
+ "Typography must be an object or array, got: {value:?}"
))),
}
}
@@ -373,13 +374,14 @@ impl<'de> Deserialize<'de> for TokenValues {
}
}
-/// LengthTheme stores a set of named length tokens for one theme variant
-/// e.g., { "gutterMd": ["2px", "4px"], "gutterLg": "16px", "gap": 8 }
+/// `LengthTheme` stores named length tokens for one theme variant.
+///
+/// e.g., `{ "gutterMd": ["2px", "4px"], "gutterLg": "16px", "gap": 8 }`
/// Plain numbers are multiplied by 4 and suffixed with "px" (e.g., 8 → "32px").
pub type LengthTheme = BTreeMap;
-/// ShadowTheme stores a set of named shadow tokens for one theme variant
-/// e.g., { "sm": "0 1px 2px rgba(0,0,0,0.1)", "md": ["0 2px 4px rgba(0,0,0,0.1)", null, "0 4px 8px rgba(0,0,0,0.2)"] }
+/// `ShadowTheme` stores a set of named shadow tokens for one theme variant
+/// e.g., `{ "sm": "0 1px 2px rgba(0,0,0,0.1)", "md": ["0 2px 4px rgba(0,0,0,0.1)", null, "0 4px 8px rgba(0,0,0,0.2)"] }`
pub type ShadowTheme = BTreeMap;
fn default_variant_key(themes: &BTreeMap) -> Option<&str> {
@@ -518,6 +520,7 @@ impl Theme {
default_variant_key(&self.colors).map(str::to_string)
}
+ #[must_use]
pub fn get_length_token_levels(&self) -> BTreeMap> {
self.length.values().flat_map(|theme| theme.iter()).fold(
BTreeMap::>::new(),
@@ -536,6 +539,7 @@ impl Theme {
)
}
+ #[must_use]
pub fn get_shadow_token_levels(&self) -> BTreeMap> {
self.shadows.values().flat_map(|theme| theme.iter()).fold(
BTreeMap::>::new(),
@@ -554,6 +558,7 @@ impl Theme {
)
}
+ #[must_use]
pub fn get_default_length_value(&self, token: &str) -> Option<&str> {
let default_key = default_variant_key(&self.length)?;
self.length
@@ -564,6 +569,7 @@ impl Theme {
.as_deref()
}
+ #[must_use]
pub fn get_default_shadow_value(&self, token: &str) -> Option<&str> {
let default_key = default_variant_key(&self.shadows)?;
self.shadows
@@ -574,6 +580,7 @@ impl Theme {
.as_deref()
}
+ #[must_use]
pub fn to_css(&self) -> String {
let mut theme_declaration = String::new();
@@ -598,25 +605,26 @@ impl Theme {
entries
.iter()
.find(|(k, _)| *k != &default_theme_key)
- .map(|(k, _)| k.to_string())
+ .map(|(k, _)| (*k).clone())
} else {
None
};
for (theme_name, theme_properties) in entries {
- let mut css_contents = vec![];
- let mut css_color_contents = vec![];
+ let mut theme_contents = String::new();
let theme_key = if *theme_name == *default_theme_key {
None
} else {
Some(theme_name)
};
if let Some(theme_key) = theme_key {
- write!(theme_declaration, ":root[data-theme={theme_key}]{{").unwrap();
- css_contents.push("color-scheme:dark".to_string());
+ theme_declaration.push_str(":root[data-theme=");
+ theme_declaration.push_str(theme_key);
+ theme_declaration.push_str("]{");
+ push_css_declaration(&mut theme_contents, "color-scheme:dark");
} else {
theme_declaration.push_str(":root{");
if !single_theme {
- css_contents.push("color-scheme:light".to_string());
+ push_css_declaration(&mut theme_contents, "color-scheme:light");
}
}
for (prop, value) in theme_properties.css_entries() {
@@ -634,7 +642,7 @@ impl Theme {
})
})
{
- css_color_contents.push(format!("--{prop}:{default_value}"));
+ push_css_variable(&mut theme_contents, prop, &default_value);
}
} else {
let other_theme_value =
@@ -651,85 +659,91 @@ impl Theme {
})
});
// default theme
- css_color_contents.push(format!(
- "--{prop}:{}",
- if let Some(other_theme_value) = other_theme_value {
- format!("light-dark({optimized_value},{other_theme_value})")
- } else {
- optimized_value
- }
- ));
+ if !theme_contents.is_empty() {
+ theme_contents.push(';');
+ }
+ theme_contents.push_str("--");
+ theme_contents.push_str(prop);
+ theme_contents.push(':');
+ if let Some(other_theme_value) = other_theme_value {
+ theme_contents.push_str("light-dark(");
+ theme_contents.push_str(&optimized_value);
+ theme_contents.push(',');
+ theme_contents.push_str(&other_theme_value);
+ theme_contents.push(')');
+ } else {
+ theme_contents.push_str(&optimized_value);
+ }
}
}
- theme_declaration.push_str(
- [css_contents, css_color_contents]
- .concat()
- .join(";")
- .as_str(),
- );
+ theme_declaration.push_str(&theme_contents);
theme_declaration.push('}');
}
}
let mut css = theme_declaration;
- let mut level_map = BTreeMap::>::new();
- for ty in self.typography.iter() {
+ let mut level_map = BTreeMap::::new();
+ for ty in &self.typography {
for (idx, t) in ty.1.0.iter().enumerate() {
if let Some(t) = t {
let resolve = |v: &str| -> String {
if let Some(token) = v.strip_prefix('$') {
- format!("var(--{})", token)
+ format!("var(--{token})")
} else {
optimize_value(v)
}
};
- let css_content = [
- t.font_family
- .as_ref()
- .map(|v| format!("font-family:{}", resolve(v)))
- .unwrap_or_default(),
- t.font_size
- .as_ref()
- .map(|v| format!("font-size:{}", resolve(v)))
- .unwrap_or_default(),
- t.font_weight
- .as_ref()
- .map(|v| format!("font-weight:{}", resolve(v)))
- .unwrap_or_default(),
- t.line_height
- .as_ref()
- .map(|v| format!("line-height:{}", resolve(v)))
- .unwrap_or_default(),
- t.letter_spacing
- .as_ref()
- .map(|v| format!("letter-spacing:{}", resolve(v)))
- .unwrap_or_default(),
- ]
- .iter()
- .filter_map(|v| {
- let v = v.trim();
- if v.is_empty() { None } else { Some(v) }
- })
- .collect::>()
- .join(";");
+ let mut css_content = String::new();
+ push_typography_property(
+ &mut css_content,
+ "font-family",
+ t.font_family.as_deref(),
+ &resolve,
+ );
+ push_typography_property(
+ &mut css_content,
+ "font-size",
+ t.font_size.as_deref(),
+ &resolve,
+ );
+ push_typography_property(
+ &mut css_content,
+ "font-weight",
+ t.font_weight.as_deref(),
+ &resolve,
+ );
+ push_typography_property(
+ &mut css_content,
+ "line-height",
+ t.line_height.as_deref(),
+ &resolve,
+ );
+ push_typography_property(
+ &mut css_content,
+ "letter-spacing",
+ t.letter_spacing.as_deref(),
+ &resolve,
+ );
if !css_content.is_empty() {
- level_map
- .entry(idx as u8)
- .or_default()
- .push(format!(".typo-{}{{{}}}", ty.0, css_content));
+ let level_css = level_map.entry(idx as u8).or_default();
+ level_css.push_str(".typo-");
+ level_css.push_str(ty.0);
+ level_css.push('{');
+ level_css.push_str(&css_content);
+ level_css.push('}');
}
}
}
}
for (level, css_vec) in level_map {
if level == 0 {
- css.push_str(css_vec.join("").as_str());
- } else if let Some(media) = self
- .breakpoints
- .get(level as usize)
- .map(|v| format!("(min-width:{v}px)"))
- {
- css.push_str(&format!("@media{media}{{{}}}", css_vec.join("")));
+ css.push_str(&css_vec);
+ } else if let Some(bp) = self.breakpoints.get(level as usize) {
+ write!(css, "@media(min-width:{bp}px)")
+ .unwrap_or_else(|err| panic!("failed to write CSS into string: {err}"));
+ css.push('{');
+ css.push_str(&css_vec);
+ css.push('}');
}
}
// Generate CSS variables for length tokens
@@ -746,16 +760,14 @@ impl Theme {
themes: &BTreeMap>,
breakpoints: &[u16],
) {
- if themes.is_empty() {
- return;
- }
- // Safe: themes is non-empty, so at least one key exists
- let default_key = themes
+ let Some(default_key) = themes
.keys()
.find(|k| *k == "default")
.or_else(|| themes.keys().next())
.cloned()
- .unwrap();
+ else {
+ return;
+ };
// Sort variants: default first, then alphabetical
let mut sorted_variants: Vec<_> = themes.iter().collect();
@@ -774,9 +786,9 @@ impl Theme {
format!(":root[data-theme={variant_name}]")
};
- // Group variables by breakpoint level
- let mut level_map = BTreeMap::>::new();
- for (name, values) in token_theme.iter() {
+ // Group variables by breakpoint level without allocating one String per variable.
+ let mut level_map = BTreeMap::::new();
+ for (name, values) in *token_theme {
for (idx, val) in values.0.iter().enumerate() {
if let Some(v) = val {
let optimized = optimize_value(v);
@@ -790,10 +802,14 @@ impl Theme {
.is_some_and(|d| optimize_value(d) == optimized)
});
if !is_same_as_default {
- level_map
- .entry(idx)
- .or_default()
- .push(format!("--{name}:{optimized}"));
+ let vars = level_map.entry(idx).or_default();
+ if !vars.is_empty() {
+ vars.push(';');
+ }
+ vars.push_str("--");
+ vars.push_str(name);
+ vars.push(':');
+ vars.push_str(&optimized);
}
}
}
@@ -801,12 +817,18 @@ impl Theme {
for (level, vars) in &level_map {
if !vars.is_empty() {
- let vars_str = vars.join(";");
if *level == 0 {
- write!(css, "{selector}{{{vars_str}}}").unwrap();
+ css.push_str(&selector);
+ css.push('{');
+ css.push_str(vars);
+ css.push('}');
} else if let Some(bp) = breakpoints.get(*level) {
- write!(css, "@media(min-width:{bp}px){{{selector}{{{vars_str}}}}}")
- .unwrap();
+ write!(css, "@media(min-width:{bp}px){{")
+ .unwrap_or_else(|err| panic!("failed to write CSS into string: {err}"));
+ css.push_str(&selector);
+ css.push('{');
+ css.push_str(vars);
+ css.push_str("}}");
}
}
}
@@ -814,12 +836,57 @@ impl Theme {
}
}
+fn push_typography_property(
+ css_content: &mut String,
+ property: &str,
+ value: Option<&str>,
+ resolve: &impl Fn(&str) -> String,
+) {
+ let Some(value) = value else {
+ return;
+ };
+ let value = value.trim();
+ if value.is_empty() {
+ return;
+ }
+ if !css_content.is_empty() {
+ css_content.push(';');
+ }
+ css_content.push_str(property);
+ css_content.push(':');
+ css_content.push_str(&resolve(value));
+}
+
+fn push_css_declaration(css_content: &mut String, declaration: &str) {
+ if !css_content.is_empty() {
+ css_content.push(';');
+ }
+ css_content.push_str(declaration);
+}
+
+fn push_css_variable(css_content: &mut String, name: &str, value: &str) {
+ if !css_content.is_empty() {
+ css_content.push(';');
+ }
+ css_content.push_str("--");
+ css_content.push_str(name);
+ css_content.push(':');
+ css_content.push_str(value);
+}
+
#[cfg(test)]
+#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use insta::assert_debug_snapshot;
use rstest::rstest;
+ fn make_named_color_theme(name: &str, value: &str) -> ColorTheme {
+ let mut ct = ColorTheme::default();
+ ct.add_color(name, value);
+ ct
+ }
+
#[test]
fn to_css_from_theme() {
let mut theme = Theme::default();
@@ -876,44 +943,37 @@ mod tests {
);
assert_eq!(theme.to_css(), "");
- // Helper to create a ColorTheme with a single color
- fn make_color_theme(name: &str, value: &str) -> ColorTheme {
- let mut ct = ColorTheme::default();
- ct.add_color(name, value);
- ct
- }
-
let mut theme = Theme::default();
- theme.add_color_theme("default", make_color_theme("primary", "#000"));
- theme.add_color_theme("dark", make_color_theme("primary", "#000"));
+ theme.add_color_theme("default", make_named_color_theme("primary", "#000"));
+ theme.add_color_theme("dark", make_named_color_theme("primary", "#000"));
assert_debug_snapshot!(theme.to_css());
let mut theme = Theme::default();
- theme.add_color_theme("light", make_color_theme("primary", "#000"));
- theme.add_color_theme("dark", make_color_theme("primary", "#000"));
+ theme.add_color_theme("light", make_named_color_theme("primary", "#000"));
+ theme.add_color_theme("dark", make_named_color_theme("primary", "#000"));
assert_debug_snapshot!(theme.to_css());
let mut theme = Theme::default();
- theme.add_color_theme("a", make_color_theme("primary", "#000"));
- theme.add_color_theme("b", make_color_theme("primary", "#000"));
+ theme.add_color_theme("a", make_named_color_theme("primary", "#000"));
+ theme.add_color_theme("b", make_named_color_theme("primary", "#000"));
assert_debug_snapshot!(theme.to_css());
let mut theme = Theme::default();
- theme.add_color_theme("light", make_color_theme("primary", "#000"));
- theme.add_color_theme("b", make_color_theme("primary", "#000"));
- theme.add_color_theme("a", make_color_theme("primary", "#000"));
- theme.add_color_theme("c", make_color_theme("primary", "#000"));
+ theme.add_color_theme("light", make_named_color_theme("primary", "#000"));
+ theme.add_color_theme("b", make_named_color_theme("primary", "#000"));
+ theme.add_color_theme("a", make_named_color_theme("primary", "#000"));
+ theme.add_color_theme("c", make_named_color_theme("primary", "#000"));
assert_debug_snapshot!(theme.to_css());
let mut theme = Theme::default();
- theme.add_color_theme("light", make_color_theme("primary", "#000"));
+ theme.add_color_theme("light", make_named_color_theme("primary", "#000"));
assert_debug_snapshot!(theme.to_css());
let mut theme = Theme::default();
- theme.add_color_theme("light", make_color_theme("primary", "#000"));
- theme.add_color_theme("b", make_color_theme("primary", "#001"));
- theme.add_color_theme("a", make_color_theme("primary", "#002"));
- theme.add_color_theme("c", make_color_theme("primary", "#000"));
+ theme.add_color_theme("light", make_named_color_theme("primary", "#000"));
+ theme.add_color_theme("b", make_named_color_theme("primary", "#001"));
+ theme.add_color_theme("a", make_named_color_theme("primary", "#002"));
+ theme.add_color_theme("c", make_named_color_theme("primary", "#000"));
assert_debug_snapshot!(theme.to_css());
}
@@ -1119,7 +1179,7 @@ mod tests {
fn test_nested_with_number_value_should_fail() {
// Nested object with non-string value should fail
let result: Result = serde_json::from_str(
- r##"{
+ r#"{
"colors": {
"light": {
"gray": {
@@ -1127,7 +1187,7 @@ mod tests {
}
}
}
- }"##,
+ }"#,
);
assert!(result.is_err());
}
@@ -1186,20 +1246,17 @@ mod tests {
.unwrap();
let light = theme.colors.get("light").unwrap();
- let interface_keys: Vec<_> = light.interface_keys().cloned().collect();
- let css_keys: Vec<_> = light.css_keys().cloned().collect();
-
// Interface key uses dots
- assert!(interface_keys.contains(&"a.b.c".to_string()));
+ assert!(light.interface_keys().any(|key| key == "a.b.c"));
// CSS key uses dashes
- assert!(css_keys.contains(&"a-b-c".to_string()));
+ assert!(light.css_keys().any(|key| key == "a-b-c"));
}
#[test]
fn test_compact_typography_format() {
// Test new compact format with property-level arrays
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": {
"fontFamily": "Pretendard",
@@ -1210,7 +1267,7 @@ mod tests {
"letterSpacing": "-0.03em"
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1239,7 +1296,7 @@ mod tests {
fn test_compact_typography_all_arrays() {
// Test compact format where multiple properties have arrays
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"body": {
"fontFamily": "Pretendard",
@@ -1248,7 +1305,7 @@ mod tests {
"lineHeight": [1.3, null, 1.5]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1276,7 +1333,7 @@ mod tests {
fn test_compact_typography_single_value() {
// Test compact format with all single values (no arrays)
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"caption": {
"fontFamily": "Pretendard",
@@ -1287,7 +1344,7 @@ mod tests {
"letterSpacing": "-0.03em"
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1306,7 +1363,7 @@ mod tests {
fn test_traditional_typography_array_still_works() {
// Ensure backward compatibility with traditional array format
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": [
{
@@ -1326,7 +1383,7 @@ mod tests {
}
]
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1348,7 +1405,7 @@ mod tests {
fn test_compact_typography_css_output() {
// Verify CSS output is correct for compact format
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": {
"fontFamily": "Pretendard",
@@ -1357,7 +1414,7 @@ mod tests {
"lineHeight": 1.3
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1376,11 +1433,11 @@ mod tests {
fn test_invalid_top_level_array_should_fail() {
// Top-level array that's not traditional format should fail
let result: Result = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": ["38px", null, "52px"]
}
- }"##,
+ }"#,
);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
@@ -1390,7 +1447,7 @@ mod tests {
#[test]
fn test_typography_variable_reference() {
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"body": {
"fontSize": "$text",
@@ -1398,20 +1455,18 @@ mod tests {
"fontWeight": 400
}
}
- }"##,
+ }"#,
)
.unwrap();
let css = theme.to_css();
assert!(
css.contains("font-size:var(--text)"),
- "Expected font-size:var(--text), got: {}",
- css
+ "Expected font-size:var(--text), got: {css}"
);
assert!(
css.contains("line-height:var(--leading)"),
- "Expected line-height:var(--leading), got: {}",
- css
+ "Expected line-height:var(--leading), got: {css}"
);
assert!(css.contains("font-weight:400"));
}
@@ -1419,7 +1474,7 @@ mod tests {
#[test]
fn test_typography_variable_reference_responsive() {
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"heading": [
{
@@ -1435,20 +1490,18 @@ mod tests {
}
]
}
- }"##,
+ }"#,
)
.unwrap();
let css = theme.to_css();
assert!(
css.contains("font-size:var(--textSm)"),
- "Expected font-size:var(--textSm), got: {}",
- css
+ "Expected font-size:var(--textSm), got: {css}"
);
assert!(
css.contains("font-size:var(--textLg)"),
- "Expected font-size:var(--textLg), got: {}",
- css
+ "Expected font-size:var(--textLg), got: {css}"
);
}
@@ -1456,7 +1509,7 @@ mod tests {
fn test_mixed_typography_formats() {
// Test that both formats can coexist in the same theme
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": [
{ "fontFamily": "Pretendard", "fontSize": "38px" },
@@ -1468,7 +1521,7 @@ mod tests {
"fontSize": ["14px", null, "16px"]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1493,14 +1546,14 @@ mod tests {
fn test_deserialize_typo_prop_null_value() {
// Test compact format with null values in arrays
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": {
"fontFamily": null,
"fontSize": ["14px", null, "16px"]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1518,13 +1571,13 @@ mod tests {
fn test_deserialize_typo_prop_invalid_array_value() {
// Test that invalid values in typography arrays fail
let result: Result = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": {
"fontSize": ["14px", {"invalid": "object"}, "16px"]
}
}
- }"##,
+ }"#,
);
assert!(result.is_err());
}
@@ -1533,13 +1586,13 @@ mod tests {
fn test_deserialize_typo_prop_invalid_single_value() {
// Test that invalid single value fails
let result: Result = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": {
"fontSize": true
}
}
- }"##,
+ }"#,
);
assert!(result.is_err());
}
@@ -1548,11 +1601,11 @@ mod tests {
fn test_typography_invalid_type() {
// Test that typography with invalid type (string) fails
let result: Result = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": "invalid string"
}
- }"##,
+ }"#,
);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
@@ -1621,11 +1674,11 @@ mod tests {
fn test_typography_empty_properties_all_none() {
// Test that empty compact format with no properties creates None
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"empty": {}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1638,13 +1691,13 @@ mod tests {
fn test_typography_with_only_letter_spacing() {
// Test typography with only letterSpacing property
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": {
"letterSpacing": ["-0.02em", null, "-0.03em"]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1675,7 +1728,7 @@ mod tests {
fn test_traditional_typography_with_invalid_item() {
// Test that traditional array with invalid item (not object/null) fails
let result: Result = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": [
{ "fontFamily": "Arial" },
@@ -1683,7 +1736,7 @@ mod tests {
null
]
}
- }"##,
+ }"#,
);
// This should fail because "invalid string item" is not null or object
// But the current implementation detects this as non-traditional and fails differently
@@ -1694,14 +1747,14 @@ mod tests {
fn test_compact_typography_different_array_lengths() {
// Test when different properties have different array lengths
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": {
"fontSize": ["14px", "16px"],
"fontWeight": ["400", "500", "600", "700"]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1746,14 +1799,14 @@ mod tests {
fn test_typography_float_values() {
// Test that float values are properly converted
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"typography": {
"h1": {
"lineHeight": [1.2, 1.5, 1.8],
"fontWeight": [400.5, 500, 600]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1772,11 +1825,11 @@ mod tests {
fn test_typographies_direct_traditional_array_deserialize() {
// Directly deserialize Typographies to ensure Value::Object branch is covered (line 183)
let typographies: Typographies = serde_json::from_str(
- r##"[
+ r#"[
{ "fontFamily": "Arial", "fontSize": "16px" },
null,
{ "fontFamily": "Helvetica", "fontSize": "18px" }
- ]"##,
+ ]"#,
)
.unwrap();
@@ -1796,11 +1849,11 @@ mod tests {
fn test_typographies_direct_invalid_array_item() {
// Directly deserialize Typographies with invalid array item to cover line 188
let result: Result = serde_json::from_str(
- r##"[
+ r#"[
{ "fontFamily": "Arial" },
"invalid string",
null
- ]"##,
+ ]"#,
);
assert!(result.is_err());
@@ -1812,11 +1865,11 @@ mod tests {
fn test_typographies_direct_number_in_array() {
// Test with number in traditional array to ensure error branch is hit
let result: Result = serde_json::from_str(
- r##"[
+ r#"[
{ "fontFamily": "Arial" },
123,
null
- ]"##,
+ ]"#,
);
assert!(result.is_err());
@@ -1828,11 +1881,11 @@ mod tests {
fn test_typographies_direct_bool_in_array() {
// Test with boolean in traditional array
let result: Result = serde_json::from_str(
- r##"[
+ r#"[
null,
{ "fontFamily": "Arial" },
true
- ]"##,
+ ]"#,
);
assert!(result.is_err());
@@ -1844,11 +1897,11 @@ mod tests {
fn test_typographies_direct_nested_array_in_array() {
// Test with nested array in traditional array
let result: Result = serde_json::from_str(
- r##"[
+ r#"[
{ "fontFamily": "Arial" },
["nested", "array"],
null
- ]"##,
+ ]"#,
);
assert!(result.is_err());
@@ -1861,13 +1914,13 @@ mod tests {
#[test]
fn test_length_deserialization_single_string() {
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"length": {
"default": {
"gutterMd": "8px"
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1880,13 +1933,13 @@ mod tests {
#[test]
fn test_length_deserialization_single_number() {
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"length": {
"default": {
"gap": 4
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1899,13 +1952,13 @@ mod tests {
#[test]
fn test_length_deserialization_responsive_array() {
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"length": {
"default": {
"gutterMd": ["2px", "4px"]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1919,13 +1972,13 @@ mod tests {
#[test]
fn test_length_deserialization_responsive_array_with_nulls() {
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"length": {
"default": {
"gutterLg": ["8px", null, null, null, "16px"]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1942,13 +1995,13 @@ mod tests {
#[test]
fn test_length_deserialization_number_in_array() {
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"length": {
"default": {
"gap": [4, null, 8]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -1963,13 +2016,13 @@ mod tests {
#[test]
fn test_length_deserialization_invalid_value() {
let result: Result = serde_json::from_str(
- r##"{
+ r#"{
"length": {
"default": {
"gap": true
}
}
- }"##,
+ }"#,
);
assert!(result.is_err());
}
@@ -1977,13 +2030,13 @@ mod tests {
#[test]
fn test_length_deserialization_invalid_array_value() {
let result: Result = serde_json::from_str(
- r##"{
+ r#"{
"length": {
"default": {
"gap": [true]
}
}
- }"##,
+ }"#,
);
assert!(result.is_err());
}
@@ -2113,7 +2166,7 @@ mod tests {
#[test]
fn test_length_deserialization_from_json() {
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"length": {
"default": {
"gutterMd": ["2px", "4px"],
@@ -2121,7 +2174,7 @@ mod tests {
"gap": 8
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -2146,14 +2199,14 @@ mod tests {
#[test]
fn test_shadow_deserialization_from_json() {
let theme: Theme = serde_json::from_str(
- r##"{
+ r#"{
"shadows": {
"default": {
"sm": "0 1px 2px rgba(0,0,0,0.1)",
"md": ["0 2px 4px rgba(0,0,0,0.1)", null, "0 4px 8px rgba(0,0,0,0.2)"]
}
}
- }"##,
+ }"#,
)
.unwrap();
@@ -2256,7 +2309,7 @@ mod tests {
#[test]
fn test_token_values_deserialize_invalid_array_item() {
// Covers _ branch inside array match
- let result: Result = serde_json::from_str(r#"[true]"#);
+ let result: Result = serde_json::from_str(r"[true]");
assert!(result.is_err());
}
@@ -2380,4 +2433,65 @@ mod tests {
let empty = Theme::default();
assert_eq!(empty.get_default_shadow_value("card"), None);
}
+
+ // ===== Coverage: push_typography_property edge cases =====
+
+ #[test]
+ fn test_push_typography_property_none_value() {
+ // Covers early return when value is None
+ let mut css = String::new();
+ push_typography_property(&mut css, "font-family", None, &|v| v.to_string());
+ assert_eq!(css, "");
+ }
+
+ #[test]
+ fn test_push_typography_property_empty_value() {
+ // Covers early return when trimmed value is empty
+ let mut css = String::new();
+ push_typography_property(&mut css, "font-family", Some(""), &|v| v.to_string());
+ assert_eq!(css, "");
+
+ // Whitespace-only also returns early
+ let mut css = String::new();
+ push_typography_property(&mut css, "font-family", Some(" "), &|v| v.to_string());
+ assert_eq!(css, "");
+ }
+
+ #[test]
+ fn test_push_typography_property_appends_separator() {
+ // Empty css → no leading semicolon
+ let mut css = String::new();
+ push_typography_property(&mut css, "font-family", Some("Arial"), &|v| v.to_string());
+ assert_eq!(css, "font-family:Arial");
+
+ // Non-empty css → prepends ';' before declaration
+ push_typography_property(&mut css, "font-size", Some("16px"), &|v| v.to_string());
+ assert_eq!(css, "font-family:Arial;font-size:16px");
+ }
+
+ // ===== Coverage: push_css_declaration / push_css_variable separators =====
+
+ #[test]
+ fn test_push_css_declaration_separator() {
+ let mut css = String::new();
+ push_css_declaration(&mut css, "color-scheme:light");
+ // First call → no separator
+ assert_eq!(css, "color-scheme:light");
+
+ // Second call on non-empty buffer → prepends ';'
+ push_css_declaration(&mut css, "color:red");
+ assert_eq!(css, "color-scheme:light;color:red");
+ }
+
+ #[test]
+ fn test_push_css_variable_separator() {
+ let mut css = String::new();
+ push_css_variable(&mut css, "primary", "#000");
+ // First call → no separator
+ assert_eq!(css, "--primary:#000");
+
+ // Second call → prepends ';'
+ push_css_variable(&mut css, "secondary", "#fff");
+ assert_eq!(css, "--primary:#000;--secondary:#fff");
+ }
}
diff --git a/package.json b/package.json
index c57fc11a..a6df7df8 100644
--- a/package.json
+++ b/package.json
@@ -15,20 +15,21 @@
"dev": "bun run --filter '*' dev",
"benchmark": "bun benchmark.js",
"prepare": "husky",
- "changepacks": "bunx @changepacks/cli"
+ "changepacks": "bunx @changepacks/cli",
+ "build:landing": "bun run --filter landing build"
},
"devDependencies": {
"@devup-ui/eslint-plugin": "workspace:^",
- "@playwright/test": "^1.58.2",
+ "@playwright/test": "^1.59.1",
"@types/bun": "latest",
- "@types/node": "^25.5",
+ "@types/node": "^25.6",
"bun-test-env-dom": "^1.0.3",
"eslint": "^9",
"eslint-plugin-devup": "^2.0",
"eslint-plugin-eslint-plugin": "^7.3",
"eslint-plugin-jsonc": "^3.1",
"eslint-plugin-mdx": "^3.7",
- "globals": "^17.4",
+ "globals": "^17.6",
"husky": "^9.1"
},
"author": "devfive",
diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json
index db8e8ec7..2a03b1ab 100644
--- a/packages/bun-plugin/package.json
+++ b/packages/bun-plugin/package.json
@@ -46,7 +46,7 @@
},
"devDependencies": {
"@types/bun": "latest",
- "typescript": "^6.0.2"
+ "typescript": "^6.0.3"
},
"peerDependencies": {
"@devup-ui/wasm": "*"
diff --git a/packages/bun-plugin/src/__tests__/plugin.test.ts b/packages/bun-plugin/src/__tests__/plugin.test.ts
index 1fb4d931..e93d6741 100644
--- a/packages/bun-plugin/src/__tests__/plugin.test.ts
+++ b/packages/bun-plugin/src/__tests__/plugin.test.ts
@@ -26,6 +26,20 @@ let setDebugSpy: ReturnType
let hasDevupUISpy: ReturnType
let codeExtractSpy: ReturnType
+type CodeExtractResult = ReturnType
+
+function createCodeExtractResult(): CodeExtractResult {
+ return {
+ code: 'code',
+ css: '',
+ cssFile: null,
+ map: null,
+ updatedBaseStyle: false,
+ free: mock(),
+ [Symbol.dispose]: mock(),
+ } as unknown as CodeExtractResult
+}
+
beforeEach(() => {
getDefaultThemeSpy = spyOn(wasm, 'getDefaultTheme').mockReturnValue('default')
existsSyncSpy = spyOn(fs, 'existsSync').mockReturnValue(false)
@@ -38,15 +52,9 @@ beforeEach(() => {
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
setDebugSpy = spyOn(wasm, 'setDebug').mockReturnValue(undefined)
hasDevupUISpy = spyOn(wasm, 'hasDevupUI').mockReturnValue(false)
- codeExtractSpy = spyOn(wasm, 'codeExtract').mockReturnValue({
- code: 'code',
- css: '',
- cssFile: null,
- map: null,
- updatedBaseStyle: false,
- free: mock(),
- [Symbol.dispose]: mock(),
- } as any)
+ codeExtractSpy = spyOn(wasm, 'codeExtract').mockReturnValue(
+ createCodeExtractResult(),
+ )
})
afterEach(() => {
@@ -78,11 +86,10 @@ describe('getDevupDefine', () => {
describe('writeDataFiles behavior', () => {
it('should register theme from devup.json when it exists', async () => {
- existsSyncSpy.mockImplementation((path: string) => path === 'devup.json')
readFileSpy.mockResolvedValue('{"theme": {"colors": {"primary": "#000"}}}')
getThemeInterfaceSpy.mockReturnValue('interface CustomColors {}')
- // Simulate writeDataFiles behavior
+ // Simulate writeDataFiles behavior without exporting private plugin helpers.
const content = '{"theme": {"colors": {"primary": "#000"}}}'
const parsed = JSON.parse(content)
registerThemeSpy(parsed?.['theme'] ?? {})
@@ -93,7 +100,6 @@ describe('writeDataFiles behavior', () => {
})
it('should write theme.d.ts when interfaceCode is returned', async () => {
- existsSyncSpy.mockImplementation((path: string) => path === 'devup.json')
getThemeInterfaceSpy.mockReturnValue('interface CustomColors {}')
const interfaceCode = getThemeInterfaceSpy(
@@ -119,7 +125,7 @@ describe('writeDataFiles behavior', () => {
it('should register empty theme when devup.json does not exist', async () => {
existsSyncSpy.mockReturnValue(false)
- // Simulate the else branch
+ // Simulate the missing config branch.
const content = undefined
if (!content) {
registerThemeSpy({})
@@ -156,7 +162,7 @@ describe('writeDataFiles behavior', () => {
it('should create css directory when it does not exist', async () => {
existsSyncSpy.mockReturnValue(false)
- // Simulate the Promise.all behavior
+ // Simulate the directory creation branch without exposing internals.
if (!existsSyncSpy('df/devup-ui')) {
await mkdirSpy('df/devup-ui', { recursive: true })
}
diff --git a/packages/bun-plugin/src/plugin.ts b/packages/bun-plugin/src/plugin.ts
index 995e7f53..d8bb06e1 100644
--- a/packages/bun-plugin/src/plugin.ts
+++ b/packages/bun-plugin/src/plugin.ts
@@ -2,7 +2,11 @@ import { existsSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { basename, dirname, join, relative, resolve } from 'node:path'
-import { loadDevupConfig, mergeImportAliases } from '@devup-ui/plugin-utils'
+import {
+ createThemeInterfaceArgs,
+ loadDevupConfig,
+ mergeImportAliases,
+} from '@devup-ui/plugin-utils'
import {
codeExtract,
getThemeInterface,
@@ -32,14 +36,7 @@ async function writeDataFiles() {
// Generate theme interface after registration (always write, even if empty)
await writeFile(
join(distDir, 'theme.d.ts'),
- getThemeInterface(
- libPackage,
- 'CustomColors',
- 'DevupThemeTypography',
- 'CustomLength',
- 'CustomShadows',
- 'DevupTheme',
- ),
+ getThemeInterface(...createThemeInterfaceArgs(libPackage)),
'utf-8',
)
diff --git a/packages/components/package.json b/packages/components/package.json
index 64aa57be..c433848d 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -44,7 +44,7 @@
"types": "./dist/index.d.ts",
"dependencies": {
"@devup-ui/react": "workspace:^",
- "react": "^19.2.4",
+ "react": "^19.2.6",
"clsx": "^2.1"
},
"devDependencies": {
@@ -58,7 +58,7 @@
"storybook": "^10.3",
"typescript": "^6.0",
"vite": "^8.0",
- "vite-plugin-dts": "^4.5"
+ "vite-plugin-dts": "^5.0"
},
"peerDependencies": {
"@devup-ui/react": "workspace:^",
diff --git a/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap
index 065451e1..dbdda827 100644
--- a/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap
+++ b/packages/components/src/components/Button/__tests__/__snapshots__/index.browser.test.tsx.snap
@@ -159,7 +159,7 @@ exports[`Button should render icon when icon is provided 1`] = `