clava 0.4.0 → 0.4.1

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
@@ -59,7 +59,7 @@ type ResolveRefineFn = (
59
59
 
60
60
  interface RefineRunState {
61
61
  remaining: number;
62
- warned: boolean;
62
+ warned?: boolean;
63
63
  }
64
64
 
65
65
  // Internal metadata stored on components but hidden from public types.
@@ -94,6 +94,10 @@ interface ComponentMeta {
94
94
 
95
95
  const META_KEY = "__meta";
96
96
 
97
+ interface ComponentWithMeta {
98
+ [META_KEY]?: ComponentMeta;
99
+ }
100
+
97
101
  const EMPTY_DEFAULTS: Record<string, unknown> = Object.freeze({}) as Record<
98
102
  string,
99
103
  unknown
@@ -101,6 +105,13 @@ const EMPTY_DEFAULTS: Record<string, unknown> = Object.freeze({}) as Record<
101
105
 
102
106
  const MAX_REFINE_RUNS = 50;
103
107
 
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
+
104
115
  function areVariantsEqual(
105
116
  a: Record<string, unknown>,
106
117
  b: Record<string, unknown>,
@@ -116,16 +127,94 @@ function areVariantsEqual(
116
127
  return true;
117
128
  }
118
129
 
119
- function warnRefineLimit(runState: RefineRunState): void {
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;
120
202
  if (runState.warned) return;
121
203
  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
- );
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(", ")}.`;
128
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);
129
218
  }
130
219
 
131
220
  function getExtUserVariantProps(
@@ -154,7 +243,9 @@ function mergeVariants(
154
243
  for (const key in source) {
155
244
  if (!Object.hasOwn(source, key)) continue;
156
245
  const value = source[key];
157
- if (!Object.is(target[key], value)) changed = true;
246
+ if (!Object.is(target[key], value)) {
247
+ changed = true;
248
+ }
158
249
  target[key] = value;
159
250
  }
160
251
  return changed;
@@ -163,21 +254,22 @@ function mergeVariants(
163
254
  if (!Object.hasOwn(source, key)) continue;
164
255
  if (skipKeys.has(key)) continue;
165
256
  const value = source[key];
166
- if (!Object.is(target[key], value)) changed = true;
257
+ if (!Object.is(target[key], value)) {
258
+ changed = true;
259
+ }
167
260
  target[key] = value;
168
261
  }
169
262
  return changed;
170
263
  }
171
264
 
172
- // Dynamic property access on function requires cast through unknown.
265
+ // Components carry internal metadata on a non-public property so user-facing
266
+ // component types stay clean.
173
267
  function getComponentMeta(component: AnyComponent): ComponentMeta | undefined {
174
- return (component as unknown as Record<string, unknown>)[META_KEY] as
175
- | ComponentMeta
176
- | undefined;
268
+ return (component as AnyComponent & ComponentWithMeta)[META_KEY];
177
269
  }
178
270
 
179
271
  function setComponentMeta(component: AnyComponent, meta: ComponentMeta): void {
180
- (component as unknown as Record<string, unknown>)[META_KEY] = meta;
272
+ (component as AnyComponent & ComponentWithMeta)[META_KEY] = meta;
181
273
  }
182
274
 
183
275
  export type {
@@ -321,9 +413,15 @@ function isVariantDisabled(
321
413
  }
322
414
 
323
415
  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);
416
+ if (typeof value === "string") {
417
+ return value;
418
+ }
419
+ if (typeof value === "number") {
420
+ return String(value);
421
+ }
422
+ if (typeof value === "boolean") {
423
+ return String(value);
424
+ }
327
425
  return undefined;
328
426
  }
329
427
 
@@ -343,7 +441,9 @@ function collectDisabledVariantKeys(
343
441
  config: CVConfig<Variants, AnyComponent[]>,
344
442
  ): Set<string> {
345
443
  const keys = new Set<string>();
346
- if (!config.variants) return keys;
444
+ if (!config.variants) {
445
+ return keys;
446
+ }
347
447
  for (const key in config.variants) {
348
448
  if (!Object.hasOwn(config.variants, key)) continue;
349
449
  if ((config.variants as Record<string, unknown>)[key] === null) {
@@ -357,7 +457,9 @@ function collectDisabledVariantValues(
357
457
  config: CVConfig<Variants, AnyComponent[]>,
358
458
  ): Record<string, Set<string>> {
359
459
  const values: Record<string, Set<string>> = {};
360
- if (!config.variants) return values;
460
+ if (!config.variants) {
461
+ return values;
462
+ }
361
463
  for (const key in config.variants) {
362
464
  if (!Object.hasOwn(config.variants, key)) continue;
363
465
  const variant = (config.variants as Record<string, unknown>)[key];
@@ -397,14 +499,22 @@ function normalizeKeySource(source: unknown): NormalizedSource {
397
499
  };
398
500
  }
399
501
 
400
- if (!source) return EMPTY_SOURCE;
502
+ if (!source) {
503
+ return EMPTY_SOURCE;
504
+ }
401
505
  if (typeof source !== "object" && typeof source !== "function") {
402
506
  return EMPTY_SOURCE;
403
507
  }
404
508
  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;
509
+ if (typeof typed.getVariants !== "function") {
510
+ return EMPTY_SOURCE;
511
+ }
512
+ if (!Array.isArray(typed.propKeys)) {
513
+ return EMPTY_SOURCE;
514
+ }
515
+ if (!Array.isArray(typed.variantKeys)) {
516
+ return EMPTY_SOURCE;
517
+ }
408
518
 
409
519
  return {
410
520
  propKeys: typed.propKeys as string[],
@@ -540,7 +650,9 @@ function buildPrebuiltVariant(variantDef: unknown): PrebuiltVariant {
540
650
  if (!Object.hasOwn(variantDef, key)) continue;
541
651
  const value = variantDef[key];
542
652
  if (value === null) {
543
- if (!disabledValues) disabledValues = new Set<string>();
653
+ if (!disabledValues) {
654
+ disabledValues = new Set<string>();
655
+ }
544
656
  disabledValues.add(key);
545
657
  continue;
546
658
  }
@@ -619,7 +731,9 @@ export function create({
619
731
  if (extend) {
620
732
  for (const ext of extend) {
621
733
  const meta = getComponentMeta(ext);
622
- if (meta) Object.assign(staticDefaults, meta.staticDefaults);
734
+ if (meta) {
735
+ Object.assign(staticDefaults, meta.staticDefaults);
736
+ }
623
737
  }
624
738
  }
625
739
  if (variants) {
@@ -700,6 +814,18 @@ export function create({
700
814
  const extMetasWithRefineCount = extMetasWithRefine.length;
701
815
  const shouldCollectChangedVariants = extMetasWithRefineCount > 0;
702
816
 
817
+ // Call-site frame captured at the `cv()` call site so refine-limit warnings
818
+ // can point developers at the component definition. Skipped entirely for
819
+ // components that can never enter the refine loop, and stripped in
820
+ // production via the NODE_ENV guard inside `captureCreationFrame`. The
821
+ // frame is captured at creation time but the underlying `.stack` string is
822
+ // formatted lazily on first access, so component creation stays cheap
823
+ // unless the warning actually fires.
824
+ const canTriggerRefineWarning = !!refine || extMetasWithRefineCount > 0;
825
+ const creationFrame = canTriggerRefineWarning
826
+ ? captureCreationFrame(cv)
827
+ : undefined;
828
+
703
829
  // Function variant keys inherited from extends, filtered through this
704
830
  // component's own variants: a static (object/shorthand) variant in this
705
831
  // component replaces an inherited function variant for the same key.
@@ -753,7 +879,9 @@ export function create({
753
879
  staticVariantsOverridingExtFn !== null
754
880
  ) {
755
881
  staticExtSkipKeys = new Set<string>();
756
- for (const k of disabledVariantKeys) staticExtSkipKeys.add(k);
882
+ for (const k of disabledVariantKeys) {
883
+ staticExtSkipKeys.add(k);
884
+ }
757
885
  for (let i = 0; i < functionVariantCount; i++) {
758
886
  staticExtSkipKeys.add(functionVariantNames[i]);
759
887
  }
@@ -777,7 +905,9 @@ export function create({
777
905
  ): void {
778
906
  if (!hasAnyDisabled) {
779
907
  for (const key in input) {
780
- if (Object.hasOwn(input, key)) out[key] = input[key];
908
+ if (Object.hasOwn(input, key)) {
909
+ out[key] = input[key];
910
+ }
781
911
  }
782
912
  return;
783
913
  }
@@ -901,7 +1031,9 @@ export function create({
901
1031
  defaults[k] = v;
902
1032
  }
903
1033
 
904
- if (!hasAnyDisabled) return defaults;
1034
+ if (!hasAnyDisabled) {
1035
+ return defaults;
1036
+ }
905
1037
 
906
1038
  // Filter disabled
907
1039
  const result: Record<string, unknown> = {};
@@ -938,7 +1070,9 @@ export function create({
938
1070
  const filteredVariants: Record<string, unknown> = {};
939
1071
  for (let i = 0; i < variantKeysLength; i++) {
940
1072
  const k = variantKeys[i];
941
- if (Object.hasOwn(resolved, k)) filteredVariants[k] = resolved[k];
1073
+ if (Object.hasOwn(resolved, k)) {
1074
+ filteredVariants[k] = resolved[k];
1075
+ }
942
1076
  }
943
1077
  ownVariants = filteredVariants;
944
1078
  }
@@ -949,7 +1083,9 @@ export function create({
949
1083
  const localCClasses: ClassValue[] | null = collectOutput ? [] : null;
950
1084
  let localCStyle: StyleValue | null = null;
951
1085
  const ensureUpdated = (): Record<string, unknown> => {
952
- if (updatedVariants) return updatedVariants;
1086
+ if (updatedVariants) {
1087
+ return updatedVariants;
1088
+ }
953
1089
  const u: Record<string, unknown> = {};
954
1090
  Object.assign(u, ownVariants);
955
1091
  updatedVariants = u;
@@ -961,7 +1097,9 @@ export function create({
961
1097
  protect = false,
962
1098
  ) => {
963
1099
  if (shouldCollectChangedVariants) {
964
- if (!changedVariants) changedVariants = {};
1100
+ if (!changedVariants) {
1101
+ changedVariants = {};
1102
+ }
965
1103
  changedVariants[key] = value;
966
1104
  }
967
1105
  if (protect && protectedVariants) {
@@ -1036,16 +1174,22 @@ export function create({
1036
1174
  },
1037
1175
  addStyle: (newStyle: StyleValue) => {
1038
1176
  if (!collectOutput) return;
1039
- if (!localCStyle) localCStyle = {};
1177
+ if (!localCStyle) {
1178
+ localCStyle = {};
1179
+ }
1040
1180
  Object.assign(localCStyle, newStyle);
1041
1181
  },
1042
1182
  };
1043
1183
  const result = refine(ctx);
1044
1184
  if (collectOutput && result != null) {
1045
1185
  const r = extractClassAndStylePrebuilt(result);
1046
- if (r.class != null) localCClasses?.push(r.class);
1186
+ if (r.class != null) {
1187
+ localCClasses?.push(r.class);
1188
+ }
1047
1189
  if (r.style) {
1048
- if (!localCStyle) localCStyle = {};
1190
+ if (!localCStyle) {
1191
+ localCStyle = {};
1192
+ }
1049
1193
  Object.assign(localCStyle, r.style);
1050
1194
  }
1051
1195
  }
@@ -1129,7 +1273,9 @@ export function create({
1129
1273
  extSkipKeys = skipKeys;
1130
1274
  } else {
1131
1275
  extSkipKeys = new Set(skipKeys);
1132
- for (const k of staticExtSkipKeys) extSkipKeys.add(k);
1276
+ for (const k of staticExtSkipKeys) {
1277
+ extSkipKeys.add(k);
1278
+ }
1133
1279
  }
1134
1280
 
1135
1281
  let extSkipVals: Record<string, Set<string>> | null;
@@ -1146,7 +1292,9 @@ export function create({
1146
1292
  const existing = extSkipVals[k];
1147
1293
  if (existing) {
1148
1294
  const merged = new Set<string>(existing);
1149
- for (const v of staticExtSkipValues[k]) merged.add(v);
1295
+ for (const v of staticExtSkipValues[k]) {
1296
+ merged.add(v);
1297
+ }
1150
1298
  extSkipVals[k] = merged;
1151
1299
  } else {
1152
1300
  extSkipVals[k] = staticExtSkipValues[k];
@@ -1211,7 +1359,9 @@ export function create({
1211
1359
  }
1212
1360
 
1213
1361
  // Apply own base style (after extends' styles, matching original order).
1214
- if (hasBaseStyle) Object.assign(styleOut, baseStyle);
1362
+ if (hasBaseStyle) {
1363
+ Object.assign(styleOut, baseStyle);
1364
+ }
1215
1365
 
1216
1366
  // Apply own variants. Skip keys/values come from caller (e.g., parent
1217
1367
  // wants its own function variant to override this variant).
@@ -1246,12 +1396,20 @@ export function create({
1246
1396
  if (selectedKey == null) continue;
1247
1397
  const v = variant.values[selectedKey];
1248
1398
  if (!v) continue;
1249
- if (v.class != null) classesOut.push(v.class as ClsxClassValue);
1250
- if (v.style) Object.assign(styleOut, v.style);
1399
+ if (v.class != null) {
1400
+ classesOut.push(v.class as ClsxClassValue);
1401
+ }
1402
+ if (v.style) {
1403
+ Object.assign(styleOut, v.style);
1404
+ }
1251
1405
  } else if (variant.shorthand && selectedValue === true) {
1252
1406
  const v = variant.shorthand;
1253
- if (v.class != null) classesOut.push(v.class as ClsxClassValue);
1254
- if (v.style) Object.assign(styleOut, v.style);
1407
+ if (v.class != null) {
1408
+ classesOut.push(v.class as ClsxClassValue);
1409
+ }
1410
+ if (v.style) {
1411
+ Object.assign(styleOut, v.style);
1412
+ }
1255
1413
  }
1256
1414
  }
1257
1415
 
@@ -1275,8 +1433,12 @@ export function create({
1275
1433
  const computedResult = fn(selectedValue);
1276
1434
  if (computedResult == null) continue;
1277
1435
  const r = extractClassAndStylePrebuilt(computedResult);
1278
- if (r.class != null) classesOut.push(r.class as ClsxClassValue);
1279
- if (r.style) Object.assign(styleOut, r.style);
1436
+ if (r.class != null) {
1437
+ classesOut.push(r.class as ClsxClassValue);
1438
+ }
1439
+ if (r.style) {
1440
+ Object.assign(styleOut, r.style);
1441
+ }
1280
1442
  }
1281
1443
 
1282
1444
  // Apply `refine` results — must come after own variants (static and
@@ -1286,38 +1448,16 @@ export function create({
1286
1448
  classesOut.push(cClasses[i] as ClsxClassValue);
1287
1449
  }
1288
1450
  }
1289
- if (cStyle) Object.assign(styleOut, cStyle);
1451
+ if (cStyle) {
1452
+ Object.assign(styleOut, cStyle);
1453
+ }
1290
1454
 
1291
1455
  return workingResolved;
1292
1456
  };
1293
1457
 
1294
1458
  const compute: ComputeFn =
1295
1459
  !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
- }
1460
+ ? computeOnce
1321
1461
  : (
1322
1462
  resolved,
1323
1463
  userVariantProps,
@@ -1330,10 +1470,15 @@ export function create({
1330
1470
  pendingProtectedVariants,
1331
1471
  protectedVariantKeys,
1332
1472
  ) => {
1333
- runState ??= { remaining: MAX_REFINE_RUNS, warned: false };
1473
+ runState ??= { remaining: MAX_REFINE_RUNS };
1334
1474
  protectedVariants ??= {};
1335
1475
  protectedVariantKeys ??= new Set<string>();
1336
1476
  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;
1337
1482
  let lastClasses: ClsxClassValue[] = [];
1338
1483
  let lastStyle: StyleValue = {};
1339
1484
  let isFirstRun = true;
@@ -1397,10 +1542,24 @@ export function create({
1397
1542
  return nextResolved;
1398
1543
  }
1399
1544
 
1545
+ if (
1546
+ process.env.NODE_ENV !== "production" &&
1547
+ runState.remaining < REFINE_UNSTABLE_TRACKING_WINDOW
1548
+ ) {
1549
+ if (!unstableKeys) {
1550
+ unstableKeys = new Set<string>();
1551
+ }
1552
+ accumulateUnstableVariantKeys(
1553
+ unstableKeys,
1554
+ workingResolved,
1555
+ nextResolved,
1556
+ );
1557
+ }
1558
+
1400
1559
  if (useDirectOutput && runState.remaining === 0) {
1401
1560
  // Keep the direct output from the last allowed run. Rolling
1402
1561
  // back here would drop it before the fallback copy below.
1403
- warnRefineLimit(runState);
1562
+ warnRefineLimit(runState, creationFrame, unstableKeys);
1404
1563
  return nextResolved;
1405
1564
  }
1406
1565
 
@@ -1420,7 +1579,7 @@ export function create({
1420
1579
  isFirstRun = false;
1421
1580
  }
1422
1581
 
1423
- warnRefineLimit(runState);
1582
+ warnRefineLimit(runState, creationFrame, unstableKeys);
1424
1583
 
1425
1584
  for (let i = 0; i < lastClasses.length; i++) {
1426
1585
  classesOut.push(lastClasses[i]);
@@ -1493,10 +1652,15 @@ export function create({
1493
1652
  pendingProtectedVariants,
1494
1653
  protectedVariantKeys,
1495
1654
  ) => {
1496
- runState ??= { remaining: MAX_REFINE_RUNS, warned: false };
1655
+ runState ??= { remaining: MAX_REFINE_RUNS };
1497
1656
  protectedVariants ??= {};
1498
1657
  protectedVariantKeys ??= new Set<string>();
1499
1658
  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;
1500
1664
  let reachedLimit = true;
1501
1665
 
1502
1666
  while (runState.remaining > 0) {
@@ -1536,11 +1700,24 @@ export function create({
1536
1700
  break;
1537
1701
  }
1538
1702
 
1703
+ if (
1704
+ process.env.NODE_ENV !== "production" &&
1705
+ runState.remaining < REFINE_UNSTABLE_TRACKING_WINDOW
1706
+ ) {
1707
+ if (!unstableKeys) {
1708
+ unstableKeys = new Set<string>();
1709
+ }
1710
+ accumulateUnstableVariantKeys(
1711
+ unstableKeys,
1712
+ workingResolved,
1713
+ nextResolved,
1714
+ );
1715
+ }
1539
1716
  workingResolved = nextResolved;
1540
1717
  }
1541
1718
 
1542
1719
  if (reachedLimit) {
1543
- warnRefineLimit(runState);
1720
+ warnRefineLimit(runState, creationFrame, unstableKeys);
1544
1721
  }
1545
1722
 
1546
1723
  return workingResolved;
package/src/types.ts CHANGED
@@ -76,11 +76,11 @@ type ComponentPropKey<R extends ComponentResult> =
76
76
 
77
77
  // Key source types - what can be passed as additional parameters to splitProps
78
78
  export type KeySourceArray = readonly string[];
79
- export type KeySourceComponent = {
79
+ export interface KeySourceComponent {
80
80
  propKeys: readonly string[];
81
81
  variantKeys: readonly string[];
82
82
  getVariants: () => Record<string, unknown>;
83
- };
83
+ }
84
84
  export type KeySource = KeySourceArray | KeySourceComponent;
85
85
 
86
86
  // Check if source is a component (has getVariants)
package/src/utils.ts CHANGED
@@ -39,7 +39,9 @@ export function hyphenToCamel(str: string) {
39
39
  }
40
40
  // Fast path: no hyphen -> return as-is
41
41
  let hyphenIndex = str.indexOf("-");
42
- if (hyphenIndex === -1) return str;
42
+ if (hyphenIndex === -1) {
43
+ return str;
44
+ }
43
45
 
44
46
  let result = "";
45
47
  let lastIndex = 0;
@@ -91,7 +93,9 @@ export function camelToHyphen(str: string) {
91
93
  lastIndex = i + 1;
92
94
  }
93
95
 
94
- if (lastIndex === 0) return str;
96
+ if (lastIndex === 0) {
97
+ return str;
98
+ }
95
99
  return result + str.slice(lastIndex);
96
100
  }
97
101
 
@@ -102,7 +106,9 @@ export function camelToHyphen(str: string) {
102
106
  * parseLengthValue("2em"); // "2em"
103
107
  */
104
108
  export function parseLengthValue(value: string | number) {
105
- if (typeof value === "string") return value;
109
+ if (typeof value === "string") {
110
+ return value;
111
+ }
106
112
  return `${value}px`;
107
113
  }
108
114
 
@@ -135,7 +141,10 @@ export function htmlStyleToStyleValue(styleString: string) {
135
141
  }
136
142
  if (i >= len || styleString.charCodeAt(i) === 59) {
137
143
  // No colon found - skip this declaration
138
- if (i < len) i++; // skip ';'
144
+ if (i < len) {
145
+ // Skip ';'.
146
+ i++;
147
+ }
139
148
  continue;
140
149
  }
141
150
  let propEnd = i;
@@ -147,12 +156,17 @@ export function htmlStyleToStyleValue(styleString: string) {
147
156
  }
148
157
  if (propEnd === propStart) {
149
158
  // Empty property - skip
150
- while (i < len && styleString.charCodeAt(i) !== 59) i++;
151
- if (i < len) i++;
159
+ while (i < len && styleString.charCodeAt(i) !== 59) {
160
+ i++;
161
+ }
162
+ if (i < len) {
163
+ i++;
164
+ }
152
165
  continue;
153
166
  }
154
167
  const property = styleString.slice(propStart, propEnd);
155
- i++; // skip ':'
168
+ // Skip ':'.
169
+ i++;
156
170
  // Skip whitespace before value
157
171
  while (i < len) {
158
172
  const c = styleString.charCodeAt(i);
@@ -160,14 +174,19 @@ export function htmlStyleToStyleValue(styleString: string) {
160
174
  i++;
161
175
  }
162
176
  const valStart = i;
163
- while (i < len && styleString.charCodeAt(i) !== 59) i++;
177
+ while (i < len && styleString.charCodeAt(i) !== 59) {
178
+ i++;
179
+ }
164
180
  let valEnd = i;
165
181
  while (valEnd > valStart) {
166
182
  const c = styleString.charCodeAt(valEnd - 1);
167
183
  if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
168
184
  valEnd--;
169
185
  }
170
- if (i < len) i++; // skip ';'
186
+ if (i < len) {
187
+ // Skip ';'.
188
+ i++;
189
+ }
171
190
  if (valEnd === valStart) continue;
172
191
  const value = styleString.slice(valStart, valEnd);
173
192
  // CSS property names and values are dynamic - cast required for index access
@@ -229,7 +248,9 @@ export function styleValueToHTMLStyle(style: StyleValue): string {
229
248
  if (!hasOwn.call(style, key)) continue;
230
249
  const value = (style as Record<string, unknown>)[key];
231
250
  if (value == null) continue;
232
- if (result) result += "; ";
251
+ if (result) {
252
+ result += "; ";
253
+ }
233
254
  result += camelToHyphen(key);
234
255
  result += ": ";
235
256
  result += value as string | number;