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/CHANGELOG.md +20 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +57 -16
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/index.ts +252 -75
- package/src/types.ts +2 -2
- package/src/utils.ts +31 -10
- package/tests/_utils.ts +6 -2
- package/tests/build.test.ts +81 -7
- package/tests/extend.test.ts +7 -2
- package/tests/refine.test.ts +149 -0
package/src/index.ts
CHANGED
|
@@ -59,7 +59,7 @@ type ResolveRefineFn = (
|
|
|
59
59
|
|
|
60
60
|
interface RefineRunState {
|
|
61
61
|
remaining: number;
|
|
62
|
-
warned
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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))
|
|
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))
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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")
|
|
325
|
-
|
|
326
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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")
|
|
406
|
-
|
|
407
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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))
|
|
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)
|
|
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))
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
1186
|
+
if (r.class != null) {
|
|
1187
|
+
localCClasses?.push(r.class);
|
|
1188
|
+
}
|
|
1047
1189
|
if (r.style) {
|
|
1048
|
-
if (!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)
|
|
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])
|
|
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)
|
|
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)
|
|
1250
|
-
|
|
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)
|
|
1254
|
-
|
|
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)
|
|
1279
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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")
|
|
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)
|
|
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)
|
|
151
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
251
|
+
if (result) {
|
|
252
|
+
result += "; ";
|
|
253
|
+
}
|
|
233
254
|
result += camelToHyphen(key);
|
|
234
255
|
result += ": ";
|
|
235
256
|
result += value as string | number;
|