react-state-basis 0.6.5 → 0.6.7
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/README.md +2 -2
- package/dist/{chunk-EJXGN76H.mjs → chunk-KIXM6YRX.mjs} +307 -212
- package/dist/chunk-KIXM6YRX.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +306 -211
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/integrations/zustand.js +301 -210
- package/dist/integrations/zustand.js.map +1 -1
- package/dist/integrations/zustand.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-EJXGN76H.mjs.map +0 -1
|
@@ -24,33 +24,92 @@ __export(zustand_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(zustand_exports);
|
|
26
26
|
|
|
27
|
+
// src/core/constants.ts
|
|
28
|
+
var WINDOW_SIZE = 50;
|
|
29
|
+
var PAIR_RARITY_TARGET = 0.01;
|
|
30
|
+
var RELATIVE_OVERLAP_FLOOR = 0.65;
|
|
31
|
+
var LOOP_THRESHOLD = 150;
|
|
32
|
+
var VOLATILITY_THRESHOLD = 25;
|
|
33
|
+
var INSTANCE_SEP = "##";
|
|
34
|
+
|
|
27
35
|
// src/core/math.ts
|
|
28
|
-
var
|
|
36
|
+
var choose = (n, k) => {
|
|
37
|
+
if (k < 0 || k > n) return 0;
|
|
38
|
+
k = Math.min(k, n - k);
|
|
39
|
+
let result = 1;
|
|
40
|
+
for (let i = 0; i < k; i++) {
|
|
41
|
+
result = result * (n - i) / (i + 1);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
var hypergeomPMF = (k, n1, m1, N) => {
|
|
46
|
+
const total = choose(N, n1);
|
|
47
|
+
if (total === 0) return 0;
|
|
48
|
+
return choose(m1, k) * choose(N - m1, n1 - k) / total;
|
|
49
|
+
};
|
|
50
|
+
var hypergeomUpperTailP = (kStart, n1, m1, N) => {
|
|
51
|
+
const kMax = Math.min(n1, m1);
|
|
52
|
+
let p = 0;
|
|
53
|
+
for (let k = Math.max(0, kStart); k <= kMax; k++) {
|
|
54
|
+
p += hypergeomPMF(k, n1, m1, N);
|
|
55
|
+
}
|
|
56
|
+
return p;
|
|
57
|
+
};
|
|
58
|
+
var minOverlapForRarity = (n1, m1, N, targetP) => {
|
|
59
|
+
const kMax = Math.min(n1, m1);
|
|
60
|
+
for (let k = 0; k <= kMax; k++) {
|
|
61
|
+
if (hypergeomUpperTailP(k, n1, m1, N) <= targetP) return k;
|
|
62
|
+
}
|
|
63
|
+
return kMax + 1;
|
|
64
|
+
};
|
|
65
|
+
var overlapThresholdCache = /* @__PURE__ */ new Map();
|
|
66
|
+
var getMinOverlap = (densityA, densityB, windowSize) => {
|
|
67
|
+
const lo = Math.min(densityA, densityB) | 0;
|
|
68
|
+
const hi = Math.max(densityA, densityB) | 0;
|
|
69
|
+
const key = `${lo}_${hi}_${windowSize}`;
|
|
70
|
+
let cached = overlapThresholdCache.get(key);
|
|
71
|
+
if (cached === void 0) {
|
|
72
|
+
const rare = minOverlapForRarity(lo, hi, windowSize, PAIR_RARITY_TARGET);
|
|
73
|
+
const floor = Math.ceil(RELATIVE_OVERLAP_FLOOR * lo);
|
|
74
|
+
cached = Math.max(rare, floor);
|
|
75
|
+
overlapThresholdCache.set(key, cached);
|
|
76
|
+
}
|
|
77
|
+
return cached;
|
|
78
|
+
};
|
|
79
|
+
var isSignificantOverlap = (overlap, densityA, densityB, windowSize) => {
|
|
80
|
+
if (densityA < 2 || densityB < 2) return false;
|
|
81
|
+
return overlap >= getMinOverlap(densityA, densityB, windowSize);
|
|
82
|
+
};
|
|
83
|
+
var countOverlapsCircular = (bufferA, headA, bufferB, headB) => {
|
|
29
84
|
const L = bufferA.length;
|
|
30
|
-
|
|
31
|
-
const
|
|
85
|
+
const offSync = ((headB - headA) % L + L) % L;
|
|
86
|
+
const offALeadsB = ((headB - headA + 1) % L + L) % L;
|
|
87
|
+
const offBLeadsA = ((headB - headA - 1) % L + L) % L;
|
|
88
|
+
let kSync = 0;
|
|
89
|
+
let kALeadsB = 0;
|
|
90
|
+
let kBLeadsA = 0;
|
|
91
|
+
let densityA = 0;
|
|
92
|
+
let densityB = 0;
|
|
32
93
|
for (let i = 0; i < L; i++) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
94
|
+
const a = bufferA[i] ? 1 : 0;
|
|
95
|
+
const b = bufferB[i] ? 1 : 0;
|
|
96
|
+
densityA += a;
|
|
97
|
+
densityB += b;
|
|
98
|
+
let iSync = i + offSync;
|
|
99
|
+
if (iSync >= L) iSync -= L;
|
|
100
|
+
let iALeadsB = i + offALeadsB;
|
|
101
|
+
if (iALeadsB >= L) iALeadsB -= L;
|
|
102
|
+
let iBLeadsA = i + offBLeadsA;
|
|
103
|
+
if (iBLeadsA >= L) iBLeadsA -= L;
|
|
104
|
+
if (a && bufferB[iSync]) kSync++;
|
|
105
|
+
if (a && bufferB[iALeadsB]) kALeadsB++;
|
|
106
|
+
if (a && bufferB[iBLeadsA]) kBLeadsA++;
|
|
42
107
|
}
|
|
43
|
-
|
|
44
|
-
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
108
|
+
return { kSync, kALeadsB, kBLeadsA, densityA, densityB };
|
|
45
109
|
};
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
dot += A[i] * B[i];
|
|
50
|
-
magA += A[i] * A[i];
|
|
51
|
-
magB += B[i] * B[i];
|
|
52
|
-
}
|
|
53
|
-
return magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
110
|
+
var cosineFromOverlap = (overlap, densityA, densityB) => {
|
|
111
|
+
if (densityA <= 0 || densityB <= 0) return 0;
|
|
112
|
+
return overlap / Math.sqrt(densityA * densityB);
|
|
54
113
|
};
|
|
55
114
|
|
|
56
115
|
// src/core/graph.ts
|
|
@@ -109,13 +168,6 @@ var groupEventSources = (nodes, edges) => {
|
|
|
109
168
|
return groups.sort((a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences);
|
|
110
169
|
};
|
|
111
170
|
|
|
112
|
-
// src/core/constants.ts
|
|
113
|
-
var WINDOW_SIZE = 50;
|
|
114
|
-
var SIMILARITY_THRESHOLD = 0.88;
|
|
115
|
-
var LOOP_THRESHOLD = 150;
|
|
116
|
-
var VOLATILITY_THRESHOLD = 25;
|
|
117
|
-
var INSTANCE_SEP = "##";
|
|
118
|
-
|
|
119
171
|
// src/core/label.ts
|
|
120
172
|
var stripInstance = (label) => {
|
|
121
173
|
const idx = label.indexOf(INSTANCE_SEP);
|
|
@@ -229,32 +281,22 @@ var LAST_LOG_TIMES = /* @__PURE__ */ new Map();
|
|
|
229
281
|
var LOG_COOLDOWN = 3e3;
|
|
230
282
|
var THEME = {
|
|
231
283
|
identity: "#6C5CE7",
|
|
232
|
-
// Purple (Brand)
|
|
233
284
|
problem: "#D63031",
|
|
234
|
-
// Red (Bugs)
|
|
235
285
|
solution: "#FBC531",
|
|
236
|
-
// Yellow (Fixes)
|
|
237
286
|
context: "#0984E3",
|
|
238
|
-
// Blue (Locations)
|
|
239
287
|
muted: "#9AA0A6",
|
|
240
|
-
// Gray (Metadata)
|
|
241
288
|
border: "#2E2E35",
|
|
242
289
|
success: "#00b894"
|
|
243
|
-
// Green (Good Score)
|
|
244
290
|
};
|
|
245
291
|
var STYLES = {
|
|
246
|
-
// Structure
|
|
247
292
|
basis: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 2px 6px; border-radius: 3px;`,
|
|
248
293
|
headerIdentity: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
|
|
249
294
|
headerProblem: `background: ${THEME.problem}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
|
|
250
295
|
version: `background: #a29bfe; color: #2d3436; padding: 2px 6px; border-radius: 3px; margin-left: -4px;`,
|
|
251
|
-
// Actions
|
|
252
296
|
actionLabel: `color: ${THEME.solution}; font-weight: bold;`,
|
|
253
297
|
actionPill: `color: ${THEME.solution}; font-weight: bold; border: 1px solid ${THEME.solution}; padding: 0 4px; border-radius: 3px;`,
|
|
254
|
-
// Context
|
|
255
298
|
impactLabel: `color: ${THEME.context}; font-weight: bold;`,
|
|
256
299
|
location: `color: ${THEME.context}; font-family: monospace; font-weight: bold;`,
|
|
257
|
-
// Text
|
|
258
300
|
subText: `color: ${THEME.muted}; font-size: 11px;`,
|
|
259
301
|
bold: "font-weight: bold;",
|
|
260
302
|
label: "background: #dfe6e9; color: #2d3436; padding: 0 4px; border-radius: 3px; font-family: monospace; font-weight: bold; border: 1px solid #b2bec3;"
|
|
@@ -269,97 +311,95 @@ var shouldLog = (key) => {
|
|
|
269
311
|
return false;
|
|
270
312
|
};
|
|
271
313
|
var isBooleanLike = (name) => /^(is|has|can|should|did|will|show|hide)(?=[A-Z_])/.test(name);
|
|
314
|
+
var displayName = (raw) => {
|
|
315
|
+
const { name } = parseLabel(raw);
|
|
316
|
+
return name.replace(/:\d+$/, "");
|
|
317
|
+
};
|
|
318
|
+
var areSyncSignificant = (metaA, metaB) => {
|
|
319
|
+
const { kSync, densityA, densityB } = countOverlapsCircular(
|
|
320
|
+
metaA.buffer,
|
|
321
|
+
metaA.head,
|
|
322
|
+
metaB.buffer,
|
|
323
|
+
metaB.head
|
|
324
|
+
);
|
|
325
|
+
return isSignificantOverlap(kSync, densityA, densityB, metaA.buffer.length);
|
|
326
|
+
};
|
|
272
327
|
var getSuggestedFix = (issue, info) => {
|
|
273
328
|
if (issue.label.includes("Global Event")) {
|
|
274
|
-
return `
|
|
329
|
+
return `One interaction is updating state in several places. If that is really one transition, put it in one %cstore / reducer%c. If it is intentional, ignore this.`;
|
|
275
330
|
}
|
|
276
331
|
const violations = issue.violations || [];
|
|
277
332
|
const leaks = violations.filter((v) => v.type === "causal_leak");
|
|
278
333
|
const mirrors = violations.filter((v) => v.type === "context_mirror");
|
|
279
334
|
const duplicates = violations.filter((v) => v.type === "duplicate_state");
|
|
280
335
|
if (mirrors.length > 0) {
|
|
281
|
-
return `
|
|
336
|
+
return `A local hook is only tracking context or a store. If it is not a draft, delete it and read the %ccontext / store%c in render.`;
|
|
282
337
|
}
|
|
283
338
|
if (leaks.length > 0) {
|
|
284
|
-
const targetName =
|
|
339
|
+
const targetName = displayName(leaks[0].target);
|
|
285
340
|
if (issue.label.includes("effect")) {
|
|
286
|
-
return `
|
|
341
|
+
return `An effect is calling setState on ${targetName}, which paints again. If you can compute ${targetName} while rendering, drop the %ceffect%c.`;
|
|
287
342
|
}
|
|
288
|
-
return
|
|
343
|
+
return `${info.name} updates, then ${targetName} updates on the next frame. If they are one fact, write them in the same %csetState%c.`;
|
|
289
344
|
}
|
|
290
345
|
if (duplicates.length > 0) {
|
|
291
346
|
if (isBooleanLike(info.name)) {
|
|
292
|
-
return `
|
|
347
|
+
return `Several flags move together. One %cstatus%c value avoids impossible combinations.`;
|
|
293
348
|
}
|
|
294
|
-
return `
|
|
349
|
+
return `These hooks move together. If one is just the other in another shape, compute it while %crendering%c.`;
|
|
295
350
|
}
|
|
296
351
|
if (issue.metric === "density") {
|
|
297
|
-
return `
|
|
352
|
+
return `This hook updates faster than a frame. %cDebounce%c it or keep it in a ref if the UI does not need every pulse.`;
|
|
298
353
|
}
|
|
299
|
-
return `
|
|
354
|
+
return `Inspect ${info.name} and what updates with it.`;
|
|
300
355
|
};
|
|
301
|
-
var displayHealthReport = (history2,
|
|
356
|
+
var displayHealthReport = (history2, violationMap) => {
|
|
302
357
|
if (!isWeb) return;
|
|
303
358
|
const entries = Array.from(history2.entries());
|
|
304
359
|
if (entries.length === 0) return;
|
|
305
360
|
const topIssues = identifyTopIssues(instance.graph, history2, instance.redundantLabels, violationMap);
|
|
306
|
-
console.group(`%c
|
|
361
|
+
console.group(`%c BASIS | report `, STYLES.headerIdentity);
|
|
307
362
|
if (topIssues.length > 0) {
|
|
308
|
-
console.log(
|
|
309
|
-
`%c\u{1F3AF} REFACTOR PRIORITIES %c(PRIME MOVERS)`,
|
|
310
|
-
`font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`,
|
|
311
|
-
`font-weight: normal; color: ${THEME.muted}; font-style: italic;`
|
|
312
|
-
);
|
|
363
|
+
console.log(`%cStart here`, `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`);
|
|
313
364
|
topIssues.forEach((issue, idx) => {
|
|
314
365
|
const info = parseLabel(issue.label);
|
|
315
|
-
const icon = issue.metric === "influence" ? "\
|
|
366
|
+
const icon = issue.metric === "influence" ? "\u2192" : "\u2022";
|
|
316
367
|
const pColor = idx === 0 ? THEME.problem : idx === 1 ? THEME.solution : THEME.identity;
|
|
317
|
-
let displayName = info.name;
|
|
318
|
-
let displayFile = info.file;
|
|
319
|
-
if (issue.label.includes("Global Event")) {
|
|
320
|
-
displayName = info.name;
|
|
321
|
-
displayFile = info.file;
|
|
322
|
-
}
|
|
323
368
|
console.group(
|
|
324
|
-
` %c${idx + 1}%c ${icon} ${displayName} %c(${
|
|
369
|
+
` %c${idx + 1}%c ${icon} ${displayName(issue.label)} %c(${info.file})`,
|
|
325
370
|
`background: ${pColor}; color: ${idx === 1 ? "black" : "white"}; border-radius: 50%; padding: 0 5px;`,
|
|
326
371
|
"font-family: monospace; font-weight: 700;",
|
|
327
|
-
`color: ${THEME.muted}; font-size: 10px; font-weight: normal
|
|
372
|
+
`color: ${THEME.muted}; font-size: 10px; font-weight: normal;`
|
|
328
373
|
);
|
|
329
|
-
console.log(`%c${issue.reason}`, `color: ${THEME.muted}
|
|
374
|
+
console.log(`%c${issue.reason}`, `color: ${THEME.muted};`);
|
|
330
375
|
if (issue.violations.length > 0) {
|
|
331
376
|
const byFile = /* @__PURE__ */ new Map();
|
|
332
377
|
issue.violations.forEach((v) => {
|
|
333
378
|
if (issue.label.includes("Global Event") && v.type === "context_mirror") return;
|
|
334
379
|
const { file, name } = parseLabel(v.target);
|
|
335
380
|
if (!byFile.has(file)) byFile.set(file, []);
|
|
336
|
-
byFile.get(file).push(name);
|
|
381
|
+
byFile.get(file).push(name.replace(/:\d+$/, ""));
|
|
337
382
|
});
|
|
338
383
|
const impactParts = [];
|
|
339
384
|
byFile.forEach((vars, file) => {
|
|
340
|
-
|
|
341
|
-
impactParts.push(`${file} (${varList})`);
|
|
385
|
+
impactParts.push(`${file} (${vars.join(", ")})`);
|
|
342
386
|
});
|
|
343
387
|
if (impactParts.length > 0) {
|
|
344
|
-
console.log(`%
|
|
388
|
+
console.log(`%cAlso updates: %c${impactParts.join(" \xB7 ")}`, STYLES.impactLabel, "");
|
|
345
389
|
}
|
|
346
390
|
}
|
|
347
391
|
const fix = getSuggestedFix(issue, info);
|
|
348
392
|
const fixParts = fix.split("%c");
|
|
349
393
|
if (fixParts.length === 3) {
|
|
350
394
|
console.log(
|
|
351
|
-
`%
|
|
395
|
+
`%cTry: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
|
|
352
396
|
STYLES.actionLabel,
|
|
353
397
|
"",
|
|
354
398
|
STYLES.actionPill,
|
|
355
399
|
""
|
|
356
400
|
);
|
|
357
401
|
} else {
|
|
358
|
-
console.log(
|
|
359
|
-
`%cSolution: %c${fix}`,
|
|
360
|
-
STYLES.actionLabel,
|
|
361
|
-
""
|
|
362
|
-
);
|
|
402
|
+
console.log(`%cTry: %c${fix}`, STYLES.actionLabel, "");
|
|
363
403
|
}
|
|
364
404
|
console.groupEnd();
|
|
365
405
|
});
|
|
@@ -374,145 +414,179 @@ var displayHealthReport = (history2, threshold, violationMap) => {
|
|
|
374
414
|
processed.add(labelA);
|
|
375
415
|
entries.forEach(([labelB, metaB]) => {
|
|
376
416
|
if (labelA === labelB || processed.has(labelB)) return;
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
417
|
+
if (!areSyncSignificant(metaA, metaB)) return;
|
|
418
|
+
if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
|
|
419
|
+
currentCluster.push(labelB);
|
|
420
|
+
processed.add(labelB);
|
|
382
421
|
});
|
|
383
422
|
if (currentCluster.length > 1) clusters.push(currentCluster);
|
|
384
423
|
else independentCount++;
|
|
385
424
|
});
|
|
386
425
|
const totalVars = entries.length;
|
|
387
|
-
const redundancyScore = (independentCount + clusters.length) / totalVars * 100;
|
|
388
|
-
let internalEdges = 0;
|
|
389
|
-
instance.graph.forEach((targets, source) => {
|
|
390
|
-
if (source.startsWith("Event_Tick_")) return;
|
|
391
|
-
internalEdges += targets.size;
|
|
392
|
-
});
|
|
393
|
-
const causalPenalty = internalEdges / totalVars * 100;
|
|
394
|
-
let healthScore = redundancyScore - causalPenalty;
|
|
395
|
-
if (healthScore < 0) healthScore = 0;
|
|
396
|
-
const scoreColor = healthScore > 85 ? THEME.success : THEME.problem;
|
|
397
426
|
console.log(
|
|
398
|
-
`%
|
|
399
|
-
STYLES.
|
|
400
|
-
`color: ${scoreColor}; font-weight: bold;`
|
|
427
|
+
`%c${independentCount + clusters.length} of ${totalVars} instrumented hooks look independent in this window.`,
|
|
428
|
+
STYLES.subText
|
|
401
429
|
);
|
|
402
|
-
console.log(`%cSources of Truth: ${independentCount + clusters.length}/${totalVars} | Causal Leaks: ${internalEdges}`, STYLES.subText);
|
|
403
430
|
if (clusters.length > 0) {
|
|
404
|
-
console.log(
|
|
431
|
+
console.log(
|
|
432
|
+
`%c${clusters.length} group${clusters.length === 1 ? "" : "s"} that keep updating together:`,
|
|
433
|
+
`font-weight: bold; color: ${THEME.problem}; margin-top: 10px;`
|
|
434
|
+
);
|
|
405
435
|
clusters.forEach((cluster, idx) => {
|
|
406
436
|
const clusterMetas = cluster.map((l) => ({
|
|
407
437
|
label: l,
|
|
408
438
|
meta: history2.get(l),
|
|
409
|
-
name:
|
|
439
|
+
name: displayName(l)
|
|
410
440
|
}));
|
|
411
441
|
const hasCtx = clusterMetas.some(
|
|
412
442
|
(c) => c.meta.role === "context" /* CONTEXT */ || c.meta.role === "store" /* STORE */
|
|
413
443
|
);
|
|
414
|
-
const names = clusterMetas.map((c) =>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
444
|
+
const names = clusterMetas.map((c) => c.name).join(", ");
|
|
445
|
+
console.group(
|
|
446
|
+
` %c${idx + 1}%c ${names}`,
|
|
447
|
+
`background: ${THEME.problem}; color: white; border-radius: 50%; padding: 0 5px;`,
|
|
448
|
+
"font-family: monospace; font-weight: bold;"
|
|
449
|
+
);
|
|
419
450
|
if (hasCtx) {
|
|
420
451
|
const hasStore = clusterMetas.some((c) => c.meta.role === "store" /* STORE */);
|
|
421
|
-
const sourceType = hasStore ? "
|
|
422
|
-
console.log(
|
|
423
|
-
console.log(
|
|
452
|
+
const sourceType = hasStore ? "a store" : "context";
|
|
453
|
+
console.log(`A local hook is only following ${sourceType}.`);
|
|
454
|
+
console.log(
|
|
455
|
+
`%cTry:%c Read ${sourceType} in render if the local value is not a draft.`,
|
|
456
|
+
STYLES.actionLabel,
|
|
457
|
+
""
|
|
458
|
+
);
|
|
424
459
|
} else {
|
|
425
|
-
const boolKeywords = [
|
|
460
|
+
const boolKeywords = [
|
|
461
|
+
"is",
|
|
462
|
+
"has",
|
|
463
|
+
"can",
|
|
464
|
+
"should",
|
|
465
|
+
"loading",
|
|
466
|
+
"success",
|
|
467
|
+
"error",
|
|
468
|
+
"active",
|
|
469
|
+
"enabled",
|
|
470
|
+
"open",
|
|
471
|
+
"visible"
|
|
472
|
+
];
|
|
426
473
|
const boolCount = clusterMetas.filter(
|
|
427
474
|
(c) => boolKeywords.some((kw) => c.name.toLowerCase().startsWith(kw))
|
|
428
475
|
).length;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
console.log(
|
|
432
|
-
|
|
476
|
+
if (cluster.length > 2 && boolCount / cluster.length > 0.5) {
|
|
477
|
+
console.log(`These flags move together.`);
|
|
478
|
+
console.log(
|
|
479
|
+
`%cTry:%c One %cstatus%c instead of several booleans.`,
|
|
480
|
+
STYLES.actionLabel,
|
|
481
|
+
"",
|
|
482
|
+
STYLES.actionPill,
|
|
483
|
+
""
|
|
484
|
+
);
|
|
433
485
|
} else if (cluster.length > 2) {
|
|
434
|
-
console.log(
|
|
435
|
-
console.log(
|
|
486
|
+
console.log(`These hooks move on the same frames. Often the same click or fetch.`);
|
|
487
|
+
console.log(
|
|
488
|
+
`%cTry:%c Leave it if that is intentional. Otherwise one %creducer%c.`,
|
|
489
|
+
STYLES.actionLabel,
|
|
490
|
+
"",
|
|
491
|
+
STYLES.actionPill,
|
|
492
|
+
""
|
|
493
|
+
);
|
|
436
494
|
} else {
|
|
437
|
-
console.log(
|
|
438
|
-
console.log(
|
|
495
|
+
console.log(`These two hooks keep updating in the same frame.`);
|
|
496
|
+
console.log(
|
|
497
|
+
`%cTry:%c If one is derived, compute it while %crendering%c.`,
|
|
498
|
+
STYLES.actionLabel,
|
|
499
|
+
"",
|
|
500
|
+
STYLES.actionPill,
|
|
501
|
+
""
|
|
502
|
+
);
|
|
439
503
|
}
|
|
440
504
|
}
|
|
441
505
|
console.groupEnd();
|
|
442
506
|
});
|
|
443
507
|
} else {
|
|
444
|
-
console.log(
|
|
508
|
+
console.log(
|
|
509
|
+
"%cNo hooks were updating in lockstep in this window.",
|
|
510
|
+
`color: ${THEME.success}; font-weight: bold;`
|
|
511
|
+
);
|
|
445
512
|
}
|
|
446
513
|
console.groupEnd();
|
|
447
514
|
};
|
|
448
|
-
var displayRedundancyAlert = (labelA, metaA, labelB, metaB,
|
|
515
|
+
var displayRedundancyAlert = (labelA, metaA, labelB, metaB, overlap) => {
|
|
449
516
|
if (!isWeb || !shouldLog(`redundant-${labelA}-${labelB}`)) return;
|
|
450
517
|
const infoA = parseLabel(labelA);
|
|
451
|
-
const
|
|
518
|
+
const nameA = displayName(labelA);
|
|
519
|
+
const nameB = displayName(labelB);
|
|
452
520
|
const isContextMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "context" /* CONTEXT */ || metaB.role === "local" /* LOCAL */ && metaA.role === "context" /* CONTEXT */;
|
|
453
521
|
const isStoreMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "store" /* STORE */ || metaB.role === "local" /* LOCAL */ && metaA.role === "store" /* STORE */;
|
|
454
|
-
const alertType = isContextMirror ? "
|
|
455
|
-
|
|
456
|
-
console.
|
|
457
|
-
console.log(`%
|
|
522
|
+
const alertType = isContextMirror ? "local state follows context" : isStoreMirror ? "local state follows a store" : "hooks moving together";
|
|
523
|
+
const times = overlap.kSync === 1 ? "time" : "times";
|
|
524
|
+
console.group(`%c BASIS | ${alertType} `, STYLES.headerProblem);
|
|
525
|
+
console.log(`%c${infoA.file}`, STYLES.location);
|
|
526
|
+
console.log(
|
|
527
|
+
`%c${nameA}%c and %c${nameB}%c updated in the same frame ${overlap.kSync} ${times}.`,
|
|
528
|
+
STYLES.label,
|
|
529
|
+
"",
|
|
530
|
+
STYLES.label,
|
|
531
|
+
""
|
|
532
|
+
);
|
|
458
533
|
if (isContextMirror || isStoreMirror) {
|
|
459
|
-
const sourceType = isStoreMirror ? "
|
|
534
|
+
const sourceType = isStoreMirror ? "store" : "context";
|
|
535
|
+
console.log(
|
|
536
|
+
`%cTry:%c If this is not a draft, delete the local hook and read the %c${sourceType}%c in render.`,
|
|
537
|
+
STYLES.bold,
|
|
538
|
+
"",
|
|
539
|
+
STYLES.actionPill,
|
|
540
|
+
""
|
|
541
|
+
);
|
|
542
|
+
} else if (isBooleanLike(nameA) || isBooleanLike(nameB)) {
|
|
460
543
|
console.log(
|
|
461
|
-
`%
|
|
544
|
+
`%cTry:%c One %cstatus%c instead of several flags.`,
|
|
462
545
|
STYLES.bold,
|
|
463
546
|
"",
|
|
464
547
|
STYLES.actionPill,
|
|
465
548
|
""
|
|
466
549
|
);
|
|
467
550
|
} else {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
);
|
|
478
|
-
} else {
|
|
479
|
-
console.log(
|
|
480
|
-
`%cFix:%c Redundant State detected. Derive %c${infoB.name}%c from %c${infoA.name}%c during render, or use %cuseMemo%c.`,
|
|
481
|
-
STYLES.bold,
|
|
482
|
-
"",
|
|
483
|
-
STYLES.label,
|
|
484
|
-
"",
|
|
485
|
-
STYLES.label,
|
|
486
|
-
"",
|
|
487
|
-
STYLES.actionPill,
|
|
488
|
-
""
|
|
489
|
-
);
|
|
490
|
-
}
|
|
551
|
+
console.log(
|
|
552
|
+
`%cTry:%c If %c${nameB}%c is just %c${nameA}%c in another shape, compute it while rendering.`,
|
|
553
|
+
STYLES.bold,
|
|
554
|
+
"",
|
|
555
|
+
STYLES.label,
|
|
556
|
+
"",
|
|
557
|
+
STYLES.label,
|
|
558
|
+
""
|
|
559
|
+
);
|
|
491
560
|
}
|
|
492
561
|
console.groupEnd();
|
|
493
562
|
};
|
|
494
|
-
var displayCausalHint = (targetLabel,
|
|
563
|
+
var displayCausalHint = (targetLabel, _targetMeta, sourceLabel, sourceMeta) => {
|
|
495
564
|
if (!isWeb || !shouldLog(`causal-${sourceLabel}-${targetLabel}`)) return;
|
|
496
565
|
const target = parseLabel(targetLabel);
|
|
497
|
-
const
|
|
498
|
-
const
|
|
566
|
+
const sourceName = displayName(sourceLabel);
|
|
567
|
+
const targetName = displayName(targetLabel);
|
|
568
|
+
const headerType = sourceMeta.role === "context" /* CONTEXT */ ? "extra render from context" : sourceMeta.role === "store" /* STORE */ ? "extra render from a store" : "extra render";
|
|
499
569
|
const isEffect = sourceLabel.includes("effect") || sourceLabel.includes("useLayoutEffect");
|
|
500
|
-
console.groupCollapsed(`%c
|
|
501
|
-
console.log(`%c
|
|
502
|
-
console.log(
|
|
570
|
+
console.groupCollapsed(`%c BASIS | ${headerType} `, STYLES.headerProblem);
|
|
571
|
+
console.log(`%c${target.file}`, STYLES.location);
|
|
572
|
+
console.log(
|
|
573
|
+
`%c${sourceName}%c updates %c${targetName}%c on the next frame.`,
|
|
574
|
+
STYLES.label,
|
|
575
|
+
"",
|
|
576
|
+
STYLES.label,
|
|
577
|
+
""
|
|
578
|
+
);
|
|
503
579
|
if (isEffect) {
|
|
504
580
|
console.log(
|
|
505
|
-
`%
|
|
581
|
+
`%cTry:%c If %c${targetName}%c can be computed while rendering, drop the extra setState.`,
|
|
506
582
|
STYLES.bold,
|
|
507
583
|
"",
|
|
508
584
|
STYLES.label,
|
|
509
|
-
"",
|
|
510
|
-
STYLES.actionPill,
|
|
511
585
|
""
|
|
512
586
|
);
|
|
513
587
|
} else {
|
|
514
588
|
console.log(
|
|
515
|
-
`%
|
|
589
|
+
`%cTry:%c Write %c${targetName}%c in the same update as %c${sourceName}%c if they are one fact.`,
|
|
516
590
|
STYLES.bold,
|
|
517
591
|
"",
|
|
518
592
|
STYLES.label,
|
|
@@ -545,7 +619,7 @@ var displayGraphReport = (graph) => {
|
|
|
545
619
|
if (!isWeb) return;
|
|
546
620
|
if (graph.nodes.length === 0) {
|
|
547
621
|
console.log(
|
|
548
|
-
`%c
|
|
622
|
+
`%c BASIS | update graph %c(nothing recorded yet)`,
|
|
549
623
|
STYLES.headerIdentity,
|
|
550
624
|
`color: ${THEME.muted}; font-style: italic;`
|
|
551
625
|
);
|
|
@@ -564,17 +638,22 @@ var displayGraphReport = (graph) => {
|
|
|
564
638
|
occurrences: g.occurrences
|
|
565
639
|
}));
|
|
566
640
|
const groupedSourceIds = new Set(graph.eventGroups.flatMap((g) => g.sourceIds));
|
|
567
|
-
const nonEventGroups = Array.from(outgoing.keys()).filter((id) => !groupedSourceIds.has(id)).map((id) => ({
|
|
641
|
+
const nonEventGroups = Array.from(outgoing.keys()).filter((id) => !groupedSourceIds.has(id)).map((id) => ({
|
|
642
|
+
sourceIds: [id],
|
|
643
|
+
sourceNode: nodeById.get(id),
|
|
644
|
+
edges: outgoing.get(id),
|
|
645
|
+
occurrences: 1
|
|
646
|
+
}));
|
|
568
647
|
const groups = [...eventGroups, ...nonEventGroups].sort(
|
|
569
648
|
(a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences
|
|
570
649
|
);
|
|
571
650
|
console.group(
|
|
572
|
-
`%c
|
|
651
|
+
`%c BASIS | update graph %c${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 ${groups.length} sources \xB7 last ${graph.bufferWindowSize} frames`,
|
|
573
652
|
STYLES.headerIdentity,
|
|
574
|
-
`color: ${THEME.muted}; font-weight: normal
|
|
653
|
+
`color: ${THEME.muted}; font-weight: normal;`
|
|
575
654
|
);
|
|
576
655
|
console.log(
|
|
577
|
-
`%cparent \u2192 child =
|
|
656
|
+
`%cparent \u2192 child = what we saw cause an update. (\xD7N) = times in this window. Repeat clicks with the same targets are grouped.`,
|
|
578
657
|
STYLES.subText
|
|
579
658
|
);
|
|
580
659
|
groups.forEach((group) => {
|
|
@@ -582,12 +661,12 @@ var displayGraphReport = (graph) => {
|
|
|
582
661
|
const isCtx = group.sourceNode?.role === "context" /* CONTEXT */;
|
|
583
662
|
const isFx = group.sourceNode?.role === "effect";
|
|
584
663
|
const isUnknown = group.sourceNode?.role === "unknown";
|
|
585
|
-
const icon = isEvent ? "\
|
|
664
|
+
const icon = isEvent ? "\u2022" : isCtx ? "ctx" : isFx ? "fx" : isUnknown ? "?" : "\u2022";
|
|
586
665
|
const color = isEvent ? THEME.solution : isCtx ? THEME.context : THEME.identity;
|
|
587
666
|
const fanout = group.edges.length;
|
|
588
667
|
const hits = group.occurrences;
|
|
589
668
|
const hitLabel = hits > 1 ? ` \xB7 \xD7${hits}` : "";
|
|
590
|
-
const title = isEvent ? `
|
|
669
|
+
const title = isEvent ? `click / event \xB7 ${fanout} update${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
|
|
591
670
|
console.groupCollapsed(
|
|
592
671
|
`%c${icon} %c${title}`,
|
|
593
672
|
`color: ${color};`,
|
|
@@ -599,16 +678,16 @@ var displayGraphReport = (graph) => {
|
|
|
599
678
|
const weight = edge.weight > 1 ? ` (\xD7${edge.weight})` : "";
|
|
600
679
|
if (target?.redundant) {
|
|
601
680
|
console.log(
|
|
602
|
-
`%c ${label}%c${weight} %
|
|
681
|
+
`%c ${label}%c${weight} %cmoving with another hook`,
|
|
603
682
|
`color: ${THEME.muted}; font-family: monospace;`,
|
|
604
|
-
`color: ${THEME.muted}
|
|
683
|
+
`color: ${THEME.muted};`,
|
|
605
684
|
`color: ${THEME.problem}; font-weight: bold;`
|
|
606
685
|
);
|
|
607
686
|
} else {
|
|
608
687
|
console.log(
|
|
609
688
|
`%c ${label}%c${weight}`,
|
|
610
689
|
`color: ${THEME.muted}; font-family: monospace;`,
|
|
611
|
-
`color: ${THEME.muted}
|
|
690
|
+
`color: ${THEME.muted};`
|
|
612
691
|
);
|
|
613
692
|
}
|
|
614
693
|
});
|
|
@@ -616,19 +695,21 @@ var displayGraphReport = (graph) => {
|
|
|
616
695
|
});
|
|
617
696
|
console.groupEnd();
|
|
618
697
|
};
|
|
619
|
-
var displayViolentBreaker = (label, count,
|
|
698
|
+
var displayViolentBreaker = (label, count, _threshold) => {
|
|
620
699
|
if (!isWeb) return;
|
|
621
|
-
const
|
|
622
|
-
console.group(`%c
|
|
623
|
-
console.error(
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
console.log(
|
|
700
|
+
const name = displayName(label);
|
|
701
|
+
console.group(`%c BASIS | loop guard `, STYLES.headerProblem);
|
|
702
|
+
console.error(
|
|
703
|
+
`${name} updated ${count} times in one second. Basis stopped recording this path so the tab stays usable.`
|
|
704
|
+
);
|
|
705
|
+
console.log(
|
|
706
|
+
`%cReact may still error on its own. Fix the effect that writes a value it also lists as a dependency.`,
|
|
707
|
+
`color: ${THEME.muted};`
|
|
708
|
+
);
|
|
627
709
|
console.groupEnd();
|
|
628
710
|
};
|
|
629
711
|
|
|
630
712
|
// src/core/analysis.ts
|
|
631
|
-
var CAUSAL_MARGIN = 0.05;
|
|
632
713
|
var isEventDriven = (label, graph) => {
|
|
633
714
|
for (const [parent, targets] of graph.entries()) {
|
|
634
715
|
if (parent.startsWith("Event_Tick_") && targets.has(label)) {
|
|
@@ -638,29 +719,40 @@ var isEventDriven = (label, graph) => {
|
|
|
638
719
|
return false;
|
|
639
720
|
};
|
|
640
721
|
var calculateAllSimilarities = (entryA, entryB) => {
|
|
641
|
-
const
|
|
642
|
-
entryA.meta.buffer,
|
|
643
|
-
entryA.meta.head,
|
|
644
|
-
entryB.meta.buffer,
|
|
645
|
-
entryB.meta.head,
|
|
646
|
-
0
|
|
647
|
-
);
|
|
648
|
-
const bA = calculateSimilarityCircular(
|
|
649
|
-
entryA.meta.buffer,
|
|
650
|
-
entryA.meta.head,
|
|
651
|
-
entryB.meta.buffer,
|
|
652
|
-
entryB.meta.head,
|
|
653
|
-
1
|
|
654
|
-
);
|
|
655
|
-
const aB = calculateSimilarityCircular(
|
|
722
|
+
const { kSync, kALeadsB, kBLeadsA, densityA, densityB } = countOverlapsCircular(
|
|
656
723
|
entryA.meta.buffer,
|
|
657
724
|
entryA.meta.head,
|
|
658
725
|
entryB.meta.buffer,
|
|
659
|
-
entryB.meta.head
|
|
660
|
-
-1
|
|
726
|
+
entryB.meta.head
|
|
661
727
|
);
|
|
662
|
-
|
|
728
|
+
const sync = cosineFromOverlap(kSync, densityA, densityB);
|
|
729
|
+
const bA = cosineFromOverlap(kALeadsB, densityA, densityB);
|
|
730
|
+
const aB = cosineFromOverlap(kBLeadsA, densityA, densityB);
|
|
731
|
+
const max = Math.max(sync, bA, aB);
|
|
732
|
+
const windowSize = entryA.meta.buffer.length;
|
|
733
|
+
const significantSync = isSignificantOverlap(kSync, densityA, densityB, windowSize);
|
|
734
|
+
const kLead = Math.max(kALeadsB, kBLeadsA);
|
|
735
|
+
const significantLead = isSignificantOverlap(kLead, densityA, densityB, windowSize) && kLead >= kSync + 1;
|
|
736
|
+
return {
|
|
737
|
+
sync,
|
|
738
|
+
bA,
|
|
739
|
+
aB,
|
|
740
|
+
max,
|
|
741
|
+
kSync,
|
|
742
|
+
kALeadsB,
|
|
743
|
+
kBLeadsA,
|
|
744
|
+
densityA,
|
|
745
|
+
densityB,
|
|
746
|
+
significantSync,
|
|
747
|
+
significantLead
|
|
748
|
+
};
|
|
663
749
|
};
|
|
750
|
+
var overlapFrom = (s) => ({
|
|
751
|
+
kSync: s.kSync,
|
|
752
|
+
densityA: s.densityA,
|
|
753
|
+
densityB: s.densityB,
|
|
754
|
+
cosine: s.sync
|
|
755
|
+
});
|
|
664
756
|
var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
|
|
665
757
|
if (entryA.label === entryB.label) return true;
|
|
666
758
|
if (isSameField(entryA.label, entryB.label)) return true;
|
|
@@ -684,52 +776,51 @@ var detectRedundancy = (entryA, entryB, similarities, redundantSet, violationMap
|
|
|
684
776
|
const roleA = entryA.meta.role;
|
|
685
777
|
const roleB = entryB.meta.role;
|
|
686
778
|
if (isGlobalSource(roleA) && isGlobalSource(roleB)) return;
|
|
687
|
-
if (
|
|
779
|
+
if (similarities.densityA < 2 || similarities.densityB < 2) return;
|
|
780
|
+
const overlap = overlapFrom(similarities);
|
|
688
781
|
if (roleA === "local" /* LOCAL */ && isGlobalSource(roleB)) {
|
|
689
782
|
redundantSet.add(entryA.label);
|
|
690
|
-
pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label,
|
|
691
|
-
displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta,
|
|
783
|
+
pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, overlap });
|
|
784
|
+
displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, overlap);
|
|
692
785
|
} else if (isGlobalSource(roleA) && roleB === "local" /* LOCAL */) {
|
|
693
786
|
redundantSet.add(entryB.label);
|
|
694
|
-
pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label,
|
|
695
|
-
displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta,
|
|
787
|
+
pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, overlap });
|
|
788
|
+
displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, overlap);
|
|
696
789
|
} else if (roleA === "local" /* LOCAL */ && roleB === "local" /* LOCAL */) {
|
|
697
790
|
redundantSet.add(entryA.label);
|
|
698
791
|
redundantSet.add(entryB.label);
|
|
699
|
-
pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label,
|
|
700
|
-
pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label,
|
|
701
|
-
displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta,
|
|
792
|
+
pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, overlap });
|
|
793
|
+
pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, overlap });
|
|
794
|
+
displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, overlap);
|
|
702
795
|
}
|
|
703
796
|
};
|
|
704
797
|
var detectCausalLeak = (entryA, entryB, similarities, violationMap, graph) => {
|
|
705
798
|
if (entryA.isVolatile || entryB.isVolatile) return;
|
|
706
|
-
if (similarities.max - similarities.sync < CAUSAL_MARGIN) return;
|
|
707
799
|
const addLeak = (source, target) => {
|
|
708
800
|
if (isEventDriven(target, graph)) return;
|
|
709
|
-
|
|
710
|
-
violationMap.set(source, []);
|
|
711
|
-
}
|
|
712
|
-
violationMap.get(source).push({ type: "causal_leak", target });
|
|
801
|
+
pushViolation(violationMap, source, { type: "causal_leak", target });
|
|
713
802
|
const sourceEntry = source === entryA.label ? entryA : entryB;
|
|
714
803
|
const targetEntry = source === entryA.label ? entryB : entryA;
|
|
715
804
|
displayCausalHint(target, targetEntry.meta, source, sourceEntry.meta);
|
|
716
805
|
};
|
|
717
|
-
if (similarities.
|
|
806
|
+
if (similarities.kALeadsB >= similarities.kBLeadsA) {
|
|
718
807
|
addLeak(entryA.label, entryB.label);
|
|
719
|
-
} else
|
|
808
|
+
} else {
|
|
720
809
|
addLeak(entryB.label, entryA.label);
|
|
721
810
|
}
|
|
722
811
|
};
|
|
723
812
|
var detectSubspaceOverlap = (dirtyEntries, allEntries, redundantSet, dirtyLabels2, graph) => {
|
|
724
|
-
let compCount = 0;
|
|
725
813
|
const violationMap = /* @__PURE__ */ new Map();
|
|
814
|
+
let compCount = 0;
|
|
726
815
|
for (const entryA of dirtyEntries) {
|
|
727
816
|
for (const entryB of allEntries) {
|
|
728
817
|
if (shouldSkipComparison(entryA, entryB, dirtyLabels2)) continue;
|
|
729
818
|
compCount++;
|
|
730
819
|
const similarities = calculateAllSimilarities(entryA, entryB);
|
|
731
|
-
if (similarities.
|
|
820
|
+
if (similarities.significantSync) {
|
|
732
821
|
detectRedundancy(entryA, entryB, similarities, redundantSet, violationMap);
|
|
822
|
+
}
|
|
823
|
+
if (similarities.significantLead) {
|
|
733
824
|
detectCausalLeak(entryA, entryB, similarities, violationMap, graph);
|
|
734
825
|
}
|
|
735
826
|
}
|
|
@@ -962,9 +1053,9 @@ var registerVariable = (l, o = {}) => {
|
|
|
962
1053
|
});
|
|
963
1054
|
}
|
|
964
1055
|
};
|
|
965
|
-
var printBasisHealthReport = (
|
|
1056
|
+
var printBasisHealthReport = () => {
|
|
966
1057
|
if (!instance.config.debug) return;
|
|
967
|
-
displayHealthReport(instance.history,
|
|
1058
|
+
displayHealthReport(instance.history, instance.violationMap);
|
|
968
1059
|
};
|
|
969
1060
|
var getBasisMetrics = () => ({
|
|
970
1061
|
engine: "v0.6.x",
|