clava 0.4.1 → 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.
@@ -0,0 +1,161 @@
1
+ export interface RefineRunState {
2
+ remaining: number;
3
+ warned?: boolean;
4
+ }
5
+
6
+ export interface CreationFrame {
7
+ stack?: string;
8
+ }
9
+
10
+ export interface VariantChange {
11
+ from: unknown;
12
+ to: unknown;
13
+ }
14
+
15
+ // Once a refine loop is within this many iterations of the cap, start tracking
16
+ // the latest value transition for every variant key that changes between
17
+ // iterations so the warning can report every key that contributed to the
18
+ // oscillation, not just the keys that happened to flip on the final step.
19
+ // Convergent loops (the common case) exit well before this threshold and pay no
20
+ // per-iteration tracking cost.
21
+ export const REFINE_UNSTABLE_TRACKING_WINDOW = 10;
22
+
23
+ // Captures the call site of the function passed in `skipFn` so refine-limit
24
+ // warnings can point developers at the originating `cv()` call. Returns
25
+ // `undefined` in production so bundlers that replace `process.env.NODE_ENV` at
26
+ // build time can drop the entire warning machinery. The underlying `.stack`
27
+ // string is formatted lazily on first access in every major engine (V8,
28
+ // SpiderMonkey, JavaScriptCore), so holding the captured frame for the
29
+ // lifetime of the component is cheap when no warning fires.
30
+ export function captureCreationFrame(
31
+ skipFn: Function,
32
+ ): CreationFrame | undefined {
33
+ if (process.env.NODE_ENV === "production") return undefined;
34
+ if (typeof Error.captureStackTrace === "function") {
35
+ const holder: CreationFrame = {};
36
+ Error.captureStackTrace(holder, skipFn);
37
+ return holder;
38
+ }
39
+ // Engines without `Error.captureStackTrace` (SpiderMonkey, JavaScriptCore)
40
+ // can't strip internal frames, but their `Error.stack` getter is still
41
+ // lazy, so returning the Error instance defers the format cost. The
42
+ // resulting trace includes 1–2 extra frames at the top from this helper and
43
+ // `cv` itself.
44
+ return new Error();
45
+ }
46
+
47
+ export function formatCreationStack(frame: CreationFrame): string | undefined {
48
+ let stack = frame.stack;
49
+ if (!stack) return undefined;
50
+ // V8 prefixes the stack with a leading "Error" / "Error: message" line that
51
+ // isn't meaningful for a captured location — drop it.
52
+ const newlineIdx = stack.indexOf("\n");
53
+ if (newlineIdx > 0) {
54
+ const firstLine = stack.slice(0, newlineIdx);
55
+ if (firstLine === "Error" || firstLine.startsWith("Error:")) {
56
+ stack = stack.slice(newlineIdx + 1);
57
+ }
58
+ }
59
+ const frames = stack.split("\n");
60
+ for (let i = 0; i < frames.length; i++) {
61
+ const line = frames[i]?.trim();
62
+ if (!line) continue;
63
+ if (isInternalCreationFrame(line)) continue;
64
+ if (line.includes("/node_modules/")) continue;
65
+ if (line.includes("\\node_modules\\")) continue;
66
+ if (line.includes("node:internal")) continue;
67
+ return ` ${line}`;
68
+ }
69
+ return undefined;
70
+ }
71
+
72
+ function isInternalCreationFrame(line: string): boolean {
73
+ if (line.includes("captureCreationFrame")) return true;
74
+ if (line.startsWith("at cv ")) return true;
75
+ if (line.startsWith("at cv(")) return true;
76
+ if (line.startsWith("cv@")) return true;
77
+ return false;
78
+ }
79
+
80
+ function formatVariantValue(value: unknown): string {
81
+ if (typeof value === "string") return JSON.stringify(value);
82
+ if (typeof value === "number") {
83
+ if (Number.isNaN(value)) return "NaN";
84
+ return String(value);
85
+ }
86
+ if (typeof value === "bigint") return `${value}n`;
87
+ if (value === undefined) return "undefined";
88
+ if (value === null) return "null";
89
+ if (typeof value === "boolean") return String(value);
90
+ if (typeof value === "symbol") return String(value);
91
+ if (typeof value === "function") return "[function]";
92
+ return "[object]";
93
+ }
94
+
95
+ function setVariantChange(
96
+ into: Map<string, VariantChange>,
97
+ key: string,
98
+ from: unknown,
99
+ to: unknown,
100
+ ): void {
101
+ into.set(key, { from, to });
102
+ }
103
+
104
+ export function accumulateUnstableVariantChanges(
105
+ into: Map<string, VariantChange>,
106
+ prev: Record<string, unknown>,
107
+ next: Record<string, unknown>,
108
+ ): void {
109
+ for (const key in next) {
110
+ if (!Object.hasOwn(next, key)) continue;
111
+ if (!Object.is(prev[key], next[key])) {
112
+ setVariantChange(into, key, prev[key], next[key]);
113
+ }
114
+ }
115
+ for (const key in prev) {
116
+ if (!Object.hasOwn(prev, key)) continue;
117
+ if (Object.hasOwn(next, key)) continue;
118
+ setVariantChange(into, key, prev[key], undefined);
119
+ }
120
+ }
121
+
122
+ function formatVariantChanges(changes: Map<string, VariantChange>): string {
123
+ return Array.from(changes)
124
+ .map(([key, { from, to }]) => {
125
+ return `${key}: ${formatVariantValue(from)} -> ${formatVariantValue(to)}`;
126
+ })
127
+ .join(", ");
128
+ }
129
+
130
+ interface WarnRefineLimitParams {
131
+ runState: RefineRunState;
132
+ creationFrame: CreationFrame | undefined;
133
+ unstableChanges: Map<string, VariantChange> | null;
134
+ }
135
+
136
+ export function warnRefineLimit({
137
+ runState,
138
+ creationFrame,
139
+ unstableChanges,
140
+ }: WarnRefineLimitParams): void {
141
+ // Bundlers are expected to replace this branch with a production literal,
142
+ // allowing warning-only code below to be removed from consumer bundles.
143
+ if (process.env.NODE_ENV === "production") return;
144
+ if (runState.warned) return;
145
+ runState.warned = true;
146
+ let message =
147
+ "Clava: Maximum refine iterations exceeded. This can happen when a " +
148
+ "computed default variant or refine callback changes one of the " +
149
+ "variants on every run.";
150
+ if (unstableChanges && unstableChanges.size > 0) {
151
+ message += `\nVariant(s) that did not stabilize: ${Array.from(unstableChanges.keys()).join(", ")}.`;
152
+ message += `\nLatest variant changes before warning: ${formatVariantChanges(unstableChanges)}.`;
153
+ }
154
+ if (creationFrame) {
155
+ const creationStack = formatCreationStack(creationFrame);
156
+ if (creationStack) {
157
+ message += `\nComponent created at:\n${creationStack}`;
158
+ }
159
+ }
160
+ console.warn(message);
161
+ }
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
  }
