linear-grab 0.20.1 → 0.21.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/README.md +1 -1
- package/bin/linear-grab-bridge.mjs +162 -6
- package/dist/index.global.js +30 -29
- package/dist/linear-grab.js +3522 -3465
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ export default function RootLayout({ children }) {
|
|
|
26
26
|
{children}
|
|
27
27
|
{process.env.NODE_ENV === "development" && (
|
|
28
28
|
<Script
|
|
29
|
-
src="https://cdn.jsdelivr.net/gh/ahmedbanihanibh/linear-grab@v0.
|
|
29
|
+
src="https://cdn.jsdelivr.net/gh/ahmedbanihanibh/linear-grab@v0.21.0/dist/index.global.js"
|
|
30
30
|
crossOrigin="anonymous"
|
|
31
31
|
strategy="afterInteractive"
|
|
32
32
|
/>
|
|
@@ -16,7 +16,7 @@ import { createServer } from 'node:http';
|
|
|
16
16
|
import { spawn, execFile } from 'node:child_process';
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
import { createHash } from 'node:crypto';
|
|
19
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ const flag = (name, fallback) => {
|
|
|
28
28
|
const PORT = Number(flag('--port', '4577'));
|
|
29
29
|
const DIR = flag('--dir', process.cwd());
|
|
30
30
|
const CLAUDE_BIN = flag('--claude', 'claude');
|
|
31
|
-
const VERSION = '0.
|
|
31
|
+
const VERSION = '0.24.1';
|
|
32
32
|
|
|
33
33
|
/** Best-effort command runner (git/gh introspection). Never throws. */
|
|
34
34
|
function run(cmd, args, cwd = DIR) {
|
|
@@ -629,6 +629,142 @@ createServer(async (req, res) => {
|
|
|
629
629
|
);
|
|
630
630
|
return json(res, 200, { statuses, previews });
|
|
631
631
|
}
|
|
632
|
+
// ---- react-scan telemetry -------------------------------------------
|
|
633
|
+
// Browser pushes render/interaction events; they land in an append-only
|
|
634
|
+
// NDJSON file agents read directly (.lineargrab/scan.ndjson at the repo).
|
|
635
|
+
if (req.method === 'POST' && url.pathname === '/scan/events') {
|
|
636
|
+
const body = await readBody(req);
|
|
637
|
+
const events = Array.isArray(body.events) ? body.events : [body];
|
|
638
|
+
const dir = join(DIR, '.lineargrab');
|
|
639
|
+
try {
|
|
640
|
+
mkdirSync(dir, { recursive: true });
|
|
641
|
+
// Self-ignoring: telemetry must never pollute commits.
|
|
642
|
+
writeFileSync(join(dir, '.gitignore'), '*\n', { flag: 'wx' });
|
|
643
|
+
} catch {
|
|
644
|
+
/* exists */
|
|
645
|
+
}
|
|
646
|
+
const file = join(dir, 'scan.ndjson');
|
|
647
|
+
const lines =
|
|
648
|
+
events
|
|
649
|
+
.slice(0, 200)
|
|
650
|
+
.map((e) => JSON.stringify({ at: Date.now(), ...e }))
|
|
651
|
+
.join('\n') + '\n';
|
|
652
|
+
try {
|
|
653
|
+
appendFileSync(file, lines);
|
|
654
|
+
// Rotation: cap ~2MB by keeping the newest half.
|
|
655
|
+
const size = statSync(file).size;
|
|
656
|
+
if (size > 2_000_000) {
|
|
657
|
+
const keep = readFileSync(file, 'utf8');
|
|
658
|
+
writeFileSync(file, keep.slice(Math.floor(keep.length / 2)).replace(/^[^\n]*\n/, ''));
|
|
659
|
+
}
|
|
660
|
+
} catch {
|
|
661
|
+
/* disk issues — telemetry must never error the page */
|
|
662
|
+
}
|
|
663
|
+
return json(res, 200, { ok: true });
|
|
664
|
+
}
|
|
665
|
+
// Aggregated "what's slow right now" — a convenience view over the file.
|
|
666
|
+
if (req.method === 'GET' && url.pathname === '/scan/report') {
|
|
667
|
+
const windowMs = Number(url.searchParams.get('window') ?? 120_000);
|
|
668
|
+
let raw = '';
|
|
669
|
+
try {
|
|
670
|
+
raw = readFileSync(join(DIR, '.lineargrab', 'scan.ndjson'), 'utf8');
|
|
671
|
+
} catch {
|
|
672
|
+
return json(res, 200, { report: 'No scan telemetry yet — is react-scan running with the bridge enabled?', components: [] });
|
|
673
|
+
}
|
|
674
|
+
const cutoff = Date.now() - windowMs;
|
|
675
|
+
const byComponent = new Map();
|
|
676
|
+
const interactions = [];
|
|
677
|
+
let bursts = 0;
|
|
678
|
+
let worstFps = 60;
|
|
679
|
+
const pages = new Set();
|
|
680
|
+
for (const line of raw.split('\n')) {
|
|
681
|
+
if (!line) continue;
|
|
682
|
+
let e;
|
|
683
|
+
try {
|
|
684
|
+
e = JSON.parse(line);
|
|
685
|
+
} catch {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if ((e.at ?? 0) < cutoff) continue;
|
|
689
|
+
if (e.page) pages.add(e.page);
|
|
690
|
+
if (e.kind === 'interaction') interactions.push(e);
|
|
691
|
+
if (e.kind === 'long-render') {
|
|
692
|
+
bursts += 1;
|
|
693
|
+
if (typeof e.fps === 'number' && e.fps > 0) worstFps = Math.min(worstFps, e.fps);
|
|
694
|
+
}
|
|
695
|
+
for (const c of e.components ?? []) {
|
|
696
|
+
const cur = byComponent.get(c.name) ?? {
|
|
697
|
+
name: c.name,
|
|
698
|
+
renders: 0,
|
|
699
|
+
selfTime: 0,
|
|
700
|
+
source: c.source ?? null,
|
|
701
|
+
element: c.element ?? null,
|
|
702
|
+
pages: new Set(),
|
|
703
|
+
memoizableRenders: 0,
|
|
704
|
+
changes: {},
|
|
705
|
+
};
|
|
706
|
+
cur.renders += c.renders ?? 1;
|
|
707
|
+
cur.selfTime += c.selfTime ?? 0;
|
|
708
|
+
cur.source ??= c.source ?? null;
|
|
709
|
+
cur.element ??= c.element ?? null;
|
|
710
|
+
if (e.page) cur.pages.add(e.page);
|
|
711
|
+
if (c.memoizable) cur.memoizableRenders += c.renders ?? 1;
|
|
712
|
+
for (const ch of c.changes ?? []) cur.changes[ch] = (cur.changes[ch] ?? 0) + 1;
|
|
713
|
+
byComponent.set(c.name, cur);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const components = [...byComponent.values()]
|
|
717
|
+
.sort((a, b) => b.selfTime - a.selfTime)
|
|
718
|
+
.slice(0, 25)
|
|
719
|
+
.map((c) => ({ ...c, pages: [...c.pages] }));
|
|
720
|
+
const md = [
|
|
721
|
+
`# react-scan report (last ${Math.round(windowMs / 1000)}s)`,
|
|
722
|
+
'',
|
|
723
|
+
`Target 60 FPS — worst observed: ${worstFps} FPS across ${bursts} slow-frame burst(s).` +
|
|
724
|
+
(pages.size ? ` Pages: ${[...pages].join(', ')}` : ''),
|
|
725
|
+
'',
|
|
726
|
+
'## Components (fix top-down; each line = who, where, and WHY it re-rendered)',
|
|
727
|
+
...components.map(
|
|
728
|
+
(c) =>
|
|
729
|
+
`- **${c.name}** — ${c.renders} renders · ${c.selfTime.toFixed(1)}ms self` +
|
|
730
|
+
(c.memoizableRenders
|
|
731
|
+
? ` · ${c.memoizableRenders} renders with ZERO changes (memo() candidate)`
|
|
732
|
+
: '') +
|
|
733
|
+
(c.source ? ` · src \`${c.source}\`` : '') +
|
|
734
|
+
(c.element ? ` · el \`${c.element}\`` : '') +
|
|
735
|
+
(c.pages.length ? ` · on ${c.pages.join(', ')}` : '') +
|
|
736
|
+
(Object.keys(c.changes).length
|
|
737
|
+
? ` · causes: ${Object.entries(c.changes)
|
|
738
|
+
.sort((a, b) => b[1] - a[1])
|
|
739
|
+
.slice(0, 4)
|
|
740
|
+
.map(([k, v]) => `${k}×${v}`)
|
|
741
|
+
.join(', ')} — (fn)=unstable callback→useCallback, (ref)=new identity→useMemo/hoist`
|
|
742
|
+
: ''),
|
|
743
|
+
),
|
|
744
|
+
'',
|
|
745
|
+
'## Recent interactions (what the user did → what re-rendered because of it)',
|
|
746
|
+
...interactions.slice(-10).flatMap((i) => [
|
|
747
|
+
`- ${i.type ?? '?'} on **${i.target ?? '?'}**${i.page ? ` (${i.page})` : ''} — ${Math.round(i.duration ?? 0)}ms` +
|
|
748
|
+
(i.slow ? ' ⚠ SLOW (>150ms INP)' : ''),
|
|
749
|
+
...(i.components ?? [])
|
|
750
|
+
.slice(0, 4)
|
|
751
|
+
.map(
|
|
752
|
+
(c) =>
|
|
753
|
+
` ↳ ${c.name} ×${c.renders} · ${(c.selfTime ?? 0).toFixed(1)}ms` +
|
|
754
|
+
(c.changes?.length ? ` · ${c.changes.join(', ')}` : '') +
|
|
755
|
+
(c.source ? ` · \`${c.source}\`` : ''),
|
|
756
|
+
),
|
|
757
|
+
]),
|
|
758
|
+
].join('\n');
|
|
759
|
+
return json(res, 200, {
|
|
760
|
+
report: md,
|
|
761
|
+
worstFps,
|
|
762
|
+
bursts,
|
|
763
|
+
pages: [...pages],
|
|
764
|
+
components,
|
|
765
|
+
interactions: interactions.slice(-20),
|
|
766
|
+
});
|
|
767
|
+
}
|
|
632
768
|
// Reset the staging branch: delete + recreate from the default branch.
|
|
633
769
|
if (req.method === 'POST' && url.pathname === '/branch/reset') {
|
|
634
770
|
const body = await readBody(req);
|
|
@@ -780,9 +916,29 @@ createServer(async (req, res) => {
|
|
|
780
916
|
json(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
781
917
|
}
|
|
782
918
|
}).listen(PORT, '127.0.0.1', () => {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
919
|
+
const B = '\x1b[1m';
|
|
920
|
+
const D = '\x1b[2m';
|
|
921
|
+
const C = '\x1b[36m';
|
|
922
|
+
const G = '\x1b[32m';
|
|
923
|
+
const R = '\x1b[0m';
|
|
924
|
+
console.log('');
|
|
925
|
+
console.log(`${B}◆ linear-grab bridge${R} ${D}v${VERSION}${R}`);
|
|
926
|
+
console.log(`${D}──────────────────────────────────────────────────${R}`);
|
|
927
|
+
console.log(` repo ${C}${DIR}${R}`);
|
|
928
|
+
console.log(` listen ${C}http://127.0.0.1:${PORT}${R} ${D}(localhost only)${R}`);
|
|
929
|
+
console.log(` agent ${C}${CLAUDE_BIN} -p${R} ${D}(interactive stream-json)${R}`);
|
|
930
|
+
console.log('');
|
|
931
|
+
console.log(`${B}react-scan telemetry${R} ${D}(when the app loads react-scan-banihani)${R}`);
|
|
932
|
+
console.log(` events ${G}.lineargrab/scan.ndjson${R} ${D}— newest last, gitignored${R}`);
|
|
933
|
+
console.log(` report ${G}curl -s http://127.0.0.1:${PORT}/scan/report${R}`);
|
|
934
|
+
console.log('');
|
|
935
|
+
console.log(`${B}point Claude Code at the render logs${R} — paste one of these:`);
|
|
936
|
+
console.log(` ${D}»${R} Check the live render telemetry: read the newest lines of`);
|
|
937
|
+
console.log(` .lineargrab/scan.ndjson and summarize the slow components.`);
|
|
938
|
+
console.log(` ${D}»${R} curl -s http://127.0.0.1:${PORT}/scan/report ${D}(aggregated view)${R}`);
|
|
939
|
+
console.log(` ${D}tip: add a line to your repo's AGENTS.md/CLAUDE.md so agents find it`);
|
|
940
|
+
console.log(` on their own: "Live render telemetry: .lineargrab/scan.ndjson`);
|
|
941
|
+
console.log(` (react-scan via linear-grab bridge); aggregate: GET /scan/report"${R}`);
|
|
942
|
+
console.log(`${D}──────────────────────────────────────────────────${R}`);
|
|
787
943
|
loadHistory();
|
|
788
944
|
});
|