clava 0.2.3 → 0.3.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/src/types.ts CHANGED
@@ -70,11 +70,15 @@ export type ComponentProps<V = {}> = VariantValues<V> & NullableComponentResult;
70
70
 
71
71
  export type GetVariants<V> = (variants?: VariantValues<V>) => VariantValues<V>;
72
72
 
73
+ type ComponentPropKey<R extends ComponentResult> =
74
+ | keyof R
75
+ | (R extends StyleClassProps ? "className" : never);
76
+
73
77
  // Key source types - what can be passed as additional parameters to splitProps
74
78
  export type KeySourceArray = readonly string[];
75
79
  export type KeySourceComponent = {
76
- keys: readonly (string | number | symbol)[];
77
- variantKeys: readonly (string | number | symbol)[];
80
+ propKeys: readonly string[];
81
+ variantKeys: readonly string[];
78
82
  getVariants: () => Record<string, unknown>;
79
83
  };
80
84
  export type KeySource = KeySourceArray | KeySourceComponent;
@@ -85,7 +89,7 @@ type IsComponent<S> = S extends { getVariants: () => unknown } ? true : false;
85
89
  // Extract keys from a source (includes class/style for components)
86
90
  type SourceKeys<S> = S extends readonly (infer K)[]
87
91
  ? K
88
- : S extends { keys: readonly (infer K)[] }
92
+ : S extends { propKeys: readonly (infer K)[] }
89
93
  ? K
90
94
  : never;
91
95
 
@@ -169,9 +173,8 @@ export interface ModalComponent<V, R extends ComponentResult> {
169
173
  class: (props?: ComponentProps<V>) => string;
170
174
  style: (props?: ComponentProps<V>) => R["style"];
171
175
  getVariants: GetVariants<V>;
172
- keys: (keyof V | keyof NullableComponentResult)[];
173
176
  variantKeys: (keyof V)[];
174
- propKeys: (keyof V | keyof NullableComponentResult)[];
177
+ propKeys: (keyof V | ComponentPropKey<R>)[];
175
178
  }
176
179
 
177
180
  export interface CVComponent<
@@ -248,11 +251,13 @@ type ExtractVariantValue<T> = T extends null
248
251
  ? never
249
252
  : T extends (value: infer V) => any
250
253
  ? V
251
- : T extends Record<string, any>
252
- ? StringToBoolean<NonNullKeys<T>>
253
- : T extends ClassValue
254
- ? boolean
255
- : never;
254
+ : T extends readonly unknown[]
255
+ ? boolean
256
+ : T extends Record<string, any>
257
+ ? StringToBoolean<NonNullKeys<T>>
258
+ : T extends ClassValue
259
+ ? boolean
260
+ : never;
256
261
 
