clava 0.4.1 → 0.4.2

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/index.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  import clsx, { type ClassValue as ClsxClassValue } from "clsx";
2
+ import {
3
+ REFINE_UNSTABLE_TRACKING_WINDOW,
4
+ type RefineRunState,
5
+ type VariantChange,
6
+ accumulateUnstableVariantChanges,
7
+ captureCreationFrame,
8
+ warnRefineLimit,
9
+ } from "./refine-warning.ts";
2
10
  import type {
3
11
  AnyComponent,
4
12
  CVComponent,
@@ -57,11 +65,6 @@ type ResolveRefineFn = (
57
65
  protectedVariantKeys?: Set<string> | null,
58
66
  ) => Record<string, unknown>;
59
67
 
60
- interface RefineRunState {
61
- remaining: number;
62
- warned?: boolean;
63
- }
64
-
65
68
  // Internal metadata stored on components but hidden from public types.
66
69
  interface ComponentMeta {
67
70
  baseClass: string;
@@ -105,13 +108,6 @@ const EMPTY_DEFAULTS: Record<string, unknown> = Object.freeze({}) as Record<
105
108
 
106
109
  const MAX_REFINE_RUNS = 50;
107
110
 
108
- // Once a refine loop is within this many iterations of the cap, start tracking
109
- // every variant key that changes between iterations so the warning can report
110
- // every key that contributed to the oscillation, not just the keys that
111
- // happened to flip on the final step. Convergent loops (the common case) exit
112
- // well before this threshold and pay no per-iteration tracking cost.
113
- const REFINE_UNSTABLE_TRACKING_WINDOW = 10;
114
-
115
111
  function areVariantsEqual(
116
112
  a: Record<string, unknown>,
117
113
  b: Record<string, unknown>,
@@ -127,96 +123,6 @@ function areVariantsEqual(
127
123
  return true;
128
124
  }
129
125
 
130
- interface CreationFrame {
131
- stack?: string;
132
- }
133
-
134
- // Captures the call site of the function passed in `skipFn` so refine-limit
135
- // warnings can point developers at the originating `cv()` call. Returns
136
- // `undefined` in production so bundlers that replace `process.env.NODE_ENV` at
137
- // build time can drop the entire warning machinery. The underlying `.stack`
138
- // string is formatted lazily on first access in every major engine (V8,
139
- // SpiderMonkey, JavaScriptCore), so holding the captured frame for the
140
- // lifetime of the component is cheap when no warning fires.
141
- function captureCreationFrame(skipFn: Function): CreationFrame | undefined {
142
- if (process.env.NODE_ENV === "production") return undefined;
143
- if (typeof Error.captureStackTrace === "function") {
144
- const holder: CreationFrame = {};
145
- Error.captureStackTrace(holder, skipFn);
146
- return holder;
147
- }
148
- // Engines without `Error.captureStackTrace` (SpiderMonkey, JavaScriptCore)
149
- // can't strip internal frames, but their `Error.stack` getter is still
150
- // lazy, so returning the Error instance defers the format cost. The
151
- // resulting trace includes 1–2 extra frames at the top from this helper and
152
- // `cv` itself.
153
- return new Error();
154
- }
155
-
156
- function formatCreationStack(frame: CreationFrame): string | undefined {
157
- let stack = frame.stack;
158
- if (!stack) return undefined;
159
- // V8 prefixes the stack with a leading "Error" / "Error: message" line that
160
- // isn't meaningful for a captured location — drop it.
161
- const newlineIdx = stack.indexOf("\n");
162
- if (newlineIdx > 0) {
163
- const firstLine = stack.slice(0, newlineIdx);
164
- if (firstLine === "Error" || firstLine.startsWith("Error:")) {
165
- stack = stack.slice(newlineIdx + 1);
166
- }
167
- }
168
- return stack;
169
- }
170
-
171
- // Accumulates the union of variant keys that differ between `prev` and `next`
172
- // into `into`. Called on every non-converging iteration of the refine loop so
173
- // the refine-limit warning can report any key that ever changed across runs,
174
- // not just the keys that changed on the final iteration (two keys flipping at
175
- // different cadences could otherwise hide each other on the last step).
176
- function accumulateUnstableVariantKeys(
177
- into: Set<string>,
178
- prev: Record<string, unknown>,
179
- next: Record<string, unknown>,
180
- ): void {
181
- for (const key in next) {
182
- if (!Object.hasOwn(next, key)) continue;
183
- if (!Object.is(prev[key], next[key])) {
184
- into.add(key);
185
- }
186
- }
187
- for (const key in prev) {
188
- if (!Object.hasOwn(prev, key)) continue;
189
- if (Object.hasOwn(next, key)) continue;
190
- into.add(key);
191
- }
192
- }
193
-
194
- function warnRefineLimit(
195
- runState: RefineRunState,
196
- creationFrame: CreationFrame | undefined,
197
- unstableKeys: Set<string> | null,
198
- ): void {
199
- // Bundlers are expected to replace this branch with a production literal,
200
- // allowing warning-only code below to be removed from consumer bundles.
201
- if (process.env.NODE_ENV === "production") return;
202
- if (runState.warned) return;
203
- runState.warned = true;
204
- let message =
205
- "Clava: Maximum refine iterations exceeded. This can happen when a " +
206
- "refine callback calls setVariants or setDefaultVariants, but one " +
207
- "of the variants changes on every run.";
208
- if (unstableKeys && unstableKeys.size > 0) {
209
- message += `\nVariant(s) that did not stabilize: ${Array.from(unstableKeys).join(", ")}.`;
210
- }
211
- if (creationFrame) {
212
- const creationStack = formatCreationStack(creationFrame);
213
- if (creationStack) {
214
- message += `\nComponent created at:\n${creationStack}`;
215
- }
216
- }
217
- console.warn(message);
218
- }
219
-
220
126
  function getExtUserVariantProps(
221
127
  userVariantProps: Record<string, unknown>,
222
128
  protectedVariants: Record<string, unknown> | null,
@@ -1120,7 +1026,7 @@ export function create({
1120
1026
  if (!Object.hasOwn(newVariants, key)) continue;
1121
1027
  const value = (newVariants as Record<string, unknown>)[key];
1122
1028
  setChangedVariant(key, value, true);
1123
- if (getCurrentVariantValue(key) === value) continue;
1029
+ if (Object.is(getCurrentVariantValue(key), value)) continue;
1124
1030
  ensureUpdated()[key] = value;
1125
1031
  }
1126
1032
  return;
@@ -1139,7 +1045,7 @@ export function create({
1139
1045
  }
1140
1046
  }
1141
1047
  setChangedVariant(key, value, true);
1142
- if (getCurrentVariantValue(key) === value) continue;
1048
+ if (Object.is(getCurrentVariantValue(key), value)) continue;
1143
1049
  ensureUpdated()[key] = value;
1144
1050
  }
1145
1051
  },
@@ -1161,11 +1067,11 @@ export function create({
1161
1067
  continue;
1162
1068
  }
1163
1069
  }
1070
+ if (Object.is(getCurrentVariantValue(key), value)) continue;
1164
1071
  setChangedVariant(key, value);
1165
1072
  if (pendingProtectedVariants) {
1166
1073
  pendingProtectedVariants[key] = value;
1167
1074
  }
1168
- if (getCurrentVariantValue(key) === value) continue;
1169
1075
  ensureUpdated()[key] = value;
1170
1076
  }
1171
1077
  },
@@ -1474,11 +1380,9 @@ export function create({
1474
1380
  protectedVariants ??= {};
1475
1381
  protectedVariantKeys ??= new Set<string>();
1476
1382
  let workingResolved = resolved;
1477
- // Union of variant keys that differed on non-converging iterations
1478
- // inside the tracking window, so the refine-limit warning can name
1479
- // every variant that contributed to the late-stage oscillation.
1480
- // Lazy-init keeps convergent loops allocation-free.
1481
- let unstableKeys: Set<string> | null = null;
1383
+ // Latest variant changes from non-converging iterations inside the
1384
+ // tracking window. Lazy-init keeps convergent loops allocation-free.
1385
+ let unstableChanges: Map<string, VariantChange> | null = null;
1482
1386
  let lastClasses: ClsxClassValue[] = [];
1483
1387
  let lastStyle: StyleValue = {};
1484
1388
  let isFirstRun = true;
@@ -1546,11 +1450,11 @@ export function create({
1546
1450
  process.env.NODE_ENV !== "production" &&
1547
1451
  runState.remaining < REFINE_UNSTABLE_TRACKING_WINDOW
1548
1452
  ) {
1549
- if (!unstableKeys) {
1550
- unstableKeys = new Set<string>();
1453
+ if (!unstableChanges) {
1454
+ unstableChanges = new Map<string, VariantChange>();
1551
1455
  }
1552
- accumulateUnstableVariantKeys(
1553
- unstableKeys,
1456
+ accumulateUnstableVariantChanges(
1457
+ unstableChanges,
1554
1458
  workingResolved,
1555
1459
  nextResolved,
1556
1460
  );
@@ -1559,7 +1463,11 @@ export function create({
1559
1463
  if (useDirectOutput && runState.remaining === 0) {
1560
1464
  // Keep the direct output from the last allowed run. Rolling
1561
1465
  // back here would drop it before the fallback copy below.
1562
- warnRefineLimit(runState, creationFrame, unstableKeys);
1466
+ warnRefineLimit({
1467
+ runState,
1468
+ creationFrame,
1469
+ unstableChanges,
1470
+ });
1563
1471
  return nextResolved;
1564
1472
  }
1565
1473
 
@@ -1579,7 +1487,11 @@ export function create({
1579
1487
  isFirstRun = false;
1580
1488
  }
1581
1489
 
1582
- warnRefineLimit(runState, creationFrame, unstableKeys);
1490
+ warnRefineLimit({
1491
+ runState,
1492
+ creationFrame,
1493
+ unstableChanges,
1494
+ });
1583
1495
 
1584
1496
  for (let i = 0; i < lastClasses.length; i++) {
1585
1497
  classesOut.push(lastClasses[i]);
@@ -1656,11 +1568,10 @@ export function create({
1656
1568
  protectedVariants ??= {};
1657
1569
  protectedVariantKeys ??= new Set<string>();
1658
1570
  let workingResolved = resolved;
1659
- // Union of variant keys that differed on non-converging iterations
1660
- // inside the tracking window see the compute loop above for the
1661
- // shared rationale. Lazy-init keeps convergent loops
1662
- // allocation-free.
1663
- let unstableKeys: Set<string> | null = null;
1571
+ // Latest variant changes from non-converging iterations inside the
1572
+ // tracking window. See the compute loop above for the shared
1573
+ // rationale.
1574
+ let unstableChanges: Map<string, VariantChange> | null = null;
1664
1575
  let reachedLimit = true;
1665
1576
 
1666
1577
  while (runState.remaining > 0) {
@@ -1704,11 +1615,11 @@ export function create({
1704
1615
  process.env.NODE_ENV !== "production" &&
1705
1616
  runState.remaining < REFINE_UNSTABLE_TRACKING_WINDOW
1706
1617
  ) {
1707
- if (!unstableKeys) {
1708
- unstableKeys = new Set<string>();
1618
+ if (!unstableChanges) {
1619
+ unstableChanges = new Map<string, VariantChange>();
1709
1620
  }
1710
- accumulateUnstableVariantKeys(
1711
- unstableKeys,
1621
+ accumulateUnstableVariantChanges(
1622
+ unstableChanges,
1712
1623
  workingResolved,
1713
1624
  nextResolved,
1714
1625
  );
@@ -1717,7 +1628,11 @@ export function create({
1717
1628
  }
1718
1629
 
1719
1630
  if (reachedLimit) {
1720
- warnRefineLimit(runState, creationFrame, unstableKeys);
1631
+ warnRefineLimit({
1632
+ runState,
1633
+ creationFrame,
1634
+ unstableChanges,
1635
+ });
1721
1636
  }
1722
1637
 
1723
1638
  return workingResolved;
@@ -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
+ "refine callback calls setVariants or setDefaultVariants, but one " +
149
+ "of the variants changes 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
+ }
@@ -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["']/);
@@ -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
+ });
@@ -125,6 +125,125 @@ for (const config of Object.values(CONFIGS)) {
125
125
  }
126
126
  });
127
127
 
128
+ test("refine converges with undefined setDefaultVariants", () => {
129
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
130
+ const base = cv({
131
+ variants: {
132
+ invert: { true: "invert" },
133
+ offset: (value: boolean | undefined) =>
134
+ value ? "offset" : undefined,
135
+ push: (value: number | undefined) =>
136
+ value === undefined ? undefined : `push-${value}`,
137
+ },
138
+ refine: ({ variants, setDefaultVariants }) => {
139
+ setDefaultVariants({
140
+ offset: !variants.invert,
141
+ push: variants.invert ? 20 : undefined,
142
+ });
143
+ },
144
+ });
145
+ const component = getModeComponent(mode, cv({ extend: [base] }));
146
+
147
+ try {
148
+ const props = component();
149
+ expect(getStyleClass(props)).toEqual({
150
+ class: cls("offset"),
151
+ });
152
+ expect(component.getVariants()).toEqual({
153
+ offset: true,
154
+ push: undefined,
155
+ });
156
+ expect(warn).not.toHaveBeenCalled();
157
+ } finally {
158
+ warn.mockRestore();
159
+ }
160
+ });
161
+
162
+ test("refine converges with undefined setDefaultVariants in nested extends", () => {
163
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
164
+ const calls = {
165
+ layer: 0,
166
+ frame: 0,
167
+ control: 0,
168
+ };
169
+ const layer = cv({
170
+ variants: {
171
+ layer: { true: "layer" },
172
+ invert: { true: "invert" },
173
+ offset: (value: boolean | undefined) =>
174
+ value ? "offset" : undefined,
175
+ push: (value: number | undefined) =>
176
+ value === undefined ? undefined : `push-${value}`,
177
+ },
178
+ defaultVariants: {
179
+ layer: true,
180
+ },
181
+ refine: ({ variants, setDefaultVariants }) => {
182
+ calls.layer += 1;
183
+ setDefaultVariants({
184
+ offset: !variants.invert,
185
+ push: variants.invert ? 20 : undefined,
186
+ });
187
+ },
188
+ });
189
+ const frame = cv({
190
+ extend: [layer],
191
+ variants: {
192
+ frame: { true: "frame" },
193
+ },
194
+ defaultVariants: {
195
+ frame: true,
196
+ },
197
+ refine: () => {
198
+ calls.frame += 1;
199
+ },
200
+ });
201
+ const control = cv({
202
+ extend: [frame],
203
+ variants: {
204
+ control: { true: "control" },
205
+ },
206
+ defaultVariants: {
207
+ control: true,
208
+ offset: true,
209
+ },
210
+ refine: () => {
211
+ calls.control += 1;
212
+ },
213
+ });
214
+ const component = getModeComponent(mode, cv({ extend: [control] }));
215
+
216
+ try {
217
+ const props = component();
218
+ expect(getStyleClass(props)).toEqual({
219
+ class: cls("layer offset frame control"),
220
+ });
221
+ expect(calls).toEqual({
222
+ layer: 2,
223
+ frame: 2,
224
+ control: 2,
225
+ });
226
+ calls.layer = 0;
227
+ calls.frame = 0;
228
+ calls.control = 0;
229
+ expect(component.getVariants()).toEqual({
230
+ layer: true,
231
+ offset: true,
232
+ push: undefined,
233
+ frame: true,
234
+ control: true,
235
+ });
236
+ expect(calls).toEqual({
237
+ layer: 2,
238
+ frame: 2,
239
+ control: 2,
240
+ });
241
+ expect(warn).not.toHaveBeenCalled();
242
+ } finally {
243
+ warn.mockRestore();
244
+ }
245
+ });
246
+
128
247
  test("refine with setDefaultVariants", () => {
129
248
  const component = getModeComponent(
130
249
  mode,
@@ -719,6 +838,11 @@ for (const config of Object.values(CONFIGS)) {
719
838
  /Variant\(s\) that did not stabilize: [^\n]*\bsize\b/,
720
839
  ),
721
840
  );
841
+ expect(warn).toHaveBeenCalledWith(
842
+ expect.stringMatching(
843
+ /Latest variant changes before warning: [^\n]*\bsize: "(sm|lg)" -> "(sm|lg)"/,
844
+ ),
845
+ );
722
846
  expect(warn).toHaveBeenCalledWith(
723
847
  expect.stringContaining("Component created at:"),
724
848
  );
@@ -750,6 +874,11 @@ for (const config of Object.values(CONFIGS)) {
750
874
  /Variant\(s\) that did not stabilize: [^\n]*\bsize\b/,
751
875
  ),
752
876
  );
877
+ expect(warn).toHaveBeenCalledWith(
878
+ expect.stringMatching(
879
+ /Latest variant changes before warning: [^\n]*\bsize: "(sm|lg)" -> "(sm|lg)"/,
880
+ ),
881
+ );
753
882
  expect(warn).toHaveBeenCalledWith(
754
883
  expect.stringContaining("Component created at:"),
755
884
  );
@@ -853,6 +982,11 @@ for (const config of Object.values(CONFIGS)) {
853
982
  /Variant\(s\) that did not stabilize: [^\n]*\bcolor\b/,
854
983
  ),
855
984
  );
985
+ expect(warn).toHaveBeenCalledWith(
986
+ expect.stringMatching(
987
+ /Latest variant changes before warning: [^\n]*\bsize\b[^\n]*\bcolor\b/,
988
+ ),
989
+ );
856
990
  } finally {
857
991
  warn.mockRestore();
858
992
  }
@@ -879,7 +1013,50 @@ for (const config of Object.values(CONFIGS)) {
879
1013
  expect(warn).toHaveBeenCalledWith(
880
1014
  expect.stringContaining("refine.test.ts"),
881
1015
  );
1016
+ expect(warn).toHaveBeenCalledWith(
1017
+ expect.not.stringContaining("node_modules"),
1018
+ );
1019
+ } finally {
1020
+ warn.mockRestore();
1021
+ }
1022
+ });
1023
+
1024
+ test("refine warning fallback stack skips internal creation frames", () => {
1025
+ const ErrorWithCaptureStackTrace = Error as ErrorConstructor & {
1026
+ captureStackTrace?: (
1027
+ targetObject: object,
1028
+ constructorOpt?: Function,
1029
+ ) => void;
1030
+ };
1031
+ const captureStackTrace = ErrorWithCaptureStackTrace.captureStackTrace;
1032
+ Reflect.deleteProperty(ErrorWithCaptureStackTrace, "captureStackTrace");
1033
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
1034
+ const component = getModeComponent(
1035
+ mode,
1036
+ cv({
1037
+ variants: { size: { sm: "sm", lg: "lg" } },
1038
+ defaultVariants: { size: "sm" },
1039
+ refine: ({ variants, setVariants }) => {
1040
+ setVariants({ size: variants.size === "sm" ? "lg" : "sm" });
1041
+ },
1042
+ }),
1043
+ );
1044
+
1045
+ try {
1046
+ component();
1047
+ expect(warn).toHaveBeenCalledWith(
1048
+ expect.stringContaining("Component created at:"),
1049
+ );
1050
+ expect(warn).toHaveBeenCalledWith(
1051
+ expect.stringContaining("refine.test.ts"),
1052
+ );
1053
+ expect(warn).toHaveBeenCalledWith(
1054
+ expect.not.stringContaining("captureCreationFrame"),
1055
+ );
882
1056
  } finally {
1057
+ if (captureStackTrace) {
1058
+ ErrorWithCaptureStackTrace.captureStackTrace = captureStackTrace;
1059
+ }
883
1060
  warn.mockRestore();
884
1061
  }
885
1062
  });