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