clava 0.3.0 → 0.4.1
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 +86 -0
- package/README.md +38 -38
- package/dist/index.d.ts +19 -27
- package/dist/index.js +143 -86
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/index.ts +430 -215
- package/src/types.ts +38 -46
- package/src/utils.ts +31 -10
- package/tests/_utils.ts +7 -5
- package/tests/build.test.ts +81 -7
- package/tests/component-api.test.ts +28 -28
- package/tests/extend.test.ts +13 -8
- 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} +292 -149
- package/tests/variants-inference.test.ts +252 -0
|
@@ -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
|
+
});
|