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/CHANGELOG.md +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +120 -22
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/index.ts +179 -87
- package/src/refine-warning.ts +161 -0
- package/src/types.ts +2 -2
- package/src/utils.ts +31 -10
- package/tests/_utils.ts +6 -2
- package/tests/build.test.ts +82 -7
- package/tests/extend.test.ts +7 -2
- package/tests/refine-warning.test.ts +28 -0
- package/tests/refine.test.ts +326 -0
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))
|
|
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))
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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")
|
|
325
|
-
|
|
326
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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")
|
|
406
|
-
|
|
407
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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))
|
|
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)
|
|
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))
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
1092
|
+
if (r.class != null) {
|
|
1093
|
+
localCClasses?.push(r.class);
|
|
1094
|
+
}
|
|
1047
1095
|
if (r.style) {
|
|
1048
|
-
if (!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)
|
|
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])
|
|
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)
|
|
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)
|
|
1250
|
-
|
|
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)
|
|
1254
|
-
|
|
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)
|
|
1279
|
-
|
|
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)
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
+
}
|