257
262
  export type VariantValues<V> = {
258
263
  [K in keyof V]?: ExtractVariantValue<V[K]>;
@@ -0,0 +1,18 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { promisify } from "node:util";
6
+ import { expect, test } from "vitest";
7
+
8
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
9
+ const exec = promisify(execFile);
10
+
11
+ test("build preserves the production warning guard for consumers", async () => {
12
+ await exec("pnpm", ["--dir", root, "build"]);
13
+
14
+ const code = await readFile(join(root, "dist/index.js"), "utf8");
15
+ expect(code).toMatch(
16
+ /process\.env\.NODE_ENV !== "production"[\s\S]*?console\.warn\(/,
17
+ );
18
+ }, 60_000);
@@ -78,6 +78,48 @@ for (const config of Object.values(CONFIGS)) {
78
78
  expect(variants).toEqual({ size: "lg", color: "red" });
79
79
  });
80
80
 
81
+ test("getVariants re-runs when computed changes variants", () => {
82
+ const component = getModeComponent(
83
+ mode,
84
+ cv({
85
+ variants: {
86
+ size: { sm: "sm", lg: "lg" },
87
+ color: { red: "red", blue: "blue" },
88
+ },
89
+ computed: ({ variants, setVariants }) => {
90
+ if (variants.size === "lg") {
91
+ setVariants({ color: "red" });
92
+ }
93
+ if (variants.color === "red") {
94
+ setVariants({ size: "sm" });
95
+ }
96
+ },
97
+ }),
98
+ );
99
+ const variants = component.getVariants({ size: "lg" });
100
+ expect(variants).toEqual({ size: "sm", color: "red" });
101
+ });
102
+
103
+ test("getVariants re-runs when setDefaultVariants changes variants", () => {
104
+ const component = getModeComponent(
105
+ mode,
106
+ cv({
107
+ variants: {
108
+ size: { sm: "sm", lg: "lg" },
109
+ color: { red: "red", blue: "blue" },
110
+ },
111
+ computed: ({ variants, setDefaultVariants }) => {
112
+ setDefaultVariants({ color: "red" });
113
+ if (variants.color === "red") {
114
+ setDefaultVariants({ size: "lg" });
115
+ }
116
+ },
117
+ }),
118
+ );
119
+ const variants = component.getVariants();
120
+ expect(variants).toEqual({ size: "lg", color: "red" });
121
+ });
122
+
81
123
  test("getVariants returns variants set by computed setDefaultVariants", () => {
82
124
  const component = getModeComponent(
83
125
  mode,
@@ -170,17 +212,220 @@ for (const config of Object.values(CONFIGS)) {
170
212
  expect(variants).toEqual({ size: "lg", color: "red" });
171
213
  });
172
214
 
173
- test("keys returns props keys", () => {
215
+ test("getVariants re-runs when base component computed changes variants", () => {
216
+ const base = cv({
217
+ variants: { size: { sm: "sm", lg: "lg" }, active: "" },
218
+ defaultVariants: { size: "sm" },
219
+ computed: ({ variants, setVariants }) => {
220
+ if (variants.active) {
221
+ setVariants({ size: "lg" });
222
+ }
223
+ },
224
+ });
174
225
  const component = getModeComponent(
175
226
  mode,
176
- cv({ variants: { size: { sm: "sm" }, color: { red: "red" } } }),
227
+ cv({
228
+ extend: [base],
229
+ variants: { color: { red: "red", blue: "blue" } },
230
+ computed: ({ variants, setVariants }) => {
231
+ if (variants.size === "lg") {
232
+ setVariants({ color: "red" });
233
+ }
234
+ },
235
+ }),
177
236
  );
178
- expectTypeOf(component.keys).toExtend<
179
- ("class" | "className" | "style" | "size" | "color")[]
180
- >();
181
- expect(component.keys).toEqual(
182
- getExpectedPropsKeys(config, "size", "color"),
237
+ const variants = component.getVariants({ active: true });
238
+ expect(variants).toEqual({ size: "lg", active: true, color: "red" });
239
+ });
240
+
241
+ test("getVariants preserves base setDefaultVariants after its own setVariants re-run", () => {
242
+ const base = cv({
243
+ variants: {
244
+ size: { sm: "sm", lg: "lg" },
245
+ active: "",
246
+ mode: { on: "on" },
247
+ },
248
+ defaultVariants: { size: "sm" },
249
+ computed: ({ variants, setVariants, setDefaultVariants }) => {
250
+ if (variants.active) {
251
+ setVariants({ mode: "on" });
252
+ }
253
+ if (variants.mode === "on") {
254
+ setDefaultVariants({ size: "lg" });
255
+ }
256
+ },
257
+ });
258
+ const component = getModeComponent(mode, cv({ extend: [base] }));
259
+ const variants = component.getVariants({ active: true });
260
+ expect(variants).toEqual({ size: "lg", active: true, mode: "on" });
261
+ });
262
+
263
+ test("getVariants setVariants uses the latest pending value", () => {
264
+ const component = getModeComponent(
265
+ mode,
266
+ cv({
267
+ variants: { size: { sm: "sm", lg: "lg" } },
268
+ defaultVariants: { size: "sm" },
269
+ computed: ({ setVariants }) => {
270
+ setVariants({ size: "lg" });
271
+ setVariants({ size: "sm" });
272
+ },
273
+ }),
274
+ );
275
+ const variants = component.getVariants();
276
+ expect(variants).toEqual({ size: "sm" });
277
+ });
278
+
279
+ test("getVariants child setVariants keeps overriding base setDefaultVariants across re-runs", () => {
280
+ const base = cv({
281
+ variants: { color: { red: "red", blue: "blue" } },
282
+ computed: ({ setDefaultVariants }) => {
283
+ setDefaultVariants({ color: "blue" });
284
+ },
285
+ });
286
+ const component = getModeComponent(
287
+ mode,
288
+ cv({
289
+ extend: [base],
290
+ variants: { size: { sm: "sm", lg: "lg" } },
291
+ defaultVariants: { size: "sm" },
292
+ computed: ({ variants, setVariants }) => {
293
+ if (variants.size === "sm") {
294
+ setVariants({ color: "red" });
295
+ }
296
+ },
297
+ }),
298
+ );
299
+ const variants = component.getVariants();
300
+ expect(variants).toEqual({ color: "red", size: "sm" });
301
+ });
302
+
303
+ test("getVariants setVariants sticks across re-runs", () => {
304
+ const base = cv({
305
+ variants: { color: { red: "red", blue: "blue" } },
306
+ computed: ({ setDefaultVariants }) => {
307
+ setDefaultVariants({ color: "blue" });
308
+ },
309
+ });
310
+ const component = getModeComponent(
311
+ mode,
312
+ cv({
313
+ extend: [base],
314
+ variants: { color: { red: "red", blue: "blue" }, done: "" },
315
+ computed: ({ variants, setVariants }) => {
316
+ if (!variants.done) {
317
+ setVariants({ color: "red", done: true });
318
+ }
319
+ },
320
+ }),
321
+ );
322
+ const variants = component.getVariants();
323
+ expect(variants).toEqual({ color: "red", done: true });
324
+ });
325
+
326
+ test("getVariants base setDefaultVariants can override child static defaults after a re-run", () => {
327
+ const base = cv({
328
+ variants: {
329
+ size: { sm: "sm", lg: "lg" },
330
+ active: "",
331
+ mode: { on: "on" },
332
+ },
333
+ computed: ({ variants, setVariants, setDefaultVariants }) => {
334
+ if (variants.active) {
335
+ setVariants({ mode: "on" });
336
+ }
337
+ if (variants.mode === "on") {
338
+ setDefaultVariants({ size: "lg" });
339
+ }
340
+ },
341
+ });
342
+ const component = getModeComponent(
343
+ mode,
344
+ cv({ extend: [base], defaultVariants: { size: "sm" } }),
345
+ );
346
+ const variants = component.getVariants({ active: true });
347
+ expect(variants).toEqual({ size: "lg", active: true, mode: "on" });
348
+ });
349
+
350
+ test("getVariants setVariants from earlier extends overrides setDefaultVariants from later extends", () => {
351
+ const first = cv({
352
+ variants: { color: { red: "first-red", blue: "first-blue" } },
353
+ computed: ({ setVariants }) => {
354
+ setVariants({ color: "red" });
355
+ },
356
+ });
357
+ const second = cv({
358
+ variants: { color: { red: "second-red", blue: "second-blue" } },
359
+ computed: ({ setDefaultVariants }) => {
360
+ setDefaultVariants({ color: "blue" });
361
+ },
362
+ });
363
+ const component = getModeComponent(mode, cv({ extend: [first, second] }));
364
+ const variants = component.getVariants();
365
+ expect(variants).toEqual({ color: "red" });
366
+ });
367
+
368
+ test("getVariants setDefaultVariants from later extends overrides setDefaultVariants from earlier extends", () => {
369
+ const first = cv({
370
+ variants: { color: { red: "first-red", blue: "first-blue" } },
371
+ computed: ({ setDefaultVariants }) => {
372
+ setDefaultVariants({ color: "red" });
373
+ },
374
+ });
375
+ const second = cv({
376
+ variants: { color: { red: "second-red", blue: "second-blue" } },
377
+ computed: ({ setDefaultVariants }) => {
378
+ setDefaultVariants({ color: "blue" });
379
+ },
380
+ });
381
+ const component = getModeComponent(mode, cv({ extend: [first, second] }));
382
+ const variants = component.getVariants();
383
+ expect(variants).toEqual({ color: "blue" });
384
+ });
385
+
386
+ test("getVariants setDefaultVariants does not override stable setVariants on later passes", () => {
387
+ const base = cv({
388
+ variants: { color: { red: "base-red", blue: "base-blue" } },
389
+ computed: ({ setVariants }) => {
390
+ setVariants({ color: "red" });
391
+ },
392
+ });
393
+ const component = getModeComponent(
394
+ mode,
395
+ cv({
396
+ extend: [base],
397
+ variants: { color: { red: "child-red", blue: "child-blue" } },
398
+ computed: ({ variants, setDefaultVariants }) => {
399
+ if (variants.color === "red") {
400
+ setDefaultVariants({ color: "blue" });
401
+ }
402
+ },
403
+ }),
404
+ );
405
+ const variants = component.getVariants();
406
+ expect(variants).toEqual({ color: "red" });
407
+ });
408
+
409
+ test("getVariants setDefaultVariants does not override setVariants from a previous pass", () => {
410
+ const component = getModeComponent(
411
+ mode,
412
+ cv({
413
+ variants: {
414
+ color: { red: "red", blue: "blue" },
415
+ done: "",
416
+ },
417
+ computed: ({ variants, setVariants, setDefaultVariants }) => {
418
+ if (!variants.done) {
419
+ setVariants({ color: "red", done: true });
420
+ }
421
+ if (variants.done) {
422
+ setDefaultVariants({ color: "blue" });
423
+ }
424
+ },
425
+ }),
183
426
  );
427
+ const variants = component.getVariants();
428
+ expect(variants).toEqual({ color: "red", done: true });
184
429
  });
185
430
 
186
431
  test("variantKeys property", () => {
@@ -217,6 +462,30 @@ for (const config of Object.values(CONFIGS)) {
217
462
  });
218
463
  }
219
464
 
465
+ test("propKeys are mode-specific", () => {
466
+ const component = cvBase({
467
+ variants: { size: { sm: "sm", md: "md" } },
468
+ });
469
+
470
+ expectTypeOf(component.propKeys).toEqualTypeOf<
471
+ ("class" | "className" | "style" | "size")[]
472
+ >();
473
+ expectTypeOf(component.jsx.propKeys).toEqualTypeOf<
474
+ ("className" | "style" | "size")[]
475
+ >();
476
+ expectTypeOf(component.html.propKeys).toEqualTypeOf<
477
+ ("class" | "style" | "size")[]
478
+ >();
479
+ expectTypeOf(component.htmlObj.propKeys).toEqualTypeOf<
480
+ ("class" | "style" | "size")[]
481
+ >();
482
+
483
+ expect(component.propKeys).toEqual(["class", "className", "style", "size"]);
484
+ expect(component.jsx.propKeys).toEqual(["className", "style", "size"]);
485
+ expect(component.html.propKeys).toEqual(["class", "style", "size"]);
486
+ expect(component.htmlObj.propKeys).toEqual(["class", "style", "size"]);
487
+ });
488
+
220
489
  describe("Variant utility type", () => {
221
490
  test("matches variant keys from another component", () => {
222
491
  const base = cvBase({