rerender-lens 0.5.0 → 0.7.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 (64) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/README.md +23 -4
  3. package/dist/{budget-CRswn-nC.d.cts → budget-CA1MS7lB.d.cts} +1 -1
  4. package/dist/{budget-BnJlYwrV.d.ts → budget-ok1VDJ4t.d.ts} +1 -1
  5. package/dist/cli.cjs +78 -26
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +78 -26
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-BYWat7nQ.d.ts → devtools-Cl5n_52v.d.ts} +11 -2
  10. package/dist/{devtools-DJi_6AfJ.d.cts → devtools-UwQx2VYS.d.cts} +11 -2
  11. package/dist/index.cjs +99 -9
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +13 -7
  14. package/dist/index.d.ts +13 -7
  15. package/dist/index.js +99 -10
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +20 -3
  18. package/dist/jest-setup.cjs.map +1 -1
  19. package/dist/jest-setup.d.cts +4 -4
  20. package/dist/jest-setup.d.ts +4 -4
  21. package/dist/jest-setup.js +20 -3
  22. package/dist/jest-setup.js.map +1 -1
  23. package/dist/jest.cjs +20 -3
  24. package/dist/jest.cjs.map +1 -1
  25. package/dist/jest.d.cts +4 -4
  26. package/dist/jest.d.ts +4 -4
  27. package/dist/jest.js +20 -3
  28. package/dist/jest.js.map +1 -1
  29. package/dist/{notifiers-DwKlvv4H.d.cts → notifiers-B5poAEvE.d.cts} +2 -2
  30. package/dist/{notifiers-FNzKHs0a.d.ts → notifiers-CqQW0Eum.d.ts} +2 -2
  31. package/dist/playwright.d.cts +4 -4
  32. package/dist/playwright.d.ts +4 -4
  33. package/dist/relay.cjs +65 -21
  34. package/dist/relay.cjs.map +1 -1
  35. package/dist/relay.d.cts +25 -3
  36. package/dist/relay.d.ts +25 -3
  37. package/dist/relay.js +65 -21
  38. package/dist/relay.js.map +1 -1
  39. package/dist/rerender-lens.iife.js +105 -12
  40. package/dist/{runner-report-CW8X5RwK.d.ts → runner-report-Bvqoe89M.d.ts} +2 -2
  41. package/dist/{runner-report-B8Sc3FZO.d.cts → runner-report-CvpDg2zt.d.cts} +2 -2
  42. package/dist/setup.cjs +98 -9
  43. package/dist/setup.cjs.map +1 -1
  44. package/dist/setup.js +98 -9
  45. package/dist/setup.js.map +1 -1
  46. package/dist/{types-DW5-N2XH.d.cts → types-DwNN5cWe.d.cts} +28 -1
  47. package/dist/{types-DW5-N2XH.d.ts → types-DwNN5cWe.d.ts} +28 -1
  48. package/dist/vite.d.cts +1 -1
  49. package/dist/vite.d.ts +1 -1
  50. package/dist/vitest-setup.cjs +20 -3
  51. package/dist/vitest-setup.cjs.map +1 -1
  52. package/dist/vitest-setup.d.cts +4 -4
  53. package/dist/vitest-setup.d.ts +4 -4
  54. package/dist/vitest-setup.js +20 -3
  55. package/dist/vitest-setup.js.map +1 -1
  56. package/dist/vitest.cjs +20 -3
  57. package/dist/vitest.cjs.map +1 -1
  58. package/dist/vitest.d.cts +5 -5
  59. package/dist/vitest.d.ts +5 -5
  60. package/dist/vitest.js +20 -3
  61. package/dist/vitest.js.map +1 -1
  62. package/package.json +3 -2
  63. package/panel/panel.css +48 -0
  64. package/panel/panel.js +348 -20
