clava 0.4.0 → 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;
@@ -94,6 +97,10 @@ interface ComponentMeta {
94
97
 
95
98
  const META_KEY = "__meta";
96
99
 
100
+ interface ComponentWithMeta {
101
+ [META_KEY]?: ComponentMeta;
102
+ }
103
+
97
104
  const EMPTY_DEFAULTS: Record<string, unknown> = Object.freeze({}) as Record<
98
105
  string,
99
106
  unknown
@@ -116,18 +123,6 @@ function areVariantsEqual(
116
123
  return true;
117
124
  }
118
125
 
119
- function warnRefineLimit(runState: RefineRunState): void {
120
- if (runState.warned) return;
121
- runState.warned = true;
122
- if (process.env.NODE_ENV !== "production") {
123
- console.warn(
124
- "Clava: Maximum refine iterations exceeded. This can happen when a " +
125
- "refine callback calls setVariants or setDefaultVariants, but one " +
126
- "of the variants changes on every run.",
127
- );
128
- }
129
- }
130
-
131
126
  function getExtUserVariantProps(
132
127
  userVariantProps: Record<string, unknown>,
133
128
  protectedVariants: Record<string, unknown> | null,
@@ -154,7 +149,9 @@ function mergeVariants(
154
149
  for (const key in source) {
155
150
  if (!Object.hasOwn(source, key)) continue;
156
151
  const value = source[key];
157
- if (!Object.is(target[key], value)) changed = true;
152
+ if (!Object.is(target[key], value)) {
153
+ changed = true;
154
+ }
158
155
  target[key] = value;
159
156
  }
160
157
  return changed;
@@ -163,21 +160,22 @@ function mergeVariants(
163
160
  if (!Object.hasOwn(source, key)) continue;
164
161
  if (skipKeys.has(key)) continue;
165
162
  const value = source[key];
166
- if (!Object.is(target[key], value)) changed = true;
163
+ if (!Object.is(target[key], value)) {
164
+ changed = true;
165
+ }
167
166
  target[key] = value;
168
167
  }
169
168
  return changed;
170
169
  }
171
170
 
172
- // Dynamic property access on function requires cast through unknown.
171
+ // Components carry internal metadata on a non-public property so user-facing
172
+ // component types stay clean.
173
173
  function getComponentMeta(component: AnyComponent): ComponentMeta | undefined {
174
- return (component as unknown as Record<string, unknown>)[META_KEY] as
175
- | ComponentMeta
176
- | undefined;
174
+ return (component as AnyComponent & ComponentWithMeta)[META_KEY];
177
175
  }
178
176
 
179
177
  function setComponentMeta(component: AnyComponent, meta: ComponentMeta): void {
180
- (component as unknown as Record<string, unknown>)[META_KEY] = meta;
178
+ (component as AnyComponent & ComponentWithMeta)[META_KEY] = meta;
181
179
  }
182
180
 
183
181
  export type {
@@ -321,9 +319,15 @@ function isVariantDisabled(
321
319
  }
322
320
 
323
321
  function getVariantValueKey(value: unknown): string | undefined {
324
- if (typeof value === "string") return value;
325
- if (typeof value === "number") return String(value);
326
- if (typeof value === "boolean") return String(value);
322
+ if (typeof value === "string") {
323
+ return value;
324
+ }
325
+ if (typeof value === "number") {
326
+ return String(value);
327
+ }
328
+ if (typeof value === "boolean") {
329
+ return String(value);
330
+ }
327
331
  return undefined;
328
332
  }
329
333
 
@@ -343,7 +347,9 @@ function collectDisabledVariantKeys(
343
347
  config: CVConfig<Variants, AnyComponent[]>,
344
348
  ): Set<string> {
345
349
  const keys = new Set<string>();
346
- if (!config.variants) return keys;
350
+ if (!config.variants) {
351
+ return keys;
352
+ }
347
353
  for (const key in config.variants) {
348
354
  if (!Object.hasOwn(config.variants, key)) continue;
349
355
  if ((config.variants as Record<string, unknown>)[key] === null) {
@@ -357,7 +363,9 @@ function collectDisabledVariantValues(
357
363
  config: CVConfig<Variants, AnyComponent[]>,
358
364
  ): Record<string, Set<string>> {
359
365
  const values: Record<string, Set<string>> = {};
360
- if (!config.variants) return values;
366
+ if (!config.variants) {
367
+ return values;
368
+ }
361
369
  for (const key in config.variants) {
362
370
  if (!Object.hasOwn(config.variants, key)) continue;
363
371
  const variant = (config.variants as Record<string, unknown>)[key];
@@ -397,14 +405,22 @@ function normalizeKeySource(source: unknown): NormalizedSource {
397
405
  };
398
406
  }
399
407
 
400
- if (!source) return EMPTY_SOURCE;
408
+ if (!source) {
409
+ return EMPTY_SOURCE;
410
+ }
401
411
  if (typeof source !== "object" && typeof source !== "function") {
402
412
  return EMPTY_SOURCE;
403
413
  }
404
414
  const typed = source as Record<string, unknown>;
405
- if (typeof typed.getVariants !== "function") return EMPTY_SOURCE;
406
- if (!Array.isArray(typed.propKeys)) return EMPTY_SOURCE;
407
- if (!Array.isArray(typed.variantKeys)) return EMPTY_SOURCE;
415
+ if (typeof typed.getVariants !== "function") {
416
+ return EMPTY_SOURCE;
417
+ }
418
+ if (!Array.isArray(typed.propKeys)) {
419
+ return EMPTY_SOURCE;
420
+ }
421
+ if (!Array.isArray(typed.variantKeys)) {
422
+ return EMPTY_SOURCE;
423
+ }
408
424
 
409
425
  return {
410
426
  propKeys: typed.propKeys as string[],
@@ -540,7 +556,9 @@ function buildPrebuiltVariant(variantDef: unknown): PrebuiltVariant {
540
556
  if (!Object.hasOwn(variantDef, key)) continue;
541
557
  const value = variantDef[key];
542
558
  if (value === null) {
543
- if (!disabledValues) disabledValues = new Set<string>();
559
+ if (!disabledValues) {
560
+ disabledValues = new Set<string>();
561
+ }
544
562
  disabledValues.add(key);
545
563
  continue;
546
564
  }
@@ -619,7 +637,9 @@ export function create({
619
637
  if (extend) {
620
638
  for (const ext of extend) {
621
639
  const meta = getComponentMeta(ext);
622
- if (meta) Object.assign(staticDefaults, meta.staticDefaults);
640
+ if (meta) {
641
+ Object.assign(staticDefaults, meta.staticDefaults);
642
+ }
623
643
  }
624
644
  }
625
645
  if (variants) {
@@ -700,6 +720,18 @@ export function create({
700
720
  const extMetasWithRefineCount = extMetasWithRefine.length;
701
721
  const shouldCollectChangedVariants = extMetasWithRefineCount > 0;
702
722
 
723
+ // Call-site frame captured at the `cv()` call site so refine-limit warnings
724
+ // can point developers at the component definition. Skipped entirely for
725
+ // components that can never enter the refine loop, and stripped in
726
+ // production via the NODE_ENV guard inside `captureCreationFrame`. The
727
+ // frame is captured at creation time but the underlying `.stack` string is
728
+ // formatted lazily on first access, so component creation stays cheap
729
+ // unless the warning actually fires.
730
+ const canTriggerRefineWarning = !!refine || extMetasWithRefineCount > 0;
731
+ const creationFrame = canTriggerRefineWarning
732
+ ? captureCreationFrame(cv)
733
+ : undefined;
734
+
703
735
  // Function variant keys inherited from extends, filtered through this
704
736
  // component's own variants: a static (object/shorthand) variant in this
705
737
  // component replaces an inherited function variant for the same key.
@@ -753,7 +785,9 @@ export function create({
753
785
  staticVariantsOverridingExtFn !== null
754
786
  ) {
755
787
  staticExtSkipKeys = new Set<string>();
756
- for (const k of disabledVariantKeys) staticExtSkipKeys.add(k);
788
+ for (const k of disabledVariantKeys) {
789
+ staticExtSkipKeys.add(k);
790
+ }
757
791
  for (let i = 0; i < functionVariantCount; i++) {
758
792
  staticExtSkipKeys.add(functionVariantNames[i]);
759
793
  }
@@ -777,7 +811,9 @@ export function create({
777
811
  ): void {
778
812
  if (!hasAnyDisabled) {
779
813
  for (const key in input) {
780
- if (Object.hasOwn(input, key)) out[key] = input[key];
814
+ if (Object.hasOwn(input, key)) {
815
+ out[key] = input[key];
816
+ }
781
817
  }
782
818
  return;
783
819
  }
@@ -901,7 +937,9 @@ export function create({
901
937
  defaults[k] = v;
902
938
  }
903
939
 
904
- if (!hasAnyDisabled) return defaults;
940
+ if (!hasAnyDisabled) {
941
+ return defaults;
942
+ }
905
943
 
906
944
  // Filter disabled
907
945
  const result: Record<string, unknown> = {};
@@ -938,7 +976,9 @@ export function create({
938
976
  const filteredVariants: Record<string, unknown> = {};
939
977
  for (let i = 0; i < variantKeysLength; i++) {
940
978
  const k = variantKeys[i];
941
- if (Object.hasOwn(resolved, k)) filteredVariants[k] = resolved[k];
979
+ if (Object.hasOwn(resolved, k)) {
980
+ filteredVariants[k] = resolved[k];
981
+ }
942
982
  }
943
983
  ownVariants = filteredVariants;
944
984
  }
@@ -949,7 +989,9 @@ export function create({
949
989
  const localCClasses: ClassValue[] | null = collectOutput ? [] : null;
950
990
  let localCStyle: StyleValue | null = null;
951
991
  const ensureUpdated = (): Record<string, unknown> => {
952
- if (updatedVariants) return updatedVariants;
992
+ if (updatedVariants) {
993
+ return updatedVariants;
994
+ }
953
995
  const u: Record<string, unknown> = {};
954
996
  Object.assign(u, ownVariants);
955
997
  updatedVariants = u;
@@ -961,7 +1003,9 @@ export function create({
961
1003
  protect = false,
962
1004
  ) => {
963
1005
  if (shouldCollectChangedVariants) {
964
- if (!changedVariants) changedVariants = {};
1006
+ if (!changedVariants) {
1007
+ changedVariants = {};
1008
+ }
965
1009
  changedVariants[key] = value;
966
1010
  }
967
1011
  if (protect && protectedVariants) {
@@ -982,7 +1026,7 @@ export function create({
982
1026
  if (!Object.hasOwn(newVariants, key)) continue;
983
1027
  const value = (newVariants as Record<string, unknown>)[key];
984
1028
  setChangedVariant(key, value, true);
985
- if (getCurrentVariantValue(key) === value) continue;
1029
+ if (Object.is(getCurrentVariantValue(key), value)) continue;
986
1030
  ensureUpdated()[key] = value;
987
1031
  }
988
1032
  return;
@@ -1001,7 +1045,7 @@ export function create({
1001
1045
  }
1002
1046
  }
1003
1047
  setChangedVariant(key, value, true);
1004
- if (getCurrentVariantValue(key) === value) continue;
1048
+ if (Object.is(getCurrentVariantValue(key), value)) continue;
1005
1049
  ensureUpdated()[key] = value;
1006
1050
  }
1007
1051
  },
@@ -1023,11 +1067,11 @@ export function create({
1023
1067
  continue;
1024
1068
  }
1025
1069
  }
1070
+ if (Object.is(getCurrentVariantValue(key), value)) continue;
1026
1071
  setChangedVariant(key, value);
1027
1072
  if (pendingProtectedVariants) {
1028
1073
  pendingProtectedVariants[key] = value;
1029
1074
  }
1030
- if (getCurrentVariantValue(key) === value) continue;
1031
1075
  ensureUpdated()[key] = value;
1032
1076
  }
1033
1077
  },
@@ -1036,16 +1080,22 @@ export function create({
1036
1080
  },
1037
1081
  addStyle: (newStyle: StyleValue) => {
1038
1082
  if (!collectOutput) return;
1039
- if (!localCStyle) localCStyle = {};
1083
+ if (!localCStyle) {
1084
+ localCStyle = {};
1085
+ }
1040
1086
  Object.assign(localCStyle, newStyle);
1041
1087
  },
1042
1088
  };
1043
1089
  const result = refine(ctx);
1044
1090
  if (collectOutput && result != null) {
1045
1091
  const r = extractClassAndStylePrebuilt(result);
1046
- if (r.class != null) localCClasses?.push(r.class);
1092
+ if (r.class != null) {
1093
+ localCClasses?.push(r.class);
1094
+ }
1047
1095
  if (r.style) {
1048
- if (!localCStyle) localCStyle = {};
1096
+ if (!localCStyle) {
1097
+ localCStyle = {};
1098
+ }
1049
1099
  Object.assign(localCStyle, r.style);
1050
1100
  }
1051
1101
  }
@@ -1129,7 +1179,9 @@ export function create({
1129
1179
  extSkipKeys = skipKeys;
1130
1180
  } else {
1131
1181
  extSkipKeys = new Set(skipKeys);
1132
- for (const k of staticExtSkipKeys) extSkipKeys.add(k);
1182
+ for (const k of staticExtSkipKeys) {
1183
+ extSkipKeys.add(k);
1184
+ }
1133
1185
  }
1134
1186
 
1135
1187
  let extSkipVals: Record<string, Set<string>> | null;
@@ -1146,7 +1198,9 @@ export function create({
1146
1198
  const existing = extSkipVals[k];
1147
1199
  if (existing) {
1148
1200
  const merged = new Set<string>(existing);
1149
- for (const v of staticExtSkipValues[k]) merged.add(v);
1201
+ for (const v of staticExtSkipValues[k]) {
1202
+ merged.add(v);
1203
+ }
1150
1204
  extSkipVals[k] = merged;
1151
1205
  } else {
1152
1206
  extSkipVals[k] = staticExtSkipValues[k];
@@ -1211,7 +1265,9 @@ export function create({
1211
1265
  }
1212
1266
 
1213
1267
  // Apply own base style (after extends' styles, matching original order).
1214
- if (hasBaseStyle) Object.assign(styleOut, baseStyle);
1268
+ if (hasBaseStyle) {
1269
+ Object.assign(styleOut, baseStyle);
1270
+ }
1215
1271
 
1216
1272
  // Apply own variants. Skip keys/values come from caller (e.g., parent
1217
1273
  // wants its own function variant to override this variant).
@@ -1246,12 +1302,20 @@ export function create({
1246
1302
  if (selectedKey == null) continue;
1247
1303
  const v = variant.values[selectedKey];
1248
1304
  if (!v) continue;
1249
- if (v.class != null) classesOut.push(v.class as ClsxClassValue);
1250
- if (v.style) Object.assign(styleOut, v.style);
1305
+ if (v.class != null) {
1306
+ classesOut.push(v.class as ClsxClassValue);
1307
+ }
1308
+ if (v.style) {
1309
+ Object.assign(styleOut, v.style);
1310
+ }
1251
1311
  } else if (variant.shorthand && selectedValue === true) {
1252
1312
  const v = variant.shorthand;
1253
- if (v.class != null) classesOut.push(v.class as ClsxClassValue);
1254
- if (v.style) Object.assign(styleOut, v.style);
1313
+ if (v.class != null) {
1314
+ classesOut.push(v.class as ClsxClassValue);
1315
+ }
1316
+ if (v.style) {
1317
+ Object.assign(styleOut, v.style);
1318
+ }
1255
1319
  }
1256
1320
  }
1257
1321
 
@@ -1275,8 +1339,12 @@ export function create({
1275
1339
  const computedResult = fn(selectedValue);
1276
1340
  if (computedResult == null) continue;
1277
1341
  const r = extractClassAndStylePrebuilt(computedResult);
1278
- if (r.class != null) classesOut.push(r.class as ClsxClassValue);
1279
- if (r.style) Object.assign(styleOut, r.style);
1342
+ if (r.class != null) {
1343
+ classesOut.push(r.class as ClsxClassValue);
1344
+ }
1345
+ if (r.style) {
1346
+ Object.assign(styleOut, r.style);
1347
+ }
1280
1348
  }
1281
1349
 
1282
1350
  // Apply `refine` results — must come after own variants (static and
@@ -1286,38 +1354,16 @@ export function create({
1286
1354
  classesOut.push(cClasses[i] as ClsxClassValue);
1287
1355
  }
1288
1356
  }
1289
- if (cStyle) Object.assign(styleOut, cStyle);
1357
+ if (cStyle) {
1358
+ Object.assign(styleOut, cStyle);
1359
+ }
1290
1360
 
1291
1361
  return workingResolved;
1292
1362
  };
1293
1363
 
1294
1364
  const compute: ComputeFn =
1295
1365
  !refine && extMetasWithRefineCount === 0
1296
- ? (
1297
- resolved,
1298
- userVariantProps,
1299
- skipKeys,
1300
- skipValues,
1301
- classesOut,
1302
- styleOut,
1303
- runState,
1304
- protectedVariants,
1305
- pendingProtectedVariants,
1306
- protectedVariantKeys,
1307
- ) => {
1308
- return computeOnce(
1309
- resolved,
1310
- userVariantProps,
1311
- skipKeys,
1312
- skipValues,
1313
- classesOut,
1314
- styleOut,
1315
- runState,
1316
- protectedVariants,
1317
- pendingProtectedVariants,
1318
- protectedVariantKeys,
1319
- );
1320
- }
1366
+ ? computeOnce
1321
1367
  : (
1322
1368
  resolved,
1323
1369
  userVariantProps,
@@ -1330,10 +1376,13 @@ export function create({
1330
1376
  pendingProtectedVariants,
1331
1377
  protectedVariantKeys,
1332
1378
  ) => {
1333
- runState ??= { remaining: MAX_REFINE_RUNS, warned: false };
1379
+ runState ??= { remaining: MAX_REFINE_RUNS };
1334
1380
  protectedVariants ??= {};
1335
1381
  protectedVariantKeys ??= new Set<string>();
1336
1382
  let workingResolved = resolved;
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;
1337
1386
  let lastClasses: ClsxClassValue[] = [];
1338
1387
  let lastStyle: StyleValue = {};
1339
1388
  let isFirstRun = true;
@@ -1397,10 +1446,28 @@ export function create({
1397
1446
  return nextResolved;
1398
1447
  }
1399
1448
 
1449
+ if (
1450
+ process.env.NODE_ENV !== "production" &&
1451
+ runState.remaining < REFINE_UNSTABLE_TRACKING_WINDOW
1452
+ ) {
1453
+ if (!unstableChanges) {
1454
+ unstableChanges = new Map<string, VariantChange>();
1455
+ }
1456
+ accumulateUnstableVariantChanges(
1457
+ unstableChanges,
1458
+ workingResolved,
1459
+ nextResolved,
1460
+ );
1461
+ }
1462
+
1400
1463
  if (useDirectOutput && runState.remaining === 0) {
1401
1464
  // Keep the direct output from the last allowed run. Rolling
1402
1465
  // back here would drop it before the fallback copy below.
1403
- warnRefineLimit(runState);
1466
+ warnRefineLimit({
1467
+ runState,
1468
+ creationFrame,
1469
+ unstableChanges,
1470
+ });
1404
1471
  return nextResolved;
1405
1472
  }
1406
1473
 
@@ -1420,7 +1487,11 @@ export function create({
1420
1487
  isFirstRun = false;
1421
1488
  }
1422
1489
 
1423
- warnRefineLimit(runState);
1490
+ warnRefineLimit({
1491
+ runState,
1492
+ creationFrame,
1493
+ unstableChanges,
1494
+ });
1424
1495
 
1425
1496
  for (let i = 0; i < lastClasses.length; i++) {
1426
1497
  classesOut.push(lastClasses[i]);
@@ -1493,10 +1564,14 @@ export function create({
1493
1564
  pendingProtectedVariants,
1494
1565
  protectedVariantKeys,
1495
1566
  ) => {
1496
- runState ??= { remaining: MAX_REFINE_RUNS, warned: false };
1567
+ runState ??= { remaining: MAX_REFINE_RUNS };
1497
1568
  protectedVariants ??= {};
1498
1569
  protectedVariantKeys ??= new Set<string>();
1499
1570
  let workingResolved = resolved;
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;
1500
1575
  let reachedLimit = true;
1501
1576
 
1502
1577
  while (runState.remaining > 0) {
@@ -1536,11 +1611,28 @@ export function create({
1536
1611
  break;
1537
1612
  }
1538
1613
 
1614
+ if (
1615
+ process.env.NODE_ENV !== "production" &&
1616
+ runState.remaining < REFINE_UNSTABLE_TRACKING_WINDOW
1617
+ ) {
1618
+ if (!unstableChanges) {
1619
+ unstableChanges = new Map<string, VariantChange>();
1620
+ }
1621
+ accumulateUnstableVariantChanges(
1622
+ unstableChanges,
1623
+ workingResolved,
1624
+ nextResolved,
1625
+ );
1626
+ }
1539
1627
  workingResolved = nextResolved;
1540
1628
  }
1541
1629
 
1542
1630
  if (reachedLimit) {
1543
- warnRefineLimit(runState);
1631
+ warnRefineLimit({
1632
+ runState,
1633
+ creationFrame,
1634
+ unstableChanges,
1635
+ });
1544
1636
  }
1545
1637
 
1546
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
+ }