rerender-lens 0.4.0 → 0.5.0
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 +78 -0
- package/README.md +192 -362
- package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
- package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
- package/dist/cli.cjs +248 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +248 -31
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-CZebzpF6.d.ts → devtools-BYWat7nQ.d.ts} +7 -1
- package/dist/{devtools-BkCct3cJ.d.cts → devtools-DJi_6AfJ.d.cts} +7 -1
- package/dist/index.cjs +439 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -8
- package/dist/index.d.ts +131 -8
- package/dist/index.js +424 -42
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +1673 -0
- package/dist/jest-setup.cjs.map +1 -0
- package/dist/jest-setup.d.cts +8 -0
- package/dist/jest-setup.d.ts +8 -0
- package/dist/jest-setup.js +1671 -0
- package/dist/jest-setup.js.map +1 -0
- package/dist/jest.cjs +1760 -0
- package/dist/jest.cjs.map +1 -0
- package/dist/jest.d.cts +23 -0
- package/dist/jest.d.ts +23 -0
- package/dist/jest.js +1751 -0
- package/dist/jest.js.map +1 -0
- package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
- package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
- package/dist/playwright.cjs +230 -28
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +4 -4
- package/dist/playwright.d.ts +4 -4
- package/dist/playwright.js +230 -28
- package/dist/playwright.js.map +1 -1
- package/dist/relay.cjs +10 -2
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.js +10 -2
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +356 -54
- package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
- package/dist/runner-report-CW8X5RwK.d.ts +46 -0
- package/dist/setup.cjs +112 -27
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +112 -27
- package/dist/setup.js.map +1 -1
- package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
- package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +337 -41
- package/dist/vitest-setup.cjs.map +1 -1
- package/dist/vitest-setup.d.cts +4 -4
- package/dist/vitest-setup.d.ts +4 -4
- package/dist/vitest-setup.js +337 -41
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +400 -77
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +21 -39
- package/dist/vitest.d.ts +21 -39
- package/dist/vitest.js +401 -78
- package/dist/vitest.js.map +1 -1
- package/package.json +40 -4
- package/panel/panel.css +1 -0
- package/panel/panel.js +499 -309
package/dist/cli.js
CHANGED
|
@@ -4,50 +4,206 @@ import { createServer } from 'http';
|
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
|
|
7
|
-
// src/
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
// src/diff.ts
|
|
8
|
+
var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
9
|
+
|
|
10
|
+
// src/causes.ts
|
|
11
|
+
var isAncestorReport = (anc, r) => anc.path.length < r.path.length && r.path[anc.path.length] === anc.component && anc.path.every((p, i) => r.path[i] === p);
|
|
12
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
13
|
+
function indexByComponent(reports) {
|
|
14
|
+
const index = /* @__PURE__ */ new Map();
|
|
15
|
+
for (const r of reports) {
|
|
16
|
+
const list = index.get(r.component);
|
|
17
|
+
if (list) list.push(r);
|
|
18
|
+
else index.set(r.component, [r]);
|
|
19
|
+
}
|
|
20
|
+
return index;
|
|
21
|
+
}
|
|
22
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
23
|
+
let cur = r;
|
|
24
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
25
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
26
|
+
const parent = cur.parent;
|
|
27
|
+
const candidates = index.get(parent.name);
|
|
28
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
29
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
30
|
+
seen.add(p);
|
|
31
|
+
cur = p;
|
|
32
|
+
}
|
|
33
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
34
|
+
}
|
|
35
|
+
function rootCausesOf(reports) {
|
|
36
|
+
const roots = /* @__PURE__ */ new Map();
|
|
37
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
38
|
+
const index = indexByComponent(reports);
|
|
39
|
+
let avoidable = 0;
|
|
40
|
+
let wasted = 0;
|
|
41
|
+
for (const r of reports) {
|
|
42
|
+
if (!r.avoidable) continue;
|
|
43
|
+
avoidable++;
|
|
44
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
45
|
+
const root = rootCauseOf(r, reports, index);
|
|
46
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
47
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
48
|
+
rootByReport.set(r, name);
|
|
49
|
+
let agg = roots.get(name);
|
|
50
|
+
if (!agg) {
|
|
51
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
52
|
+
roots.set(name, agg);
|
|
53
|
+
}
|
|
54
|
+
agg.count++;
|
|
55
|
+
bump(agg.components, r.component);
|
|
56
|
+
}
|
|
57
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
58
|
+
}
|
|
59
|
+
function groupByCommit(reports) {
|
|
60
|
+
const out = /* @__PURE__ */ new Map();
|
|
61
|
+
for (const r of reports) {
|
|
62
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
63
|
+
const list = out.get(r.commitId);
|
|
64
|
+
if (list) list.push(r);
|
|
65
|
+
else out.set(r.commitId, [r]);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function rankRootCauses(reports) {
|
|
70
|
+
const byName = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
72
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
73
|
+
let agg = byName.get(root.name);
|
|
74
|
+
if (!agg) {
|
|
75
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
76
|
+
byName.set(root.name, agg);
|
|
77
|
+
}
|
|
78
|
+
agg.commits++;
|
|
79
|
+
agg.count += root.count;
|
|
80
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
84
|
+
}
|
|
85
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
86
|
+
function formatRootCauses(reports, limit = 5) {
|
|
87
|
+
const ranked = rankRootCauses(reports);
|
|
88
|
+
const lines = ranked.slice(0, limit).map((root, i) => `${String(i + 1).padStart(2)}. <${root.name}> (${root.trigger}) started ${plural(root.commits, "commit")}, ${plural(root.count, "avoidable re-render")} (${[...root.components].map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
|
|
89
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
90
|
+
return lines.join("\n");
|
|
17
91
|
}
|
|
18
92
|
|
|
19
93
|
// src/fixes.ts
|
|
20
|
-
var
|
|
94
|
+
var MAX_FIX_REPORTS = 50;
|
|
21
95
|
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
96
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
97
|
+
function shortValue(v, max = 60) {
|
|
98
|
+
let s;
|
|
99
|
+
try {
|
|
100
|
+
s = JSON.stringify(v);
|
|
101
|
+
} catch {
|
|
102
|
+
s = String(v);
|
|
103
|
+
}
|
|
104
|
+
if (s === void 0) s = String(v);
|
|
105
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
106
|
+
}
|
|
22
107
|
function fixesFor(r) {
|
|
23
108
|
const out = [];
|
|
24
|
-
const
|
|
25
|
-
const
|
|
109
|
+
const ownerName = r.owner || r.parent && r.parent.name || null;
|
|
110
|
+
const propChanges = r.propChanges || [];
|
|
111
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
26
112
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
113
|
+
const identical = changes.length === 0;
|
|
27
114
|
out.push({
|
|
28
115
|
kind: "memo",
|
|
29
116
|
owner: r.component,
|
|
30
117
|
target: r.component,
|
|
31
118
|
prop: null,
|
|
32
119
|
label: `Wrap <${r.component}> in React.memo`,
|
|
33
|
-
detail:
|
|
120
|
+
detail: identical ? `<${r.component}> re-rendered with identical props because <${r.parent && r.parent.name || "its parent"}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`,
|
|
121
|
+
snippet: `// ${r.component}
|
|
122
|
+
import { memo } from 'react';
|
|
123
|
+
|
|
124
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
125
|
+
// ...
|
|
126
|
+
});
|
|
127
|
+
// class components: extend PureComponent instead`
|
|
34
128
|
});
|
|
35
129
|
}
|
|
36
|
-
for (const c of
|
|
37
|
-
if (!
|
|
130
|
+
for (const c of propChanges) {
|
|
131
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
132
|
+
const owner = ownerName || "?";
|
|
38
133
|
const root = rootOf(c.path);
|
|
134
|
+
const id = identifier(root);
|
|
39
135
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
40
|
-
out.push({
|
|
136
|
+
out.push({
|
|
137
|
+
kind: "children",
|
|
138
|
+
owner,
|
|
139
|
+
target: r.component,
|
|
140
|
+
prop: "children",
|
|
141
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
142
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
143
|
+
snippet: `// ${owner}
|
|
144
|
+
import { useMemo } from 'react';
|
|
145
|
+
|
|
146
|
+
const children = useMemo(() => (
|
|
147
|
+
<>{/* the same elements */}</>
|
|
148
|
+
), [/* deps */]);
|
|
149
|
+
|
|
150
|
+
<${r.component}>{children}</${r.component}>
|
|
151
|
+
|
|
152
|
+
// static children: hoist them to module scope
|
|
153
|
+
const STATIC = <em>hi</em>;`
|
|
154
|
+
});
|
|
41
155
|
} else if (c.kind === "function") {
|
|
42
|
-
out.push({
|
|
156
|
+
out.push({
|
|
157
|
+
kind: "useCallback",
|
|
158
|
+
owner,
|
|
159
|
+
target: r.component,
|
|
160
|
+
prop: root,
|
|
161
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
162
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
163
|
+
snippet: `// ${owner}
|
|
164
|
+
import { useCallback } from 'react';
|
|
165
|
+
|
|
166
|
+
const ${id} = useCallback((/* args */) => {
|
|
167
|
+
// ...
|
|
168
|
+
}, [/* deps */]);
|
|
169
|
+
|
|
170
|
+
<${r.component} ${root}={${id}} />`
|
|
171
|
+
});
|
|
43
172
|
} else if (c.kind === "element") {
|
|
44
|
-
out.push({
|
|
173
|
+
out.push({
|
|
174
|
+
kind: "useMemoElement",
|
|
175
|
+
owner,
|
|
176
|
+
target: r.component,
|
|
177
|
+
prop: root,
|
|
178
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
179
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
180
|
+
snippet: `// ${owner}
|
|
181
|
+
import { useMemo } from 'react';
|
|
182
|
+
|
|
183
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
184
|
+
// or pass it as children from a component that does not re-render`
|
|
185
|
+
});
|
|
45
186
|
} else {
|
|
46
|
-
|
|
187
|
+
const isArray = Array.isArray(c.next);
|
|
188
|
+
out.push({
|
|
189
|
+
kind: "useMemo",
|
|
190
|
+
owner,
|
|
191
|
+
target: r.component,
|
|
192
|
+
prop: root,
|
|
193
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
194
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
195
|
+
snippet: `// ${owner}
|
|
196
|
+
import { useMemo } from 'react';
|
|
197
|
+
|
|
198
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
199
|
+
|
|
200
|
+
// or, when it never changes, hoist it to module scope:
|
|
201
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
202
|
+
});
|
|
47
203
|
}
|
|
48
204
|
}
|
|
49
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
50
|
-
const isContext = c.hook === "useContext";
|
|
205
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
206
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
51
207
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
52
208
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
53
209
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -57,20 +213,63 @@ function fixesFor(r) {
|
|
|
57
213
|
target: r.component,
|
|
58
214
|
prop: ctxName,
|
|
59
215
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
60
|
-
detail: `${c.changedKeys.
|
|
216
|
+
detail: `${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer (like <${r.component}>) re-rendered. Consumers that read the other keys re-render for nothing.`,
|
|
217
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
218
|
+
// one context per independently-changing slice
|
|
219
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
220
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
221
|
+
|
|
222
|
+
// or keep one context and let consumers select a slice:
|
|
223
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
61
224
|
});
|
|
62
225
|
continue;
|
|
63
226
|
}
|
|
64
|
-
if (!
|
|
227
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
65
228
|
if (isContext) {
|
|
66
|
-
out.push({
|
|
229
|
+
out.push({
|
|
230
|
+
kind: "contextValue",
|
|
231
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
232
|
+
target: r.component,
|
|
233
|
+
prop: ctxName,
|
|
234
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
235
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
236
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
237
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
238
|
+
<${ctxName}.Provider value={value}>`
|
|
239
|
+
});
|
|
67
240
|
} else if (c.hook === "useSyncExternalStore") {
|
|
68
241
|
const chain = c.custom || [];
|
|
69
242
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
70
243
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
71
|
-
|
|
244
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
245
|
+
out.push({
|
|
246
|
+
kind: "storeSnapshot",
|
|
247
|
+
owner: r.component,
|
|
248
|
+
target: r.component,
|
|
249
|
+
prop: c.path,
|
|
250
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
251
|
+
detail: redux ? `the selector${via} returns a new object on every call, so the component re-renders on every store change.` : zustand ? `the store selector${via} returns a new object on every call, so the component re-renders on every store change.` : `${c.path}${via} returned a new reference with the same contents; getSnapshot must return a cached value.`,
|
|
252
|
+
snippet: redux ? `// ${r.component}
|
|
253
|
+
import { shallowEqual } from 'react-redux';
|
|
254
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
255
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
256
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
257
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
258
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
259
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
260
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
261
|
+
});
|
|
72
262
|
} else {
|
|
73
|
-
out.push({
|
|
263
|
+
out.push({
|
|
264
|
+
kind: "bailout",
|
|
265
|
+
owner: r.component,
|
|
266
|
+
target: r.component,
|
|
267
|
+
prop: c.path,
|
|
268
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
269
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
270
|
+
snippet: `// ${r.component}
|
|
271
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
272
|
+
});
|
|
74
273
|
}
|
|
75
274
|
}
|
|
76
275
|
return out;
|
|
@@ -84,11 +283,12 @@ function rankFixes(reports) {
|
|
|
84
283
|
const key = fixKey(f);
|
|
85
284
|
let agg = byKey.get(key);
|
|
86
285
|
if (!agg) {
|
|
87
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
286
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
88
287
|
byKey.set(key, agg);
|
|
89
288
|
}
|
|
90
289
|
agg.count++;
|
|
91
290
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
291
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
92
292
|
}
|
|
93
293
|
}
|
|
94
294
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -98,6 +298,8 @@ function formatFixes(reports, limit = 10) {
|
|
|
98
298
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
99
299
|
const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
|
|
100
300
|
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
301
|
+
const roots = formatRootCauses(reports, 5);
|
|
302
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
101
303
|
return lines.join("\n");
|
|
102
304
|
}
|
|
103
305
|
|
|
@@ -287,12 +489,20 @@ function createRelayServer(options = {}) {
|
|
|
287
489
|
clearInterval(ping);
|
|
288
490
|
role.delete(res);
|
|
289
491
|
});
|
|
492
|
+
const count = () => ({ __rerenderLens: true, version: 2, type: "relay", payload: { apps: apps.size } });
|
|
290
493
|
if (role === apps) {
|
|
291
|
-
send(panels,
|
|
292
|
-
req.on("close", () => send(panels,
|
|
494
|
+
send(panels, count());
|
|
495
|
+
req.on("close", () => send(panels, count()));
|
|
496
|
+
} else {
|
|
497
|
+
send(/* @__PURE__ */ new Set([res]), count());
|
|
293
498
|
}
|
|
294
499
|
return;
|
|
295
500
|
}
|
|
501
|
+
if (u.pathname === "/favicon.ico") {
|
|
502
|
+
res.statusCode = 204;
|
|
503
|
+
res.end();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
296
506
|
if (u.pathname === "/message" && req.method === "POST") {
|
|
297
507
|
try {
|
|
298
508
|
const parsed = JSON.parse(await readBody(req) || "null");
|
|
@@ -393,6 +603,13 @@ function main(argv) {
|
|
|
393
603
|
console.log(formatFixes(reports, Number(flag("--limit") || 20)));
|
|
394
604
|
return 0;
|
|
395
605
|
}
|
|
606
|
+
case "causes": {
|
|
607
|
+
const file = positional[0];
|
|
608
|
+
if (!file) throw new Error("usage: rerender-lens causes <export.json> [--limit N]");
|
|
609
|
+
const { reports } = parseExport(read(file));
|
|
610
|
+
console.log(formatRootCauses(reports, Number(flag("--limit") || 10)) || "No root causes: no avoidable re-renders, or the reports carry no commit ids.");
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
396
613
|
case "summary": {
|
|
397
614
|
const file = positional[0];
|
|
398
615
|
if (!file) throw new Error("usage: rerender-lens summary <export.json> [--out summary.json]");
|
|
@@ -436,7 +653,7 @@ ${cmp.regressions.length} component${cmp.regressions.length === 1 ? "" : "s"} re
|
|
|
436
653
|
return 1;
|
|
437
654
|
}
|
|
438
655
|
default:
|
|
439
|
-
console.error("usage: rerender-lens <panel|fixes|summary|compare|budget> ...");
|
|
656
|
+
console.error("usage: rerender-lens <panel|fixes|causes|summary|compare|budget> ...");
|
|
440
657
|
return cmd ? 1 : 0;
|
|
441
658
|
}
|
|
442
659
|
} catch (e) {
|