clava 0.2.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +552 -0
- package/dist/index.d.ts +22 -30
- package/dist/index.js +356 -171
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/rolldown.config.ts +1 -0
- package/src/index.ts +718 -285
- package/src/types.ts +44 -49
- package/tests/_utils.ts +1 -3
- package/tests/build.test.ts +18 -0
- package/tests/component-api.test.ts +284 -15
- package/tests/extend.test.ts +6 -6
- package/tests/{computed-variants.test.ts → function-variants.test.ts} +105 -25
- package/tests/prototype-pollution.test.ts +6 -6
- package/tests/{computed.test.ts → refine.test.ts} +517 -100
- package/tests/solid.test.ts +30 -0
- package/tests/split-props.test.ts +73 -1
- package/tests/variants-inference.test.ts +252 -0
package/tests/solid.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentProps, JSX } from "solid-js";
|
|
2
|
+
import { splitProps as splitSolidProps } from "solid-js";
|
|
2
3
|
import { expect, expectTypeOf, test } from "vitest";
|
|
3
4
|
import { type VariantProps, cv, splitProps } from "../src/index.ts";
|
|
4
5
|
import { type HTMLObjProps } from "../src/types.ts";
|
|
@@ -32,3 +33,32 @@ test("component props", () => {
|
|
|
32
33
|
style: { "font-size": "16px" },
|
|
33
34
|
});
|
|
34
35
|
});
|
|
36
|
+
|
|
37
|
+
test("solid splitProps accepts html propKeys", () => {
|
|
38
|
+
const component = cv({ variants: { size: { sm: "sm", md: "md" } } });
|
|
39
|
+
expect(component.html.propKeys).toEqual(["class", "style", "size"]);
|
|
40
|
+
|
|
41
|
+
interface Props
|
|
42
|
+
extends ComponentProps<"button">, VariantProps<typeof component> {}
|
|
43
|
+
const props: Props = { class: "custom", size: "md", id: "my-button" };
|
|
44
|
+
|
|
45
|
+
const [variantProps, rest] = splitSolidProps(props, component.html.propKeys);
|
|
46
|
+
expect(variantProps).toEqual({ class: "custom", size: "md" });
|
|
47
|
+
expect(rest).toEqual({ id: "my-button" });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("solid splitProps accepts htmlObj propKeys", () => {
|
|
51
|
+
const component = cv({ variants: { size: { sm: "sm", md: "md" } } });
|
|
52
|
+
expect(component.htmlObj.propKeys).toEqual(["class", "style", "size"]);
|
|
53
|
+
|
|
54
|
+
interface Props
|
|
55
|
+
extends ComponentProps<"button">, VariantProps<typeof component> {}
|
|
56
|
+
const props: Props = { class: "custom", size: "md", id: "my-button" };
|
|
57
|
+
|
|
58
|
+
const [variantProps, rest] = splitSolidProps(
|
|
59
|
+
props,
|
|
60
|
+
component.htmlObj.propKeys,
|
|
61
|
+
);
|
|
62
|
+
expect(variantProps).toEqual({ class: "custom", size: "md" });
|
|
63
|
+
expect(rest).toEqual({ id: "my-button" });
|
|
64
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, test } from "vitest";
|
|
2
|
-
import { splitProps } from "../src/index.ts";
|
|
2
|
+
import { cv, splitProps } from "../src/index.ts";
|
|
3
3
|
import {
|
|
4
4
|
CONFIGS,
|
|
5
5
|
type HTMLProperties,
|
|
@@ -588,3 +588,75 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
588
588
|
});
|
|
589
589
|
});
|
|
590
590
|
}
|
|
591
|
+
|
|
592
|
+
test.each([
|
|
593
|
+
[
|
|
594
|
+
"missing getVariants",
|
|
595
|
+
{
|
|
596
|
+
propKeys: ["size"],
|
|
597
|
+
variantKeys: ["size"],
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
[
|
|
601
|
+
"non-callable getVariants",
|
|
602
|
+
{
|
|
603
|
+
getVariants: true,
|
|
604
|
+
propKeys: ["size"],
|
|
605
|
+
variantKeys: ["size"],
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
[
|
|
609
|
+
"non-array propKeys",
|
|
610
|
+
{
|
|
611
|
+
getVariants: () => ({}),
|
|
612
|
+
propKeys: null,
|
|
613
|
+
variantKeys: ["size"],
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
] as const)(
|
|
617
|
+
"splitProps ignores malformed component-like sources: %s",
|
|
618
|
+
(_, malformedSource) => {
|
|
619
|
+
const props = { id: "test", size: "lg" };
|
|
620
|
+
|
|
621
|
+
const [sourceProps, otherProps] = splitProps(
|
|
622
|
+
props,
|
|
623
|
+
// @ts-expect-error malformed source
|
|
624
|
+
malformedSource,
|
|
625
|
+
);
|
|
626
|
+
expect(sourceProps).toEqual({});
|
|
627
|
+
expect(otherProps).toEqual(props);
|
|
628
|
+
},
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
test("splitProps ignores sources with malformed variantKeys", () => {
|
|
632
|
+
const component = cv({ variants: { size: { sm: "sm", lg: "lg" } } });
|
|
633
|
+
const props = { id: "test", size: "lg", color: "red" };
|
|
634
|
+
const malformedSource = {
|
|
635
|
+
getVariants: () => ({}),
|
|
636
|
+
propKeys: ["color"],
|
|
637
|
+
variantKeys: null,
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const result = splitProps(
|
|
641
|
+
props,
|
|
642
|
+
component,
|
|
643
|
+
// @ts-expect-error malformed source
|
|
644
|
+
malformedSource,
|
|
645
|
+
) as unknown[];
|
|
646
|
+
expect(result).toEqual([{ size: "lg" }, {}, { id: "test", color: "red" }]);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test("splitProps type rejects non-string component source keys", () => {
|
|
650
|
+
const props = { size: "lg" };
|
|
651
|
+
const symbolKey = Symbol("size");
|
|
652
|
+
|
|
653
|
+
const [sourceProps, otherProps] = splitProps(props, {
|
|
654
|
+
getVariants: () => ({}),
|
|
655
|
+
// @ts-expect-error component source keys must be strings
|
|
656
|
+
propKeys: [symbolKey],
|
|
657
|
+
// @ts-expect-error component source keys must be strings
|
|
658
|
+
variantKeys: [1],
|
|
659
|
+
});
|
|
660
|
+
expect(sourceProps).toEqual({});
|
|
661
|
+
expect(otherProps).toEqual(props);
|
|
662
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { describe, expectTypeOf, test } from "vitest";
|
|
2
|
+
import { cv } from "../src/index.ts";
|
|
3
|
+
|
|
4
|
+
describe("variants type inference", () => {
|
|
5
|
+
test("function variant infers parameter type as prop type", () => {
|
|
6
|
+
const button = cv({
|
|
7
|
+
variants: {
|
|
8
|
+
size: (value: number) => `size-${value}`,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
expectTypeOf(button.getVariants()).branded.toEqualTypeOf<{
|
|
12
|
+
size?: number;
|
|
13
|
+
}>();
|
|
14
|
+
button({ size: 3 });
|
|
15
|
+
button({
|
|
16
|
+
// @ts-expect-error string is not assignable to number
|
|
17
|
+
size: "lg",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("function variant infers union parameter type", () => {
|
|
22
|
+
const button = cv({
|
|
23
|
+
variants: {
|
|
24
|
+
tone: (value: "info" | "warn") => `tone-${value}`,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
expectTypeOf(button.getVariants()).branded.toEqualTypeOf<{
|
|
28
|
+
tone?: "info" | "warn";
|
|
29
|
+
}>();
|
|
30
|
+
button({ tone: "info" });
|
|
31
|
+
button({ tone: "warn" });
|
|
32
|
+
button({
|
|
33
|
+
// @ts-expect-error "danger" is not in the union
|
|
34
|
+
tone: "danger",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("function variant infers boolean parameter type", () => {
|
|
39
|
+
const button = cv({
|
|
40
|
+
variants: {
|
|
41
|
+
active: (value: boolean) => (value ? "active" : "idle"),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
expectTypeOf(button.getVariants()).branded.toEqualTypeOf<{
|
|
45
|
+
active?: boolean;
|
|
46
|
+
}>();
|
|
47
|
+
button({ active: true });
|
|
48
|
+
button({ active: false });
|
|
49
|
+
button({
|
|
50
|
+
// @ts-expect-error string is not assignable to boolean
|
|
51
|
+
active: "yes",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("function variant accepts nullable parameter", () => {
|
|
56
|
+
const c = cv({
|
|
57
|
+
variants: {
|
|
58
|
+
color: (value: string | null) => (value ? `c-${value}` : "c-default"),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
expectTypeOf(c.getVariants()).branded.toEqualTypeOf<{
|
|
62
|
+
color?: string | null;
|
|
63
|
+
}>();
|
|
64
|
+
c({ color: "red" });
|
|
65
|
+
c({ color: null });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("mixed function and object variants infer independently", () => {
|
|
69
|
+
const c = cv({
|
|
70
|
+
variants: {
|
|
71
|
+
size: (value: number) => `s-${value}`,
|
|
72
|
+
color: { red: "r", blue: "b" },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
expectTypeOf(c.getVariants()).branded.toEqualTypeOf<{
|
|
76
|
+
size?: number;
|
|
77
|
+
color?: "red" | "blue";
|
|
78
|
+
}>();
|
|
79
|
+
c({ size: 3, color: "red" });
|
|
80
|
+
c({
|
|
81
|
+
// @ts-expect-error number expected
|
|
82
|
+
size: "3",
|
|
83
|
+
});
|
|
84
|
+
c({
|
|
85
|
+
// @ts-expect-error "yellow" not in object keys
|
|
86
|
+
color: "yellow",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("function variant in child overrides object variant in parent", () => {
|
|
91
|
+
const base = cv({ variants: { size: { sm: "sm", lg: "lg" } } });
|
|
92
|
+
const child = cv({
|
|
93
|
+
extend: [base],
|
|
94
|
+
variants: { size: (value: number) => `s-${value}` },
|
|
95
|
+
});
|
|
96
|
+
expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
|
|
97
|
+
size?: number;
|
|
98
|
+
}>();
|
|
99
|
+
child({ size: 3 });
|
|
100
|
+
child({
|
|
101
|
+
// @ts-expect-error string is not assignable to number after override
|
|
102
|
+
size: "sm",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("object variant in child overrides function variant in parent", () => {
|
|
107
|
+
const base = cv({ variants: { size: (value: number) => `s-${value}` } });
|
|
108
|
+
const child = cv({
|
|
109
|
+
extend: [base],
|
|
110
|
+
variants: { size: { sm: "child-sm", lg: "child-lg" } },
|
|
111
|
+
});
|
|
112
|
+
expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
|
|
113
|
+
size?: "sm" | "lg";
|
|
114
|
+
}>();
|
|
115
|
+
child({ size: "sm" });
|
|
116
|
+
child({
|
|
117
|
+
// @ts-expect-error number is not assignable to "sm" | "lg" after override
|
|
118
|
+
size: 3,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("object variant in child merges with object variant in parent", () => {
|
|
123
|
+
const base = cv({ variants: { size: { sm: "base-sm", md: "base-md" } } });
|
|
124
|
+
const child = cv({
|
|
125
|
+
extend: [base],
|
|
126
|
+
variants: { size: { lg: "child-lg" } },
|
|
127
|
+
});
|
|
128
|
+
expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
|
|
129
|
+
size?: "sm" | "md" | "lg";
|
|
130
|
+
}>();
|
|
131
|
+
child({ size: "sm" });
|
|
132
|
+
child({ size: "md" });
|
|
133
|
+
child({ size: "lg" });
|
|
134
|
+
child({
|
|
135
|
+
// @ts-expect-error not in merged keys
|
|
136
|
+
size: "xl",
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("function variant in parent inherits in child without override", () => {
|
|
141
|
+
const base = cv({
|
|
142
|
+
variants: { size: (value: number) => `s-${value}` },
|
|
143
|
+
});
|
|
144
|
+
const child = cv({
|
|
145
|
+
extend: [base],
|
|
146
|
+
variants: { color: { red: "r" } },
|
|
147
|
+
});
|
|
148
|
+
expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
|
|
149
|
+
size?: number;
|
|
150
|
+
color?: "red";
|
|
151
|
+
}>();
|
|
152
|
+
child({ size: 5, color: "red" });
|
|
153
|
+
child({
|
|
154
|
+
// @ts-expect-error inherited size is still number
|
|
155
|
+
size: "5",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("defaultVariants infers types from merged variants", () => {
|
|
160
|
+
cv({
|
|
161
|
+
variants: {
|
|
162
|
+
size: (value: number) => `s-${value}`,
|
|
163
|
+
color: { red: "r", blue: "b" },
|
|
164
|
+
},
|
|
165
|
+
defaultVariants: { size: 3, color: "red" },
|
|
166
|
+
});
|
|
167
|
+
cv({
|
|
168
|
+
variants: { size: (value: number) => `s-${value}` },
|
|
169
|
+
defaultVariants: {
|
|
170
|
+
// @ts-expect-error string is not assignable to number
|
|
171
|
+
size: "3",
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("refine callback sees function variant param type", () => {
|
|
177
|
+
cv({
|
|
178
|
+
variants: { size: (value: number) => `s-${value}` },
|
|
179
|
+
refine: ({ variants, setVariants }) => {
|
|
180
|
+
expectTypeOf(variants.size).toEqualTypeOf<number | undefined>();
|
|
181
|
+
setVariants({ size: 10 });
|
|
182
|
+
setVariants({
|
|
183
|
+
// @ts-expect-error string not assignable to number
|
|
184
|
+
size: "10",
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("function variant return type accepts ClassValue and StyleClassValue", () => {
|
|
191
|
+
cv({
|
|
192
|
+
variants: {
|
|
193
|
+
a: (_: number) => "class-a",
|
|
194
|
+
b: (_: number) => null,
|
|
195
|
+
c: (_: number) => undefined,
|
|
196
|
+
d: (_: number) => ["x", "y"],
|
|
197
|
+
e: (_: number) => ({ class: "c", style: { color: "red" } }),
|
|
198
|
+
f: (_: number) => ({ style: { color: "red" } }),
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("variantKeys includes function variant keys", () => {
|
|
204
|
+
const c = cv({
|
|
205
|
+
variants: {
|
|
206
|
+
size: { sm: "sm" },
|
|
207
|
+
columns: (value: number) => `cols-${value}`,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
expectTypeOf(c.variantKeys).toEqualTypeOf<("size" | "columns")[]>();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("intermediate object variant hides grandparent function variant from descendants", () => {
|
|
214
|
+
const base = cv({
|
|
215
|
+
variants: { size: (value: number) => `s-${value}` },
|
|
216
|
+
});
|
|
217
|
+
const middle = cv({
|
|
218
|
+
extend: [base],
|
|
219
|
+
variants: { size: { sm: "middle-sm", md: "middle-md" } },
|
|
220
|
+
});
|
|
221
|
+
// Descendant has no own `size` — it should inherit `middle`'s object,
|
|
222
|
+
// not the grandparent function. The intersection used to leak through.
|
|
223
|
+
const descendant = cv({ extend: [middle] });
|
|
224
|
+
expectTypeOf(descendant.getVariants()).branded.toEqualTypeOf<{
|
|
225
|
+
size?: "sm" | "md";
|
|
226
|
+
}>();
|
|
227
|
+
descendant({ size: "sm" });
|
|
228
|
+
descendant({
|
|
229
|
+
// @ts-expect-error grandparent function is no longer in the chain
|
|
230
|
+
size: 3,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("null disables inherited function variant", () => {
|
|
235
|
+
const base = cv({
|
|
236
|
+
variants: {
|
|
237
|
+
size: (value: number) => `s-${value}`,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
const child = cv({
|
|
241
|
+
extend: [base],
|
|
242
|
+
variants: { size: null },
|
|
243
|
+
});
|
|
244
|
+
expectTypeOf(child.getVariants()).branded.toEqualTypeOf<{
|
|
245
|
+
size?: never;
|
|
246
|
+
}>();
|
|
247
|
+
child({
|
|
248
|
+
// @ts-expect-error size was disabled
|
|
249
|
+
size: 1,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|