rerender-lens 0.2.0 → 0.3.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +146 -5
  3. package/dist/budget-COBu7jBU.d.cts +64 -0
  4. package/dist/budget-LkNjGtRc.d.ts +64 -0
  5. package/dist/cli.cjs +458 -0
  6. package/dist/cli.cjs.map +1 -0
  7. package/dist/cli.d.cts +4 -0
  8. package/dist/cli.d.ts +4 -0
  9. package/dist/cli.js +452 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/devtools-BHGUUo-p.d.ts +244 -0
  12. package/dist/devtools-DmhWiWcN.d.cts +244 -0
  13. package/dist/index.cjs +652 -23
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +40 -336
  16. package/dist/index.d.ts +40 -336
  17. package/dist/index.js +636 -24
  18. package/dist/index.js.map +1 -1
  19. package/dist/notifiers-BGQtWKfX.d.cts +90 -0
  20. package/dist/notifiers-BjjGSHpp.d.ts +90 -0
  21. package/dist/playwright.cjs +196 -0
  22. package/dist/playwright.cjs.map +1 -0
  23. package/dist/playwright.d.cts +35 -0
  24. package/dist/playwright.d.ts +35 -0
  25. package/dist/playwright.js +184 -0
  26. package/dist/playwright.js.map +1 -0
  27. package/dist/relay.cjs +166 -0
  28. package/dist/relay.cjs.map +1 -0
  29. package/dist/relay.d.cts +41 -0
  30. package/dist/relay.d.ts +41 -0
  31. package/dist/relay.js +161 -0
  32. package/dist/relay.js.map +1 -0
  33. package/dist/rerender-lens.iife.js +1717 -0
  34. package/dist/setup.cjs +1510 -0
  35. package/dist/setup.cjs.map +1 -0
  36. package/dist/setup.d.cts +2 -0
  37. package/dist/setup.d.ts +2 -0
  38. package/dist/setup.js +1508 -0
  39. package/dist/setup.js.map +1 -0
  40. package/dist/types-BzUEVkxJ.d.cts +177 -0
  41. package/dist/types-BzUEVkxJ.d.ts +177 -0
  42. package/dist/vite.cjs +113 -0
  43. package/dist/vite.cjs.map +1 -0
  44. package/dist/vite.d.cts +84 -0
  45. package/dist/vite.d.ts +84 -0
  46. package/dist/vite.js +103 -0
  47. package/dist/vite.js.map +1 -0
  48. package/dist/vitest-setup.cjs +1299 -0
  49. package/dist/vitest-setup.cjs.map +1 -0
  50. package/dist/vitest-setup.d.cts +8 -0
  51. package/dist/vitest-setup.d.ts +8 -0
  52. package/dist/vitest-setup.js +1297 -0
  53. package/dist/vitest-setup.js.map +1 -0
  54. package/dist/vitest.cjs +1360 -0
  55. package/dist/vitest.cjs.map +1 -0
  56. package/dist/vitest.d.cts +52 -0
  57. package/dist/vitest.d.ts +52 -0
  58. package/dist/vitest.js +1351 -0
  59. package/dist/vitest.js.map +1 -0
  60. package/package.json +84 -4
  61. package/panel/panel.css +418 -0
  62. package/panel/panel.html +12 -0
  63. package/panel/panel.js +3325 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,75 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - **`npx rerender-lens panel`.** A dependency-free relay (`rerender-lens/relay`) that serves the