@@ -82,6 +82,7 @@ test("vite removes warning logic from the production bundle", async () => {
82
82
  expect(code).not.toContain("console.warn");
83
83
  expect(code).not.toContain("Clava: Maximum refine iterations exceeded");
84
84
  expect(code).not.toContain("Variant(s) that did not stabilize");
85
+ expect(code).not.toContain("Latest variant changes before warning");
85
86
  expect(code).not.toContain("Component created at");
86
87
  expect(code).not.toContain("captureStackTrace");
87
88
  expect(code).not.toMatch(/\.warned\b|["']warned["']/);
@@ -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 setDefaultVariants changes variants", () => {
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
- refine: ({ variants, setDefaultVariants }) => {
112
- setDefaultVariants({ color: "red" });
113
- if (variants.color === "red") {
114
- setDefaultVariants({ size: "lg" });
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 variants set by refine setDefaultVariants", () => {
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
- refine: ({ variants, setDefaultVariants }) => {
132
- if (variants.size === "lg") {
133
- setDefaultVariants({ color: "blue" });
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 setDefaultVariants does not override props", () => {
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
- refine: ({ setDefaultVariants }) => {
151
- setDefaultVariants({ color: "blue" });
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 setDefaultVariants from extended component", () => {
201
+ test("getVariants picks up computed defaultVariants from extended component", () => {
177
202
  const base = cv({
178
203
  variants: { size: { sm: "sm", lg: "lg" } },
179
- refine: ({ setDefaultVariants }) => {
180
- setDefaultVariants({ size: "lg" });
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 setDefaultVariants from grandparent component", () => {
220
+ test("getVariants picks up computed defaultVariants from grandparent component", () => {
196
221
  const grandparent = cv({
197
222
  variants: { size: { sm: "sm", lg: "lg" } },
198
- refine: ({ setDefaultVariants }) => {
199
- setDefaultVariants({ size: "lg" });
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 base setDefaultVariants after its own setVariants re-run", () => {
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: { size: "sm" },
249
- refine: ({ variants, setVariants, setDefaultVariants }) => {
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 setDefaultVariants across re-runs", () => {
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
- refine: ({ setDefaultVariants }) => {
283
- setDefaultVariants({ color: "blue" });
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
- refine: ({ setDefaultVariants }) => {
307
- setDefaultVariants({ color: "blue" });
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 setDefaultVariants can override child static defaults after a re-run", () => {
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
- refine: ({ variants, setVariants, setDefaultVariants }) => {
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 setDefaultVariants from later extends", () => {
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
- refine: ({ setDefaultVariants }) => {
360
- setDefaultVariants({ color: "blue" });
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 setDefaultVariants from later extends overrides setDefaultVariants from earlier extends", () => {
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
- refine: ({ setDefaultVariants }) => {
372
- setDefaultVariants({ color: "red" });
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
- refine: ({ setDefaultVariants }) => {
378
- setDefaultVariants({ color: "blue" });
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 setDefaultVariants does not override stable setVariants on later passes", () => {
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
- refine: ({ variants, setDefaultVariants }) => {
399
- if (variants.color === "red") {
400
- setDefaultVariants({ color: "blue" });
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 setDefaultVariants does not override setVariants from a previous pass", () => {
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
- refine: ({ variants, setVariants, setDefaultVariants }) => {
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
  );
@@ -144,7 +144,7 @@ for (const config of Object.values(CONFIGS)) {
144
144
  });
145
145
  });
146
146
 
147
- test("extend disabled variant value with refine setDefaultVariants", () => {
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
- refine: ({ setDefaultVariants }) => {
162
- setDefaultVariants({ size: "lg" });
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
- refine: ({ setDefaultVariants }) => {
177
- setDefaultVariants({
178
- // @ts-expect-error disabled variant value cannot be set
179
- size:
180
- // no error
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 resolveDefaults ignores polluted prototype on parent's refine", () => {
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 resolveDefaultsFn's resolvedVariants, the refine callback
33
- // would see size = "lg" instead of the staticDefault "sm" and emit the
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" } },
@@ -0,0 +1,28 @@
1
+ import { expect, test } from "vitest";
2
+ import { formatCreationStack } from "../src/refine-warning.ts";
3
+
4
+ test("formatCreationStack returns the first app frame", () => {
5
+ const stack = [
6
+ "Error",
7
+ " at captureCreationFrame (/repo/packages/clava/src/refine-warning.ts:31:10)",
8
+ " at cv (/repo/packages/clava/src/index.ts:734:9)",
9
+ " at eval (/repo/app/src/button.ts:12:21)",
10
+ " at ModuleJob.run (node:internal/modules/esm/module_job:343:25)",
11
+ ].join("\n");
12
+
13
+ expect(formatCreationStack({ stack })).toBe(
14
+ " at eval (/repo/app/src/button.ts:12:21)",
15
+ );
16
+ });
17
+
18
+ test("formatCreationStack omits dependency and runtime frames", () => {
19
+ const stack = [
20
+ "Error",
21
+ " at captureCreationFrame (/repo/packages/clava/src/refine-warning.ts:31:10)",
22
+ " at cv(/repo/packages/clava/src/index.ts:734:9)",
23
+ " at eval (/repo/node_modules/package/index.js:1:1)",
24
+ " at ModuleJob.run (node:internal/modules/esm/module_job:343:25)",
25
+ ].join("\n");
26
+
27
+ expect(formatCreationStack({ stack })).toBeUndefined();
28
+ });