clava 0.4.2 → 0.5.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 +41 -0
- package/README.md +26 -15
- package/dist/index.d.ts +10 -3
- package/dist/index.js +190 -115
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +376 -214
- package/src/refine-warning.ts +2 -2
- package/src/types.ts +24 -2
- package/tests/component-api.test.ts +81 -55
- package/tests/extend.test.ts +44 -10
- package/tests/prototype-pollution.test.ts +3 -4
- package/tests/refine.test.ts +152 -210
- package/tests/variants-inference.test.ts +81 -0
package/src/refine-warning.ts
CHANGED
|
@@ -145,8 +145,8 @@ export function warnRefineLimit({
|
|
|
145
145
|
runState.warned = true;
|
|
146
146
|
let message =
|
|
147
147
|
"Clava: Maximum refine iterations exceeded. This can happen when a " +
|
|
148
|
-
"
|
|
149
|
-
"
|
|
148
|
+
"computed default variant or refine callback changes one of the " +
|
|
149
|
+
"variants on every run.";
|
|
150
150
|
if (unstableChanges && unstableChanges.size > 0) {
|
|
151
151
|
message += `\nVariant(s) that did not stabilize: ${Array.from(unstableChanges.keys()).join(", ")}.`;
|
|
152
152
|
message += `\nLatest variant changes before warning: ${formatVariantChanges(unstableChanges)}.`;
|
package/src/types.ts
CHANGED
|
@@ -254,7 +254,30 @@ type ExtractVariantValue<T> = T extends null
|
|
|
254
254
|
: never;
|
|
255
255
|
|
|
256
256
|
export type VariantValues<V> = {
|
|
257
|
-
[K in keyof V]?: ExtractVariantValue<V[K]
|
|
257
|
+
[K in keyof V]?: ExtractVariantValue<V[K]> | undefined;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
interface DefaultVariantContext<V, K extends keyof V> {
|
|
261
|
+
defaultValue: ExtractVariantValue<V[K]> | undefined;
|
|
262
|
+
variants: Readonly<VariantValues<V>>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type ComputedDefaultVariant<V, K extends keyof V> = (
|
|
266
|
+
context: DefaultVariantContext<V, K>,
|
|
267
|
+
) => ExtractVariantValue<V[K]> | undefined;
|
|
268
|
+
|
|
269
|
+
type NonFunctionVariantValue<T> = Exclude<T, (...args: any[]) => any>;
|
|
270
|
+
|
|
271
|
+
type DefaultVariantValue<V, K extends keyof V> = [
|
|
272
|
+
NonFunctionVariantValue<ExtractVariantValue<V[K]>>,
|
|
273
|
+
] extends [never]
|
|
274
|
+
? ComputedDefaultVariant<V, K>
|
|
275
|
+
:
|
|
276
|
+
| NonFunctionVariantValue<ExtractVariantValue<V[K]>>
|
|
277
|
+
| ComputedDefaultVariant<V, K>;
|
|
278
|
+
|
|
279
|
+
export type DefaultVariants<V> = {
|
|
280
|
+
[K in keyof V]?: DefaultVariantValue<V, K> | undefined;
|
|
258
281
|
};
|
|
259
282
|
|
|
260
283
|
export type StyleValue = CSS.Properties & {
|
|
@@ -269,7 +292,6 @@ export interface StyleClassValue {
|
|
|
269
292
|
export interface RefineContext<V> {
|
|
270
293
|
variants: VariantValues<V>;
|
|
271
294
|
setVariants: (variants: VariantValues<V>) => void;
|
|
272
|
-
setDefaultVariants: (variants: VariantValues<V>) => void;
|
|
273
295
|
addClass: (className: ClassValue) => void;
|
|
274
296
|
addStyle: (style: StyleValue) => void;
|
|
275
297
|
}
|
|
@@ -59,6 +59,33 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
59
59
|
expect(variants).toEqual({ size: "sm" });
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
test("getVariants omits undefined defaultVariants", () => {
|
|
63
|
+
const component = getModeComponent(
|
|
64
|
+
mode,
|
|
65
|
+
cv({
|
|
66
|
+
variants: { size: { sm: "sm", lg: "lg" } },
|
|
67
|
+
defaultVariants: { size: undefined },
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
const variants = component.getVariants();
|
|
71
|
+
expect(variants).toStrictEqual({});
|
|
72
|
+
expect(Object.hasOwn(variants, "size")).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("getVariants undefined defaultVariants clear inherited defaultVariants", () => {
|
|
76
|
+
const base = cv({
|
|
77
|
+
variants: { size: { sm: "sm", lg: "lg" } },
|
|
78
|
+
defaultVariants: { size: "sm" },
|
|
79
|
+
});
|
|
80
|
+
const component = getModeComponent(
|
|
81
|
+
mode,
|
|
82
|
+
cv({ extend: [base], defaultVariants: { size: undefined } }),
|
|
83
|
+
);
|
|
84
|
+
const variants = component.getVariants();
|
|
85
|
+
expect(variants).toStrictEqual({});
|
|
86
|
+
expect(Object.hasOwn(variants, "size")).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
62
89
|
test("getVariants returns variants set by refine setVariants", () => {
|
|
63
90
|
const component = getModeComponent(
|
|
64
91
|
mode,
|
|
@@ -100,7 +127,7 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
100
127
|
expect(variants).toEqual({ size: "sm", color: "red" });
|
|
101
128
|
});
|
|
102
129
|
|
|
103
|
-
test("getVariants re-runs when
|
|
130
|
+
test("getVariants re-runs when computed defaultVariants change variants", () => {
|
|
104
131
|
const component = getModeComponent(
|
|
105
132
|
mode,
|
|
106
133
|
cv({
|
|
@@ -108,11 +135,10 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
108
135
|
size: { sm: "sm", lg: "lg" },
|
|
109
136
|
color: { red: "red", blue: "blue" },
|
|
110
137
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
138
|
+
defaultVariants: {
|
|
139
|
+
color: () => "red" as const,
|
|
140
|
+
size: ({ defaultValue, variants }) =>
|
|
141
|
+
variants.color === "red" ? "lg" : defaultValue,
|
|
116
142
|
},
|
|
117
143
|
}),
|
|
118
144
|
);
|
|
@@ -120,7 +146,7 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
120
146
|
expect(variants).toEqual({ size: "lg", color: "red" });
|
|
121
147
|
});
|
|
122
148
|
|
|
123
|
-
test("getVariants returns
|
|
149
|
+
test("getVariants returns computed defaultVariants", () => {
|
|
124
150
|
const component = getModeComponent(
|
|
125
151
|
mode,
|
|
126
152
|
cv({
|
|
@@ -128,10 +154,9 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
128
154
|
size: { sm: "sm", lg: "lg" },
|
|
129
155
|
color: { red: "red", blue: "blue" },
|
|
130
156
|
},
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
157
|
+
defaultVariants: {
|
|
158
|
+
color: ({ defaultValue, variants }) =>
|
|
159
|
+
variants.size === "lg" ? "blue" : defaultValue,
|
|
135
160
|
},
|
|
136
161
|
}),
|
|
137
162
|
);
|
|
@@ -139,7 +164,7 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
139
164
|
expect(variants).toEqual({ size: "lg", color: "blue" });
|
|
140
165
|
});
|
|
141
166
|
|
|
142
|
-
test("getVariants
|
|
167
|
+
test("getVariants computed defaultVariants do not override props", () => {
|
|
143
168
|
const component = getModeComponent(
|
|
144
169
|
mode,
|
|
145
170
|
cv({
|
|
@@ -147,8 +172,8 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
147
172
|
size: { sm: "sm", lg: "lg" },
|
|
148
173
|
color: { red: "red", blue: "blue" },
|
|
149
174
|
},
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
defaultVariants: {
|
|
176
|
+
color: () => "blue" as const,
|
|
152
177
|
},
|
|
153
178
|
}),
|
|
154
179
|
);
|
|
@@ -173,11 +198,11 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
173
198
|
expect(variants).toEqual({ color: "blue" });
|
|
174
199
|
});
|
|
175
200
|
|
|
176
|
-
test("getVariants picks up
|
|
201
|
+
test("getVariants picks up computed defaultVariants from extended component", () => {
|
|
177
202
|
const base = cv({
|
|
178
203
|
variants: { size: { sm: "sm", lg: "lg" } },
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
defaultVariants: {
|
|
205
|
+
size: () => "lg" as const,
|
|
181
206
|
},
|
|
182
207
|
});
|
|
183
208
|
const component = getModeComponent(
|
|
@@ -192,11 +217,11 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
192
217
|
expect(variants).toEqual({ size: "lg", color: "red" });
|
|
193
218
|
});
|
|
194
219
|
|
|
195
|
-
test("getVariants picks up
|
|
220
|
+
test("getVariants picks up computed defaultVariants from grandparent component", () => {
|
|
196
221
|
const grandparent = cv({
|
|
197
222
|
variants: { size: { sm: "sm", lg: "lg" } },
|
|
198
|
-
|
|
199
|
-
|
|
223
|
+
defaultVariants: {
|
|
224
|
+
size: () => "lg" as const,
|
|
200
225
|
},
|
|
201
226
|
});
|
|
202
227
|
const parent = cv({ extend: [grandparent] });
|
|
@@ -238,21 +263,21 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
238
263
|
expect(variants).toEqual({ size: "lg", active: true, color: "red" });
|
|
239
264
|
});
|
|
240
265
|
|
|
241
|
-
test("getVariants preserves
|
|
266
|
+
test("getVariants preserves computed defaultVariants after a setVariants re-run", () => {
|
|
242
267
|
const base = cv({
|
|
243
268
|
variants: {
|
|
244
269
|
size: { sm: "sm", lg: "lg" },
|
|
245
270
|
active: "",
|
|
246
271
|
mode: { on: "on" },
|
|
247
272
|
},
|
|
248
|
-
defaultVariants: {
|
|
249
|
-
|
|
273
|
+
defaultVariants: {
|
|
274
|
+
size: ({ defaultValue, variants }) =>
|
|
275
|
+
variants.mode === "on" ? "lg" : defaultValue,
|
|
276
|
+
},
|
|
277
|
+
refine: ({ variants, setVariants }) => {
|
|
250
278
|
if (variants.active) {
|
|
251
279
|
setVariants({ mode: "on" });
|
|
252
280
|
}
|
|
253
|
-
if (variants.mode === "on") {
|
|
254
|
-
setDefaultVariants({ size: "lg" });
|
|
255
|
-
}
|
|
256
281
|
},
|
|
257
282
|
});
|
|
258
283
|
const component = getModeComponent(mode, cv({ extend: [base] }));
|
|
@@ -276,11 +301,11 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
276
301
|
expect(variants).toEqual({ size: "sm" });
|
|
277
302
|
});
|
|
278
303
|
|
|
279
|
-
test("getVariants child setVariants keeps overriding base
|
|
304
|
+
test("getVariants child setVariants keeps overriding base computed defaultVariants across re-runs", () => {
|
|
280
305
|
const base = cv({
|
|
281
306
|
variants: { color: { red: "red", blue: "blue" } },
|
|
282
|
-
|
|
283
|
-
|
|
307
|
+
defaultVariants: {
|
|
308
|
+
color: () => "blue" as const,
|
|
284
309
|
},
|
|
285
310
|
});
|
|
286
311
|
const component = getModeComponent(
|
|
@@ -300,11 +325,11 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
300
325
|
expect(variants).toEqual({ color: "red", size: "sm" });
|
|
301
326
|
});
|
|
302
327
|
|
|
303
|
-
test("getVariants setVariants sticks across re-runs", () => {
|
|
328
|
+
test("getVariants setVariants sticks across computed default re-runs", () => {
|
|
304
329
|
const base = cv({
|
|
305
330
|
variants: { color: { red: "red", blue: "blue" } },
|
|
306
|
-
|
|
307
|
-
|
|
331
|
+
defaultVariants: {
|
|
332
|
+
color: () => "blue" as const,
|
|
308
333
|
},
|
|
309
334
|
});
|
|
310
335
|
const component = getModeComponent(
|
|
@@ -323,20 +348,21 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
323
348
|
expect(variants).toEqual({ color: "red", done: true });
|
|
324
349
|
});
|
|
325
350
|
|
|
326
|
-
test("getVariants base
|
|
351
|
+
test("getVariants base computed defaultVariants can override child static defaults after a re-run", () => {
|
|
327
352
|
const base = cv({
|
|
328
353
|
variants: {
|
|
329
354
|
size: { sm: "sm", lg: "lg" },
|
|
330
355
|
active: "",
|
|
331
356
|
mode: { on: "on" },
|
|
332
357
|
},
|
|
333
|
-
|
|
358
|
+
defaultVariants: {
|
|
359
|
+
size: ({ defaultValue, variants }) =>
|
|
360
|
+
variants.mode === "on" ? "lg" : defaultValue,
|
|
361
|
+
},
|
|
362
|
+
refine: ({ variants, setVariants }) => {
|
|
334
363
|
if (variants.active) {
|
|
335
364
|
setVariants({ mode: "on" });
|
|
336
365
|
}
|
|
337
|
-
if (variants.mode === "on") {
|
|
338
|
-
setDefaultVariants({ size: "lg" });
|
|
339
|
-
}
|
|
340
366
|
},
|
|
341
367
|
});
|
|
342
368
|
const component = getModeComponent(
|
|
@@ -347,7 +373,7 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
347
373
|
expect(variants).toEqual({ size: "lg", active: true, mode: "on" });
|
|
348
374
|
});
|
|
349
375
|
|
|
350
|
-
test("getVariants setVariants from earlier extends overrides
|
|
376
|
+
test("getVariants setVariants from earlier extends overrides computed defaultVariants from later extends", () => {
|
|
351
377
|
const first = cv({
|
|
352
378
|
variants: { color: { red: "first-red", blue: "first-blue" } },
|
|
353
379
|
refine: ({ setVariants }) => {
|
|
@@ -356,8 +382,8 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
356
382
|
});
|
|
357
383
|
const second = cv({
|
|
358
384
|
variants: { color: { red: "second-red", blue: "second-blue" } },
|
|
359
|
-
|
|
360
|
-
|
|
385
|
+
defaultVariants: {
|
|
386
|
+
color: () => "blue" as const,
|
|
361
387
|
},
|
|
362
388
|
});
|
|
363
389
|
const component = getModeComponent(mode, cv({ extend: [first, second] }));
|
|
@@ -365,17 +391,17 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
365
391
|
expect(variants).toEqual({ color: "red" });
|
|
366
392
|
});
|
|
367
393
|
|
|
368
|
-
test("getVariants
|
|
394
|
+
test("getVariants computed defaultVariants from later extends override earlier extends", () => {
|
|
369
395
|
const first = cv({
|
|
370
396
|
variants: { color: { red: "first-red", blue: "first-blue" } },
|
|
371
|
-
|
|
372
|
-
|
|
397
|
+
defaultVariants: {
|
|
398
|
+
color: () => "red" as const,
|
|
373
399
|
},
|
|
374
400
|
});
|
|
375
401
|
const second = cv({
|
|
376
402
|
variants: { color: { red: "second-red", blue: "second-blue" } },
|
|
377
|
-
|
|
378
|
-
|
|
403
|
+
defaultVariants: {
|
|
404
|
+
color: () => "blue" as const,
|
|
379
405
|
},
|
|
380
406
|
});
|
|
381
407
|
const component = getModeComponent(mode, cv({ extend: [first, second] }));
|
|
@@ -383,7 +409,7 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
383
409
|
expect(variants).toEqual({ color: "blue" });
|
|
384
410
|
});
|
|
385
411
|
|
|
386
|
-
test("getVariants
|
|
412
|
+
test("getVariants computed defaultVariants do not override stable setVariants on later passes", () => {
|
|
387
413
|
const base = cv({
|
|
388
414
|
variants: { color: { red: "base-red", blue: "base-blue" } },
|
|
389
415
|
refine: ({ setVariants }) => {
|
|
@@ -395,10 +421,9 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
395
421
|
cv({
|
|
396
422
|
extend: [base],
|
|
397
423
|
variants: { color: { red: "child-red", blue: "child-blue" } },
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
424
|
+
defaultVariants: {
|
|
425
|
+
color: ({ defaultValue, variants }) =>
|
|
426
|
+
variants.color === "red" ? "blue" : defaultValue,
|
|
402
427
|
},
|
|
403
428
|
}),
|
|
404
429
|
);
|
|
@@ -406,7 +431,7 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
406
431
|
expect(variants).toEqual({ color: "red" });
|
|
407
432
|
});
|
|
408
433
|
|
|
409
|
-
test("getVariants
|
|
434
|
+
test("getVariants computed defaultVariants do not override setVariants from a previous pass", () => {
|
|
410
435
|
const component = getModeComponent(
|
|
411
436
|
mode,
|
|
412
437
|
cv({
|
|
@@ -414,13 +439,14 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
414
439
|
color: { red: "red", blue: "blue" },
|
|
415
440
|
done: "",
|
|
416
441
|
},
|
|
417
|
-
|
|
442
|
+
defaultVariants: {
|
|
443
|
+
color: ({ defaultValue, variants }) =>
|
|
444
|
+
variants.done ? "blue" : defaultValue,
|
|
445
|
+
},
|
|
446
|
+
refine: ({ variants, setVariants }) => {
|
|
418
447
|
if (!variants.done) {
|
|
419
448
|
setVariants({ color: "red", done: true });
|
|
420
449
|
}
|
|
421
|
-
if (variants.done) {
|
|
422
|
-
setDefaultVariants({ color: "blue" });
|
|
423
|
-
}
|
|
424
450
|
},
|
|
425
451
|
}),
|
|
426
452
|
);
|
package/tests/extend.test.ts
CHANGED
|
@@ -144,7 +144,7 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
test("extend disabled variant value with
|
|
147
|
+
test("extend disabled variant value with computed defaultVariants", () => {
|
|
148
148
|
const base = cv({
|
|
149
149
|
variants: {
|
|
150
150
|
size: {
|
|
@@ -158,8 +158,8 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
158
158
|
cv({
|
|
159
159
|
extend: [base],
|
|
160
160
|
variants: { size: { sm: null } },
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
defaultVariants: {
|
|
162
|
+
size: () => "lg" as const,
|
|
163
163
|
},
|
|
164
164
|
}),
|
|
165
165
|
);
|
|
@@ -173,19 +173,53 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
173
173
|
cv({
|
|
174
174
|
extend: [base],
|
|
175
175
|
variants: { size: { sm: null } },
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
"sm",
|
|
182
|
-
});
|
|
176
|
+
defaultVariants: {
|
|
177
|
+
// @ts-expect-error disabled variant value cannot be set
|
|
178
|
+
size: () =>
|
|
179
|
+
// no error
|
|
180
|
+
"sm",
|
|
183
181
|
},
|
|
184
182
|
}),
|
|
185
183
|
);
|
|
186
184
|
expect(getStyleClass(invalidComponent())).toEqual({ class: "" });
|
|
187
185
|
});
|
|
188
186
|
|
|
187
|
+
test("extend filters disabled values from inherited computed defaultVariants", () => {
|
|
188
|
+
const base = cv({
|
|
189
|
+
variants: {
|
|
190
|
+
size: {
|
|
191
|
+
sm: { class: "base-sm", style: { fontSize: "12px" } },
|
|
192
|
+
lg: { class: "base-lg", style: { fontSize: "16px" } },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
defaultVariants: {
|
|
196
|
+
size: () => "sm" as const,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
const component = getModeComponent(
|
|
200
|
+
mode,
|
|
201
|
+
cv({
|
|
202
|
+
extend: [base],
|
|
203
|
+
variants: {
|
|
204
|
+
size: { sm: null },
|
|
205
|
+
color: { red: "red", blue: "blue" },
|
|
206
|
+
},
|
|
207
|
+
defaultVariants: {
|
|
208
|
+
size: "lg",
|
|
209
|
+
color: ({ variants }) => (variants.size === "lg" ? "blue" : "red"),
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
expect(getStyleClass(component())).toEqual({
|
|
214
|
+
class: cls("base-lg blue"),
|
|
215
|
+
fontSize: "16px",
|
|
216
|
+
});
|
|
217
|
+
expect(component.getVariants()).toEqual({
|
|
218
|
+
size: "lg",
|
|
219
|
+
color: "blue",
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
189
223
|
test("extend disabled variant value with refine setVariants", () => {
|
|
190
224
|
const base = cv({
|
|
191
225
|
variants: {
|
|
@@ -27,11 +27,10 @@ test("render ignores keys inherited from Object.prototype", () => {
|
|
|
27
27
|
expect(component({}).class).toBe("sm");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test("extend's
|
|
30
|
+
test("extend's resolver ignores polluted prototype on parent's refine", () => {
|
|
31
31
|
// Base's refine branches on its own variants.size — if the polluted "size"
|
|
32
|
-
// key leaks into
|
|
33
|
-
//
|
|
34
|
-
// lg-specific class.
|
|
32
|
+
// key leaks into resolved variants, the refine callback would see size =
|
|
33
|
+
// "lg" instead of the staticDefault "sm" and emit the lg-specific class.
|
|
35
34
|
proto.size = "lg";
|
|
36
35
|
const base = cv({
|
|
37
36
|
variants: { size: { sm: "sm", lg: "lg" } },
|