package/CHANGELOG.md CHANGED
@@ -1,6 +1,55 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.7.0
4
+
5
+ Highlights: verdicts pinned for modern React (Compiler output, `use()`, transitions, Suspense),
6
+ a Jest integration, React 17 and 18 in CI, several apps on one relay, a commit timeline in the
7
+ panel, a docs site that answers "how do I start", a relay that authenticates off loopback, and a
8
+ panel that tells you why a component is not tracked. Details, newest first:
9
+
10
+ - **The relay authenticates when it is not on loopback.** `npx rerender-lens panel --host 0.0.0.0`
11
+ now generates a token, prints it inside the URL and refuses `/events`, `/message` and `/status`
12
+ without it, because reports carry your app's prop, state and context values. Pass the printed URL
13
+ to the app and the panel and both pick the token up; `--token` sets your own, `--no-token` opts
14
+ out with a warning. Loopback is unchanged and needs no secret.
15
+ - A nightly job records what the library costs per commit on a large tree (`npm run e2e:perf`,
16
+ 3,000 and 10,000 memoized rows) and fails when a commit blows its budget. On a laptop the worst
17
+ commit is about 32 ms at both sizes: the cost is flat in tree size because the per-commit cap
18
+ stops the walk. The extension README now documents that the package ships no remote code and no
19
+ `eval` beyond `chrome.devtools.inspectedWindow.eval`, under MV3's default content security
20
+ policy.
21
+ - **"Why is this component not tracked?"** New `explainTracking(type, options)` returns the same
22
+ decision as `shouldTrack` plus the rule that made it and what to change, and the bridge carries
23
+ it: `info().tracking` (mode, include/exclude, and how many distinct components rendered since
24
+ `init` versus how many were tracked), `inspect(node).tracking`, and a new `explain(node | instanceId)`
25
+ command that also works over the BroadcastChannel and the relay. The panel's empty state now says
26
+ "tracking every React.memo and PureComponent; 42 components rendered, 3 of them tracked" and
27
+ offers *Track every component* and *Track components matching…*; the Elements sidebar shows the
28
+ verdict, the reason and a *Track this component* button for the selected element. Additive:
29
+ the protocol stays at 2.
30
+ - **The docs site is a product page.** A landing page that answers "how do I start with my
31
+ bundler" (Vite, Next.js, Webpack, any app through the relay, the extension, tests) with the
32
+ snippets lifted straight out of the README so they cannot drift, the README as the guide, the
33
+ extension guide, a generated API reference for every entry point, and a hosted copy of the panel
34
+ running its demo data so you can click around before installing anything. A published release
35
+ recaptures the screenshots before building, so they never show an old panel.
36
+ - **Several apps on one relay.** `npx rerender-lens panel` now gives each connected app an id and a
37
+ label (its address), tells the app its id so what it posts is stamped, and sends panels the
38
+ roster. The panel shows a picker when more than one app is connected, watches one at a time and
39
+ addresses its commands to that app, so a host app and a microfrontend (or two pages, or two
40
+ machines) no longer interleave into one stream. A panel or a library that knows nothing about
41
+ ids behaves exactly as before, and the protocol stays at 2.
42
+ - The side panel and the DevTools panel are now covered by a test that changes a setting in one and
43
+ reads it back in the other, in both directions.
44
+ - **Panel: a commit timeline strip above the tree.** One bar per React commit, oldest left, height
45
+ on a sqrt scale of the commit's render count and filled from the bottom with its avoidable share.
46
+ Click a bar to open that commit in the Commits view; drag across bars to brush a time window, and
47
+ the tree, Offenders, Fixes, Commits and the live stream all narrow to it (a chip in the strip
48
+ header says how wide the window is and how many reports it holds; `Esc` or the chip clears it).
49
+ Bars are focusable, labelled ("commit 12, 4 renders, 3 avoidable, 8 seconds ago") and navigable
50
+ with the arrow keys. The strip collapses from its chevron (persisted per origin), draws at most
51
+ the 200 most recent commits, and repaints on the same `LEFT_RENDER_INTERVAL` throttle as the
52
+ other heavy views once the buffer passes 200 reports.
4
53
 
5
54
  - **Jest.** `rerender-lens/jest` and `rerender-lens/jest/setup`: the same deal as the Vitest
6
55
  integration — one `setupFilesAfterEnv` line collects every avoidable re-render, and the reporter
package/README.md CHANGED
@@ -30,9 +30,11 @@ Adds a **Re-renders** tab: the component tree with avoidable counts, why each co
30
30
  the props and hooks that changed, and the fix as a snippet. Other views rank components by wasted
31
31
  renders (Offenders), group renders by React commit with their root cause (Commits), rank every fix
32
32
  by how many re-renders it removes (Fixes), and compare a recording before and after a fix
33
- (Sessions). Also: an Elements-panel sidebar, a badge with the tab's avoidable count, hover to
34
- highlight in the page, "open source" links, and the same panel next to the page (side panel) or
35
- in its own window.
33
+ (Sessions). A timeline strip above the tree draws one bar per commit sized by its renders and
34
+ coloured by its avoidable share: click a bar for that commit, drag across bars to brush a time
35
+ window that every view narrows to. Also: an Elements-panel sidebar, a badge with the tab's
36
+ avoidable count, hover to highlight in the page, "open source" links, and the same panel next to
37
+ the page (side panel) or in its own window.
36
38
 
37
39
  **Install** (until the Web Store listing is live): download `rerender-lens-chrome-<version>.zip`