6
+ panel and forwards messages between apps and panels over server-sent events and POST, so the
7
+ panel works for any app on any origin without the extension or Vite: Next.js, Webpack, a remote
8
+ dev box, a phone. `createDevtoolsNotifier({ relay })`, `window.__RERENDER_LENS_RELAY__`, or
9
+ `RERENDER_LENS_RELAY` / `NEXT_PUBLIC_RERENDER_LENS_RELAY` with `rerender-lens/setup`. Commands
10
+ (Settings, highlight, replay, clear) go back to the app; the panel re-attaches when an app
11
+ reloads.
12
+ - **`rerender-lens/vitest`.** `setupFiles: ['rerender-lens/vitest/setup']` collects every test
13
+ file's reports; the reporter prints the run's ranked fixes, enforces a budget file, and can
14
+ write a panel-compatible export. `setupRerenderLens(options, { afterAll })` for custom options
15
+ and `failFast`.
16
+ - **`rerender-lens/playwright`.** `installRerenderLens(page, options)` injects the library at
17
+ document start (the bundle now ships as `dist/rerender-lens.iife.js`); `pullReports`,
18
+ `clearReports`, `expectWithinBudget`.
19
+ - A second copy of the library (another bundle on the same page) no longer wraps the hook
20
+ again, which doubled every report.
21
+
22
+ ## 0.3.0
23
+
24
+ Highlights: the panel without the extension (Vite plugin `panel: true`), the `rerender-lens/vite`
25
+ plugin and `rerender-lens/setup` entry, sessions with before/after comparison, custom hook names,
26
+ updaters and effect-loop detection, full hook/context/state snapshots, CI budgets and a CLI, the
27
+ side panel and window modes, and a UI refresh. Details, newest first:
28
+
29
+ - **Panel without the extension.** `rerenderLens({ panel: true })` serves the panel from the Vite
30
+ dev server at `/__rerender-lens/`; the app publishes on a same-origin `BroadcastChannel`
31
+ (`createDevtoolsNotifier({ channel })`) and the panel sends commands back over it. The `panel/`
32
+ directory ships in the package. `pages` limits which HTML pages get the setup script.
33
+ - **Overhead readout.** `info().overhead` reports the time the library spent inspecting commits
34
+ (total and worst commit); the panel shows it in the status tooltip.
35
+ - **Panel polish.** Offenders and Fixes are windowed above 200 rows (sticky header, sortable);
36
+ an Offenders column chooser adds *Places* and *Last seen*; the search box takes `~text` to
37
+ search prop, hook, context and state values; per-component notes and a *Mute* toggle (muted
38
+ components leave the Fixes ranking and the summary strip, persisted per origin); roles and
39
+ ARIA states on the tree, lists, toast and settings dialog, Escape closes Settings.
40
+ - **CI tooling.** `rankFixes` / `formatFixes`, `summarizeReports` / `compareSummaries`, and
41
+ `checkBudget` / `toBudget` / `assertWithinBudget` in the package; the collector gains
42
+ `fixes()`, `summary()` and `assertWithinBudget()`, and `assertNoAvoidable()` now ends with the
43
+ ranked fixes. A `rerender-lens` CLI (`fixes`, `summary --out`, `compare`, `budget --init`)
44
+ works on panel exports and session files and exits non-zero on regressions or violations.
45
+ - **Source context.** The report shows the lines around where the element was created (DevTools
46
+ resources or a fetch of the module in side-panel mode).
47
+ - **Shareable links.** *Copy link* puts the report into a `panel.html?report=…` URL (deflated when
48
+ the browser can); anyone with the extension opens it without the page.
49
+ - **Custom hook names, opt-in** (`resolveHookNames`, also in Settings): hook changes and
50
+ snapshots read `useCounter › useCart › useState#0`. Like React DevTools, the library re-runs a
51
+ component type once with a stand-in dispatcher and reads the custom hooks off the call stack;
52
+ results are cached per type and a failing replay yields no names.
53
+ - **Updaters.** Every report names the components that scheduled the commit (`updaters`, from
54
+ React's updater tracking), so the Commits view shows "set by <X>" even when X is untracked.
55
+ - **Effect loops.** A commit scheduled right after the previous one by a component that rendered
56
+ in it is flagged `effect-after-commit` with the offending commit id; the report explains the
57
+ effect → setState pattern. Suspense boundaries resolving are labelled `suspense-resolved`.
58
+ - **Store advice.** A `useSyncExternalStore` snapshot with equal contents now gets Redux
59
+ (`shallowEqual` / `createSelector`) or Zustand (`useShallow`) advice when the hook chain
60
+ identifies the store hook.
61
+ - **Instances.** Reports carry the element `key`; the tree can group by instance
62
+ (`<Row key="a">`, `<Row #12>`) via the Instances toggle.
63
+ - **`rerender-lens/vite`**: a Vite plugin that starts the library before React in dev, with the
64
+ DevTools notifier, from one line in `vite.config.ts`. **`rerender-lens/setup`**: a side-effect
65
+ entry for Next.js `instrumentation-client.ts`, Webpack entry arrays and the like.
66
+ - CI lints the Firefox package with `web-ext lint`; `extension/store/QA.md` is the manual
67
+ checklist for what no test reaches. A docs site builds from the READMEs (`npm run build:docs`)
68
+ and deploys to GitHub Pages; `npm run docs:media` captures its screenshots and GIF.
69
+ - **Full state on every report.** `hookState` (every `useState` / `useReducer` /
70
+ `useSyncExternalStore` value), `contexts` (every context the component reads) and, for class
71
+ components, `state`. The panel's report shows Hooks, State and Contexts in full, with the
72
+ changed entries as prev → next; the console prints them too. `includeState: false` turns it off
73
+ (also in the panel's Settings).
5
74
  - **Sessions.** Record, use the app, Stop; apply a fix; record again. The Sessions view compares
6
75
  two sessions: avoidable re-renders per component with deltas, totals, wasted time, and which
7
76
  suggested fixes went away. Summaries persist per origin; exports include them.
package/README.md CHANGED
@@ -77,6 +77,67 @@ npm i -D rerender-lens
77
77
 
78
78
  Peer dependency: `react >= 16.8`. Tested with React 18 and 19.
79
79
 
80
+ ## One-line setup per bundler
81
+
82
+ **Vite** (recommended for Vite users: no extension injection, no permissions, one line):
83
+
84
+ ```ts
85
+ // vite.config.ts
86
+ import { rerenderLens } from 'rerender-lens/vite';
87
+
88
+ export default defineConfig({
89
+ plugins: [react(), rerenderLens({ trackAllMemoized: true })],
90
+ });
91
+ ```
92
+
93
+ The plugin injects a module before your entry in dev (`vite build` is untouched) that calls
94
+ `init` with your options plus the DevTools notifier. Matchers can be strings or RegExps;
95
+ `devtools: false` skips the bridge, `applyInBuild: true` keeps it in builds, `pages: ['/']`
96
+ limits which HTML pages get it.
97
+
98
+ **No extension at all**: add `panel: true` and open `http://localhost:5173/__rerender-lens/` in a
99
+ second tab. The dev server serves the same panel the extension uses; the app publishes reports on
100
+ a same-origin `BroadcastChannel` and the panel sends settings and highlight commands back over it.
101
+ Panel state lives in `localStorage`. (`channel` renames the channel, `panel: '/some/path/'` moves
102
+ the mount.)
103
+
104
+ **Next.js** (App or Pages router): run it before React from the client instrumentation file:
105
+
106
+ ```ts
107
+ // instrumentation-client.ts
108
+ import 'rerender-lens/setup';
109
+ ```
110
+
111
+ **Webpack / Rspack / others**: put the setup entry first:
112
+
113
+ ```js
114
+ entry: ['rerender-lens/setup', './src/index.tsx'],
115
+ ```
116
+
117
+ `rerender-lens/setup` is a side-effect module that calls
118
+ `init({ trackAllMemoized: true, notifier: createDevtoolsNotifier() })` unless
119
+ `process.env.NODE_ENV === 'production'` or the page already runs the library. Use `configure()`
120
+ afterwards to change options, or the extension's Settings.
121
+
122
+ **The panel for any app, no extension, no Vite** (Next.js, Webpack, a remote dev box, a phone):
123
+
124
+ ```sh
125
+ npx rerender-lens panel # http://127.0.0.1:4141/ serves the panel and relays messages
126
+ ```
127
+
128
+ Point the app at it and open the printed URL in any browser:
129
+
130
+ ```ts
131
+ init({ trackAllMemoized: true, notifier: createDevtoolsNotifier({ relay: 'http://127.0.0.1:4141' }) });
132
+ // or, with rerender-lens/setup: RERENDER_LENS_RELAY=http://127.0.0.1:4141 (NEXT_PUBLIC_RERENDER_LENS_RELAY for Next.js)
133
+ // or, before the app loads: window.__RERENDER_LENS_RELAY__ = 'http://127.0.0.1:4141'
134
+ ```
135
+
136
+ The app streams commands in over server-sent events and posts hello, reports and replies back; the
137
+ panel connects the same way (`panel.html?relay=…`), so Settings, highlight and replay all work.
138
+ Several apps or panels can share one relay; the panel re-attaches when an app reloads.
139
+ `--port` and `--host` change where it listens (`--host 0.0.0.0` for another machine).
140
+
80
141
  ## Setup
81
142
 
82
143
  ```ts
@@ -136,6 +197,10 @@ Every update of a tracked component produces a `RenderReport`:
136
197
  | `propChanges` | one entry per changed prop with `path`, `kind`, `prev`, `next` |
137
198
  | `stateChanges` | class components: `this.state` diff |
138
199
  | `hookChanges` | `useState`, `useReducer`, `useSyncExternalStore` and `useContext` values that changed; context entries carry `provider` (who renders it) and `changedKeys` / `totalKeys` for object values |
200
+ | `hookState`, `contexts`, `state` | current value of every state hook, every context read, and class `this.state` (off with `includeState: false`) |
201
+ | `updaters` | components that scheduled the commit (`setState`, dispatch), from React's updater tracking |
202
+ | `commitCause`, `afterCommit` | `effect-after-commit` (an effect of the previous commit set state) or `suspense-resolved` |
203
+ | `key` | the element's `key`, when it has one |
139
204
  | `parent` | nearest ancestor that rendered in the same commit, and why |
140
205
  | `owner` | component that created the element (dev builds) |
141
206
  | `path` | component ancestry from the root |
@@ -184,6 +249,71 @@ test('typing in the search box does not re-render the grid rows', async () => {
184
249
  In Vitest or Jest, call `ensureDevtoolsHook()` (or `init`) from a `setupFiles` entry so the hook
185
250
  exists before `react-dom` is imported by your tests.
186
251
 
252
+ ### Vitest, whole suite
253
+
254
+ Two config lines collect every avoidable re-render across the run and print the ranked fixes at
255
+ the end; a budget file turns it into a gate:
256
+
257
+ ```ts
258
+ // vitest.config.ts
259
+ test: {
260
+ setupFiles: ['rerender-lens/vitest/setup'],
261
+ reporters: ['default', ['rerender-lens/vitest', { budget: 'rerender-budget.json', exportTo: 'rerender-lens.json' }]],
262
+ }
263
+ ```
264
+
265
+ The setup entry starts the library (all memoized components, silent) in every test file and hands
266
+ the file's reports to the reporter; the reporter prints `rerender-lens: N reports, M avoidable`
267
+ with the fixes, fails the run on budget violations, and can write a panel-compatible export
268
+ (`Sessions > Import` in the extension). For other options, or per-test assertions, write your own
269
+ setup file:
270
+
271
+ ```ts
272
+ import { afterAll } from 'vitest';
273
+ import { setupRerenderLens } from 'rerender-lens/vitest';
274
+ export const lens = setupRerenderLens({ include: [/^Grid/], failFast: true }, { afterAll });
275
+ // lens.collector.assertNoAvoidable() inside a test
276
+ ```
277
+
278
+ ### Playwright, real browser
279
+
280
+ ```ts
281
+ import { installRerenderLens, pullReports, expectWithinBudget } from 'rerender-lens/playwright';
282
+
283
+ test('search does not re-render the grid', async ({ page }) => {
284
+ await installRerenderLens(page, { include: ['ProductRow'] }); // before goto: runs before React
285
+ await page.goto('/products');
286
+ await page.getByRole('searchbox').fill('abc');
287
+ expectWithinBudget(await pullReports(page), { '*': 0 }); // throws with the ranked fixes
288
+ });
289
+ ```
290
+
291
+ `installRerenderLens` injects the same bundle the extension uses at document start, so it works on
292
+ any build the browser can load (production builds are flagged; names may be minified). Pass
293
+ `relay: 'http://127.0.0.1:4141'` to watch the run in the panel, `clearReports(page)` between
294
+ scenarios.
295
+
296
+ ### Budgets and comparisons in CI
297
+
298
+ ```ts
299
+ // allow known offenders, fail on anything new or worse
300
+ collector.assertWithinBudget(JSON.parse(readFileSync('rerender-budget.json', 'utf8')));
301
+ // produce the baseline once: writeFileSync('rerender-budget.json', JSON.stringify(toBudget(collector.reports)))
302
+ collector.fixes(); // ranked fixes, most re-renders removed first
303
+ collector.summary('after'); // a session summary, comparable with compareSummaries()
304
+ ```
305
+
306
+ The `rerender-lens` CLI does the same with files from the extension (Export) or from `summary()`:
307
+
308
+ ```sh
309
+ npx rerender-lens fixes export.json # ranked fixes
310
+ npx rerender-lens summary export.json --out before.json
311
+ npx rerender-lens compare before.json after.json # per-component deltas; exit 1 on regressions
312
+ npx rerender-lens budget export.json --init > rerender-budget.json
313
+ npx rerender-lens budget export.json rerender-budget.json # exit 1 when a component exceeds its budget
314
+ npx rerender-lens panel [--port 4141] [--host 127.0.0.1] # the panel + relay for any app (see above)
315
+ ```
316
+
187
317
  ## Track a single component from the inside
188
318
 
189
319
  Works anywhere without `init`, for example inside library packages or Storybook:
@@ -236,10 +366,18 @@ disable(): void
236
366
  isEnabled(): boolean
237
367
  track(component, name?): component
238
368
  ensureDevtoolsHook(): hook // create the global hook early (test setup files)
369
+ // 'rerender-lens/vite': rerenderLens(options), renderSetupModule(options)
370
+ // 'rerender-lens/setup': side-effect entry (init with defaults outside production)
371
+ // 'rerender-lens/relay': createRelayServer({ port?, host? }) — what `rerender-lens panel` runs
372
+ // 'rerender-lens/playwright': installRerenderLens(page, options?), pullReports(page), clearReports(page), expectWithinBudget(reports, budget)
373
+ // 'rerender-lens/vitest': setupRerenderLens(options?, { afterAll }), default reporter ({ budget?, exportTo?, limit? }); 'rerender-lens/vitest/setup'
239
374
  useWhyRerender(name, values, options?)
240
- createCollector(): { reports, avoidable, notifier, clear, assertNoAvoidable }
375
+ createCollector(): { reports, avoidable, notifier, clear, assertNoAvoidable, fixes, summary, assertWithinBudget }
376
+ rankFixes(reports), formatFixes(reports) // ranked fixes
377
+ summarizeReports(reports), compareSummaries(a, b), formatComparison(c), parseExport(json)
378
+ checkBudget(reports, budget), toBudget(reports), assertWithinBudget(reports, budget)
241
379
  combineNotifiers(...notifiers): Notifier
242
- createDevtoolsNotifier({ bufferSize?, target?, maxDepth?, flashAvoidable? }): Notifier
380
+ createDevtoolsNotifier({ bufferSize?, target?, maxDepth?, flashAvoidable?, channel?, relay? }): Notifier
243
381
  getRenderers(), isProductionReact() // what react-dom registered on the DevTools hook
244
382
  serializeOptions(o), deserializeOptions(o) // Options <-> JSON-safe form used by the bridge
245
383
  VERSION
@@ -254,6 +392,8 @@ deepEqual(a, b), diffRecords(prev, next), classify(prev, next)
254
392
  | `trackAllComponents` | `false` | track everything (noisy) |
255
393
  | `include` / `exclude` | | display-name matchers |
256
394
  | `trackHooks` | `true` | diff hook state and contexts |
395
+ | `includeState` | `true` | put every hook, context and class state value on each report |
396
+ | `resolveHookNames` | `false` | label hooks with the custom hooks that own them (`useCart › useState#0`); re-runs each component type once |
257
397
  | `logAll` | `false` | print non-avoidable reports too |
258
398
  | `silent` | `false` | never print; notifier still runs |
259
399
  | `notifier` | | receives every `RenderReport` |
@@ -291,9 +431,10 @@ npm --prefix examples/vite-react install
291
431
  npm run dev:example
292
432
  ```
293
433
 
294
- `http://localhost:5199/` runs the library itself and reports to the console and the extension.
295
- `http://localhost:5199/plain.html` is the same app without the library, for trying the
296
- extension's *Inject the library* mode.
434
+ `http://localhost:5199/` runs the library through the Vite plugin (console, extension, and the
435
+ built-in panel at `http://localhost:5199/__rerender-lens/`). `http://localhost:5199/plain.html`
436
+ is the same app without the library, for trying the extension's *Inject the library* mode.
437
+ `src/rerender-lens.ts` shows the manual setup for apps without the plugin.
297
438
 
298
439
  ## Migrating from why-did-you-render
299
440
 
@@ -0,0 +1,64 @@
1
+ import { R as RenderReport } from './types-BzUEVkxJ.cjs';
2
+
3
+ /**
4
+ * Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.
5
+ * The same logic drives the extension's Fixes view; here it is typed on `RenderReport` for tests,
6
+ * CI budgets and the CLI. Pure.
7
+ */
8
+
9
+ type FixKind = 'memo' | 'useCallback' | 'useMemo' | 'useMemoElement' | 'children' | 'contextValue' | 'splitContext' | 'storeSnapshot' | 'bailout';
10
+ interface Fix {
11
+ kind: FixKind;
12
+ /** Where the change goes (the component that must be edited, or `<Ctx>.Provider`). */
13
+ owner: string;
14
+ /** The component whose re-render this removes. */
15
+ target: string;
16
+ prop: string | null;
17
+ label: string;
18
+ detail: string;
19
+ }
20
+ interface RankedFix extends Fix {
21
+ key: string;
22
+ /** Avoidable re-renders this fix removes. */
23
+ count: number;
24
+ components: Record<string, number>;
25
+ }
26
+ declare function fixesFor(r: RenderReport): Fix[];
27
+ declare const fixKey: (f: Fix) => string;
28
+ /** Every fix across `reports`, most re-renders removed first. */
29
+ declare function rankFixes(reports: RenderReport[]): RankedFix[];
30
+ /** Multi-line text for a test failure or a CI log. */
31
+ declare function formatFixes(reports: RenderReport[], limit?: number): string;
32
+
33
+ /**
34
+ * Re-render budgets for CI: a JSON baseline of allowed avoidable re-renders per component, checked
35
+ * against a collector's reports (like a bundle-size budget). Pure; reading and writing the file is
36
+ * up to the test runner (`fs`), so this works in Node and in the browser.
37
+ */
38
+
39
+ /** `{ "*": 0, "ProductRow": 3 }`: max avoidable re-renders per component; `*` is the default. */
40
+ type Budget = Record<string, number>;
41
+ interface BudgetViolation {
42
+ component: string;
43
+ avoidable: number;
44
+ allowed: number;
45
+ }
46
+ interface BudgetResult {
47
+ ok: boolean;
48
+ violations: BudgetViolation[];
49
+ /** Components with headroom left, so budgets can be tightened. */
50
+ slack: {
51
+ component: string;
52
+ avoidable: number;
53
+ allowed: number;
54
+ }[];
55
+ counts: Record<string, number>;
56
+ }
57
+ declare function avoidableCounts(reports: RenderReport[]): Record<string, number>;
58
+ declare function checkBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
59
+ /** A budget that exactly matches the current counts (the baseline to commit). */
60
+ declare function toBudget(reports: RenderReport[]): Budget;
61
+ /** Throws with the violations and the ranked fixes; use it from a test. */
62
+ declare function assertWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
63
+
64
+ export { type Budget as B, type Fix as F, type RankedFix as R, type BudgetResult as a, type BudgetViolation as b, checkBudget as c, type FixKind as d, assertWithinBudget as e, formatFixes as f, avoidableCounts as g, fixKey as h, fixesFor as i, rankFixes as r, toBudget as t };
@@ -0,0 +1,64 @@
1
+ import { R as RenderReport } from './types-BzUEVkxJ.js';
2
+
3
+ /**
4
+ * Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.
5
+ * The same logic drives the extension's Fixes view; here it is typed on `RenderReport` for tests,
6
+ * CI budgets and the CLI. Pure.
7
+ */
8
+
9
+ type FixKind = 'memo' | 'useCallback' | 'useMemo' | 'useMemoElement' | 'children' | 'contextValue' | 'splitContext' | 'storeSnapshot' | 'bailout';
10
+ interface Fix {
11
+ kind: FixKind;
12
+ /** Where the change goes (the component that must be edited, or `<Ctx>.Provider`). */
13
+ owner: string;
14
+ /** The component whose re-render this removes. */
15
+ target: string;
16
+ prop: string | null;
17
+ label: string;
18
+ detail: string;
19
+ }
20
+ interface RankedFix extends Fix {
21
+ key: string;
22
+ /** Avoidable re-renders this fix removes. */
23
+ count: number;
24
+ components: Record<string, number>;
25
+ }
26
+ declare function fixesFor(r: RenderReport): Fix[];
27
+ declare const fixKey: (f: Fix) => string;
28
+ /** Every fix across `reports`, most re-renders removed first. */
29
+ declare function rankFixes(reports: RenderReport[]): RankedFix[];
30
+ /** Multi-line text for a test failure or a CI log. */
31
+ declare function formatFixes(reports: RenderReport[], limit?: number): string;
32
+
33
+ /**
34
+ * Re-render budgets for CI: a JSON baseline of allowed avoidable re-renders per component, checked
35
+ * against a collector's reports (like a bundle-size budget). Pure; reading and writing the file is
36
+ * up to the test runner (`fs`), so this works in Node and in the browser.
37
+ */
38
+
39
+ /** `{ "*": 0, "ProductRow": 3 }`: max avoidable re-renders per component; `*` is the default. */
40
+ type Budget = Record<string, number>;
41
+ interface BudgetViolation {
42
+ component: string;
43
+ avoidable: number;
44
+ allowed: number;
45
+ }
46
+ interface BudgetResult {
47
+ ok: boolean;
48
+ violations: BudgetViolation[];
49
+ /** Components with headroom left, so budgets can be tightened. */
50
+ slack: {
51
+ component: string;
52
+ avoidable: number;
53
+ allowed: number;
54
+ }[];
55
+ counts: Record<string, number>;
56
+ }
57
+ declare function avoidableCounts(reports: RenderReport[]): Record<string, number>;
58
+ declare function checkBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
59
+ /** A budget that exactly matches the current counts (the baseline to commit). */
60
+ declare function toBudget(reports: RenderReport[]): Budget;
61
+ /** Throws with the violations and the ranked fixes; use it from a test. */
62
+ declare function assertWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
63
+
64
+ export { type Budget as B, type Fix as F, type RankedFix as R, type BudgetResult as a, type BudgetViolation as b, checkBudget as c, type FixKind as d, assertWithinBudget as e, formatFixes as f, avoidableCounts as g, fixKey as h, fixesFor as i, rankFixes as r, toBudget as t };