38
40
  from the [latest release](https://github.com/NexaLeaf/rerender-lens/releases), unzip it, open
@@ -53,6 +55,16 @@ Injection tracks every `memo` / `PureComponent` and leaves the hook/context/stat
53
55
  (*Include state* in Settings turns them on; they are the costliest part on large apps). Settings
54
56
  are saved per origin. Details, shortcuts and packaging: [`extension/README.md`](extension/README.md).
55
57
 
58
+ ### Nothing shows up?
59
+
60
+ Most often the default is doing exactly what it says: injection tracks only `React.memo` components
61
+ and `PureComponent` classes, and an app of plain function components has none. The panel's empty
62
+ state says so in words — *"tracking every React.memo and PureComponent; 42 components rendered, 3 of
63
+ them tracked"* — with two buttons: **Track every component** and **Track components matching…**.
64
+ For one component, select its element in **Elements**: the *rerender-lens* sidebar gives the verdict,
65
+ the reason, and a *Track this component* button. Nothing avoidable in a tracked component is the
66
+ other, good, case: it simply has not re-rendered.
67
+
56
68
  ## Vite
57
69
 
58
70
  ```ts
@@ -80,10 +92,17 @@ production builds. To see the panel without the extension:
80
92
  npx rerender-lens panel # http://127.0.0.1:4141/ (--port, --host 0.0.0.0 for another machine)
81
93
  ```
82
94
 
95
+ On loopback it needs no secret. Given any other `--host` it generates a token, prints it in the
96
+ URL, and refuses connections without it, because reports carry your app's prop, state and context
97
+ values. Pass the whole printed URL (token and all) to the app and the panel; `--token` sets your
98
+ own and `--no-token` opts out.
99
+
83
100
  and point the app at it with `RERENDER_LENS_RELAY=http://127.0.0.1:4141`
84
101
  (`NEXT_PUBLIC_RERENDER_LENS_RELAY` for Next.js), `createDevtoolsNotifier({ relay })`, or
85
102
  `window.__RERENDER_LENS_RELAY__` set before the app loads. Settings, highlight and replay travel
86
- back to the app; several apps or panels can share one relay.
103
+ back to the app, and the panel re-attaches when an app reloads. Point **several apps** at one relay
104
+ (a host app and a microfrontend, two pages, two machines) and the panel shows a picker listing each
105
+ by address; it watches one at a time and commands go only to that one.
87
106
 
88
107
  ## Manual setup
89
108
 
@@ -1,4 +1,4 @@
1
- import { a as ReportLike, R as RenderReport } from './types-DW5-N2XH.cjs';
1
+ import { b as ReportLike, R as RenderReport } from './types-DwNN5cWe.cjs';
2
2
 
3
3
  /**
4
4
  * Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.
@@ -1,4 +1,4 @@
1
- import { a as ReportLike, R as RenderReport } from './types-DW5-N2XH.js';
1
+ import { b as ReportLike, R as RenderReport } from './types-DwNN5cWe.js';
2
2
 
3
3
  /**
4
4
  * Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.
package/dist/cli.cjs CHANGED
@@ -5,6 +5,7 @@ var fs = require('fs');
5
5
  var http = require('http');
6
6
  var path = require('path');
7
7
  var url = require('url');
8
+ var crypto = require('crypto');
8
9
 
9
10
  // node_modules/tsup/assets/cjs_shims.js
10
11
  var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
@@ -408,6 +409,8 @@ function checkBudget(reports, budget) {
408
409
  function toBudget(reports) {
409
410
  return { "*": 0, ...avoidableCounts(reports) };
410
411
  }
412
+ var MARKER = "__rerenderLens";
413
+ var PROTOCOL = 2;
411
414
  var MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8" };
412
415
  function findPanelDir() {
413
416
  const here = path.dirname(url.fileURLToPath(importMetaUrl));
@@ -442,26 +445,55 @@ function createRelayServer(options = {}) {
442
445
  const host = options.host ?? "127.0.0.1";
443
446
  const panelDir = options.panelDir === void 0 ? findPanelDir() : options.panelDir;
444
447
  const keepAliveMs = options.keepAliveMs ?? 15e3;
445
- const apps = /* @__PURE__ */ new Set();
448
+ const token = options.token || null;
449
+ const apps = /* @__PURE__ */ new Map();
446
450
  const panels = /* @__PURE__ */ new Set();
447
451
  let url = "";
448
- const send = (targets, message) => {
449
- const line = `data: ${JSON.stringify(message)}
452
+ let nextAppId = 1;
453
+ const connectUrl = () => token ? `${url}?token=${encodeURIComponent(token)}` : url;
454
+ const line = (message) => `data: ${JSON.stringify(message)}
450
455
 
451
456
  `;
452
- for (const res of targets) {
457
+ const sendTo = (res, message) => {
458
+ try {
459
+ res.write(line(message));
460
+ } catch {
461
+ apps.delete(res);
462
+ panels.delete(res);
463
+ }
464
+ };
465
+ const send = (targets, message) => {
466
+ const text = line(message);
467
+ for (const res of [...targets]) {
453
468
  try {
454
- res.write(line);
469
+ res.write(text);
455
470
  } catch {
456
- targets.delete(res);
471
+ apps.delete(res);
472
+ panels.delete(res);
457
473
  }
458
474
  }
459
475
  };
460
- const route = (message) => {
476
+ const appList = () => [...apps.values()];
477
+ const roster = () => ({ [MARKER]: true, version: PROTOCOL, type: "relay", payload: { apps: apps.size, list: appList() } });
478
+ const streamOf = (id) => {
479
+ for (const [res, app] of apps) if (app.id === id) return res;
480
+ return null;
481
+ };
482
+ const route = (message, from) => {
461
483
  if (!message || typeof message !== "object") return;
462
484
  const m = message;
463
- if (m.__rerenderLensCmd === true) send(apps, m);
464
- else if (m.__rerenderLens === true || m.__rerenderLensReply === true) send(panels, m);
485
+ if (m.__rerenderLensCmd === true) {
486
+ const target = typeof m.app === "string" ? streamOf(m.app) : null;
487
+ if (target) sendTo(target, m);
488
+ else send(apps.keys(), m);
489
+ } else if (m.__rerenderLens === true || m.__rerenderLensReply === true) {
490
+ send(panels, from ? { ...m, app: from.id } : m);
491
+ }
492
+ };
493
+ const authorized = (req, u) => {
494
+ if (!token) return true;
495
+ const given = u.searchParams.get("token") || (typeof req.headers["x-rerender-lens-token"] === "string" ? req.headers["x-rerender-lens-token"] : "");
496
+ return given === token;
465
497
  };
466
498
  const server = http.createServer(async (req, res) => {
467
499
  cors(res);
@@ -473,18 +505,26 @@ function createRelayServer(options = {}) {
473
505
  }
474
506
  if (u.pathname === "/" && req.method === "GET") {
475
507
  res.statusCode = 302;
476
- res.setHeader("Location", `/panel.html?relay=${encodeURIComponent(url)}`);
508
+ res.setHeader("Location", `/panel.html?relay=${encodeURIComponent(connectUrl())}`);
477
509
  res.end();
478
510
  return;
479
511
  }
512
+ if (/^\/(events|message|status)$/.test(u.pathname) && !authorized(req, u)) {
513
+ res.statusCode = 401;
514
+ res.setHeader("Content-Type", "text/plain");
515
+ res.end("rerender-lens relay: this relay needs the token from the URL it printed (?token=...)");
516
+ return;
517
+ }
480
518
  if (u.pathname === "/events" && req.method === "GET") {
481
- const role = u.searchParams.get("role") === "app" ? apps : panels;
519
+ const isApp = u.searchParams.get("role") === "app";
482
520
  res.statusCode = 200;
483
521
  res.setHeader("Content-Type", "text/event-stream");
484
522
  res.setHeader("Cache-Control", "no-store");
485
523
  res.setHeader("Connection", "keep-alive");
486
524
  res.write(": connected\n\n");
487
- role.add(res);
525
+ const app = isApp ? { id: `a${nextAppId++}`, label: (u.searchParams.get("label") || req.headers.origin || "").slice(0, 120) || `app ${nextAppId - 1}` } : null;
526
+ if (app) apps.set(res, app);
527
+ else panels.add(res);
488
528
  const ping = setInterval(() => {
489
529
  try {
490
530
  res.write(": ping\n\n");
@@ -493,14 +533,15 @@ function createRelayServer(options = {}) {
493
533
  }, keepAliveMs);
494
534
  req.on("close", () => {
495
535
  clearInterval(ping);
496
- role.delete(res);
536
+ apps.delete(res);
537
+ panels.delete(res);
497
538
  });
498
- const count = () => ({ __rerenderLens: true, version: 2, type: "relay", payload: { apps: apps.size } });
499
- if (role === apps) {
500
- send(panels, count());
501
- req.on("close", () => send(panels, count()));
539
+ if (app) {
540
+ sendTo(res, { __rerenderLensRelay: true, version: PROTOCOL, app: app.id });
541
+ send(panels, roster());
542
+ req.on("close", () => send(panels, roster()));
502
543
  } else {
503
- send(/* @__PURE__ */ new Set([res]), count());
544
+ sendTo(res, roster());
504
545
  }
505
546
  return;
506
547
  }
@@ -512,7 +553,9 @@ function createRelayServer(options = {}) {
512
553
  if (u.pathname === "/message" && req.method === "POST") {
513
554
  try {
514
555
  const parsed = JSON.parse(await readBody(req) || "null");
515
- for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m);
556
+ const sender = u.searchParams.get("app");
557
+ const from = sender ? appList().find((a) => a.id === sender) ?? void 0 : void 0;
558
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) route(m, from);
516
559
  res.statusCode = 204;
517
560
  res.end();
518
561
  } catch (e) {
@@ -525,7 +568,7 @@ function createRelayServer(options = {}) {
525
568
  if (u.pathname === "/status" && req.method === "GET") {
526
569
  res.statusCode = 200;
527
570
  res.setHeader("Content-Type", "application/json");
528
- res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size }));
571
+ res.end(JSON.stringify({ ok: true, apps: apps.size, panels: panels.size, list: appList() }));
529
572
  return;
530
573
  }
531
574
  const file = u.pathname.replace(/^\//, "");
@@ -555,10 +598,12 @@ function createRelayServer(options = {}) {
555
598
  resolve({
556
599
  server,
557
600
  url,
601
+ connectUrl: connectUrl(),
558
602
  port,
559
603
  counts: () => ({ apps: apps.size, panels: panels.size }),
604
+ appList,
560
605
  close: () => new Promise((done) => {
561
- for (const res of [...apps, ...panels]) res.end();
606
+ for (const res of [...apps.keys(), ...panels]) res.end();
562
607
  apps.clear();
563
608
  panels.clear();
564
609
  server.close(() => done());
@@ -567,8 +612,6 @@ function createRelayServer(options = {}) {
567
612
  });
568
613
  });
569
614
  }
570
-
571
- // src/cli.ts
572
615
  var read = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
573
616
  function summaryOf(file) {
574
617
  const parsed = parseExport(read(file));
@@ -576,14 +619,20 @@ function summaryOf(file) {
576
619
  if (parsed.sessions.length && !parsed.reports.length) return parsed.sessions[parsed.sessions.length - 1];
577
620
  return summarizeReports(parsed.reports, { name: file.replace(/^.*[\\/]/, "").replace(/\.json$/, "") });
578
621
  }
622
+ var isLoopback = (host) => host === "127.0.0.1" || host === "localhost" || host === "::1";
579
623
  async function panelCommand(args) {
580
624
  const flag = (name) => {
581
625
  const i = args.indexOf(name);
582
626
  return i >= 0 ? args[i + 1] ?? "" : null;
583
627
  };
584
- const relay = await createRelayServer({ port: Number(flag("--port") || 4141), host: flag("--host") || "127.0.0.1" });
585
- console.log(`rerender-lens panel: ${relay.url}/`);
586
- console.log(`in the app: createDevtoolsNotifier({ relay: '${relay.url}' }) or window.__RERENDER_LENS_RELAY__ = '${relay.url}'`);
628
+ const host = flag("--host") || "127.0.0.1";
629
+ const given = flag("--token");
630
+ const token = args.includes("--no-token") ? null : given || (isLoopback(host) ? null : randomToken());
631
+ const relay = await createRelayServer({ port: Number(flag("--port") || 4141), host, token: token ?? void 0 });
632
+ console.log(`rerender-lens panel: ${relay.connectUrl}/`);
633
+ console.log(`in the app: createDevtoolsNotifier({ relay: '${relay.connectUrl}' }) or window.__RERENDER_LENS_RELAY__ = '${relay.connectUrl}'`);
634
+ if (token) console.log("The token in that URL is required: reports carry your app's prop, state and context values.");
635
+ else if (!isLoopback(host)) console.log(`WARNING: ${host} is reachable by other machines and --no-token was given: anyone who can reach this port can read every report.`);
587
636
  console.log("Ctrl+C to stop.");
588
637
  return new Promise((resolve) => {
589
638
  const stop = () => void relay.close().then(() => resolve(0));
@@ -592,6 +641,9 @@ async function panelCommand(args) {
592
641
  relay.server.once("close", () => resolve(0));
593
642
  });
594
643
  }
644
+ function randomToken() {
645
+ return crypto.randomBytes(16).toString("base64url");
646
+ }
595
647
  function main(argv) {
596
648
  const [cmd, ...args] = argv;
597
649
  if (cmd === "panel") return panelCommand(args).catch((e) => (console.error(e.message), 1));