@0x1f320.sh/why-did-you-render-mcp 1.0.0-dev.5 → 1.0.0-dev.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 +1 -0
- package/dist/client/index.cjs +47 -0
- package/dist/client/index.js +47 -0
- package/dist/server/index.js +133 -9
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -126,6 +126,7 @@ Browser (project-b) ──┤
|
|
|
126
126
|
- **Multiple MCP instances** can run simultaneously. Only the first instance starts the WebSocket server; others gracefully skip. All instances share the same JSONL data directory.
|
|
127
127
|
- **Multi-project support** — Each project is identified by `location.origin`. Render data is stored in per-project JSONL files.
|
|
128
128
|
- **No daemon required** — Each MCP instance is independent. The WebSocket server is opportunistically claimed by whichever instance starts first.
|
|
129
|
+
- **Value dictionary deduplication** — Render reports often repeat the same `prevValue`/`nextValue` objects across thousands of entries. Each JSONL file stores a content-addressed dictionary on its first line, mapping xxhash-wasm hashes to unique values. Render lines reference them via `@@ref:<hash>` sentinels instead of inlining the full object, dramatically reducing file size. Reads hydrate refs transparently.
|
|
129
130
|
|
|
130
131
|
## Configuration
|
|
131
132
|
|
package/dist/client/index.cjs
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
//#region src/client/utils/describe-value.ts
|
|
3
3
|
const MAX_DEPTH = 8;
|
|
4
|
+
const REACT_ELEMENT_SYMBOL = Symbol.for("react.element");
|
|
5
|
+
const REACT_TRANSITIONAL_ELEMENT_SYMBOL = Symbol.for("react.transitional.element");
|
|
6
|
+
const REACT_MEMO_TYPE = Symbol.for("react.memo");
|
|
7
|
+
const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
|
|
8
|
+
function isReactElement(value) {
|
|
9
|
+
if (typeof value !== "object" || value === null) return false;
|
|
10
|
+
const v = value;
|
|
11
|
+
return v.$$typeof === REACT_ELEMENT_SYMBOL || v.$$typeof === REACT_TRANSITIONAL_ELEMENT_SYMBOL || v.$$typeof === 60103;
|
|
12
|
+
}
|
|
13
|
+
function resolveComponentInfo(type) {
|
|
14
|
+
let memo = false;
|
|
15
|
+
let forwardRef = false;
|
|
16
|
+
let current = type;
|
|
17
|
+
for (let i = 0; i < 5; i++) {
|
|
18
|
+
if (typeof current !== "object" || current === null) break;
|
|
19
|
+
const wrapper = current;
|
|
20
|
+
if (wrapper.$$typeof === REACT_MEMO_TYPE) {
|
|
21
|
+
memo = true;
|
|
22
|
+
current = wrapper.type;
|
|
23
|
+
} else if (wrapper.$$typeof === REACT_FORWARD_REF_TYPE) {
|
|
24
|
+
forwardRef = true;
|
|
25
|
+
current = wrapper.render;
|
|
26
|
+
} else break;
|
|
27
|
+
}
|
|
28
|
+
let name = "Unknown";
|
|
29
|
+
if (typeof current === "string") name = current;
|
|
30
|
+
else if (typeof current === "function") name = current.displayName || current.name || "Anonymous";
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
memo,
|
|
34
|
+
forwardRef
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function serializeReactElement(el, seen, depth) {
|
|
38
|
+
const component = resolveComponentInfo(el.type);
|
|
39
|
+
const props = {};
|
|
40
|
+
if (el.props && typeof el.props === "object") for (const key of Object.keys(el.props)) {
|
|
41
|
+
if (key === "children") continue;
|
|
42
|
+
props[key] = serialize(el.props[key], seen, depth + 1);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: "react-node",
|
|
46
|
+
component,
|
|
47
|
+
props
|
|
48
|
+
};
|
|
49
|
+
}
|
|
4
50
|
function serialize(value, seen, depth) {
|
|
5
51
|
if (value === null) return null;
|
|
6
52
|
if (value === void 0) return null;
|
|
@@ -21,6 +67,7 @@ function serialize(value, seen, depth) {
|
|
|
21
67
|
if (seen.has(value)) return "[Circular]";
|
|
22
68
|
if (depth >= MAX_DEPTH) return "[MaxDepth]";
|
|
23
69
|
seen.add(value);
|
|
70
|
+
if (isReactElement(value)) return serializeReactElement(value, seen, depth);
|
|
24
71
|
if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1));
|
|
25
72
|
const ctorName = Object.getPrototypeOf(value)?.constructor?.name;
|
|
26
73
|
if (ctorName && ctorName !== "Object") {
|
package/dist/client/index.js
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
//#region src/client/utils/describe-value.ts
|
|
2
2
|
const MAX_DEPTH = 8;
|
|
3
|
+
const REACT_ELEMENT_SYMBOL = Symbol.for("react.element");
|
|
4
|
+
const REACT_TRANSITIONAL_ELEMENT_SYMBOL = Symbol.for("react.transitional.element");
|
|
5
|
+
const REACT_MEMO_TYPE = Symbol.for("react.memo");
|
|
6
|
+
const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
|
|
7
|
+
function isReactElement(value) {
|
|
8
|
+
if (typeof value !== "object" || value === null) return false;
|
|
9
|
+
const v = value;
|
|
10
|
+
return v.$$typeof === REACT_ELEMENT_SYMBOL || v.$$typeof === REACT_TRANSITIONAL_ELEMENT_SYMBOL || v.$$typeof === 60103;
|
|
11
|
+
}
|
|
12
|
+
function resolveComponentInfo(type) {
|
|
13
|
+
let memo = false;
|
|
14
|
+
let forwardRef = false;
|
|
15
|
+
let current = type;
|
|
16
|
+
for (let i = 0; i < 5; i++) {
|
|
17
|
+
if (typeof current !== "object" || current === null) break;
|
|
18
|
+
const wrapper = current;
|
|
19
|
+
if (wrapper.$$typeof === REACT_MEMO_TYPE) {
|
|
20
|
+
memo = true;
|
|
21
|
+
current = wrapper.type;
|
|
22
|
+
} else if (wrapper.$$typeof === REACT_FORWARD_REF_TYPE) {
|
|
23
|
+
forwardRef = true;
|
|
24
|
+
current = wrapper.render;
|
|
25
|
+
} else break;
|
|
26
|
+
}
|
|
27
|
+
let name = "Unknown";
|
|
28
|
+
if (typeof current === "string") name = current;
|
|
29
|
+
else if (typeof current === "function") name = current.displayName || current.name || "Anonymous";
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
memo,
|
|
33
|
+
forwardRef
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function serializeReactElement(el, seen, depth) {
|
|
37
|
+
const component = resolveComponentInfo(el.type);
|
|
38
|
+
const props = {};
|
|
39
|
+
if (el.props && typeof el.props === "object") for (const key of Object.keys(el.props)) {
|
|
40
|
+
if (key === "children") continue;
|
|
41
|
+
props[key] = serialize(el.props[key], seen, depth + 1);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
type: "react-node",
|
|
45
|
+
component,
|
|
46
|
+
props
|
|
47
|
+
};
|
|
48
|
+
}
|
|
3
49
|
function serialize(value, seen, depth) {
|
|
4
50
|
if (value === null) return null;
|
|
5
51
|
if (value === void 0) return null;
|
|
@@ -20,6 +66,7 @@ function serialize(value, seen, depth) {
|
|
|
20
66
|
if (seen.has(value)) return "[Circular]";
|
|
21
67
|
if (depth >= MAX_DEPTH) return "[MaxDepth]";
|
|
22
68
|
seen.add(value);
|
|
69
|
+
if (isReactElement(value)) return serializeReactElement(value, seen, depth);
|
|
23
70
|
if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1));
|
|
24
71
|
const ctorName = Object.getPrototypeOf(value)?.constructor?.name;
|
|
25
72
|
if (ctorName && ctorName !== "Object") {
|
package/dist/server/index.js
CHANGED
|
@@ -2,14 +2,107 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import xxhash from "xxhash-wasm";
|
|
8
9
|
import { WebSocketServer } from "ws";
|
|
10
|
+
//#region src/server/store/utils/value-dict.ts
|
|
11
|
+
const DICT_KEY = "@@dict";
|
|
12
|
+
const REF_PREFIX = "@@ref:";
|
|
13
|
+
let h64ToString;
|
|
14
|
+
const ready = xxhash().then((api) => {
|
|
15
|
+
h64ToString = api.h64ToString;
|
|
16
|
+
});
|
|
17
|
+
function ensureReady() {
|
|
18
|
+
return ready;
|
|
19
|
+
}
|
|
20
|
+
function hashValue(value) {
|
|
21
|
+
return h64ToString(JSON.stringify(value));
|
|
22
|
+
}
|
|
23
|
+
function shouldDehydrate(value) {
|
|
24
|
+
return typeof value === "object" && value !== null;
|
|
25
|
+
}
|
|
26
|
+
function dehydrateDiffs(diffs, dict) {
|
|
27
|
+
if (!diffs) return false;
|
|
28
|
+
return diffs.map((d) => {
|
|
29
|
+
let { prevValue, nextValue } = d;
|
|
30
|
+
if (shouldDehydrate(prevValue)) {
|
|
31
|
+
const hash = hashValue(prevValue);
|
|
32
|
+
dict[hash] ??= prevValue;
|
|
33
|
+
prevValue = `${REF_PREFIX}${hash}`;
|
|
34
|
+
}
|
|
35
|
+
if (shouldDehydrate(nextValue)) {
|
|
36
|
+
const hash = hashValue(nextValue);
|
|
37
|
+
dict[hash] ??= nextValue;
|
|
38
|
+
nextValue = `${REF_PREFIX}${hash}`;
|
|
39
|
+
}
|
|
40
|
+
return prevValue === d.prevValue && nextValue === d.nextValue ? d : {
|
|
41
|
+
...d,
|
|
42
|
+
prevValue,
|
|
43
|
+
nextValue
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function dehydrate(render, dict) {
|
|
48
|
+
const { propsDifferences, stateDifferences, hookDifferences } = render.reason;
|
|
49
|
+
const newProps = dehydrateDiffs(propsDifferences, dict);
|
|
50
|
+
const newState = dehydrateDiffs(stateDifferences, dict);
|
|
51
|
+
const newHooks = dehydrateDiffs(hookDifferences, dict);
|
|
52
|
+
if (newProps === propsDifferences && newState === stateDifferences && newHooks === hookDifferences) return render;
|
|
53
|
+
return {
|
|
54
|
+
...render,
|
|
55
|
+
reason: {
|
|
56
|
+
propsDifferences: newProps,
|
|
57
|
+
stateDifferences: newState,
|
|
58
|
+
hookDifferences: newHooks
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function hydrateDiffs(diffs, dict) {
|
|
63
|
+
if (!diffs) return false;
|
|
64
|
+
return diffs.map((d) => {
|
|
65
|
+
let { prevValue, nextValue } = d;
|
|
66
|
+
if (typeof prevValue === "string" && prevValue.startsWith(REF_PREFIX)) prevValue = dict[prevValue.slice(6)] ?? prevValue;
|
|
67
|
+
if (typeof nextValue === "string" && nextValue.startsWith(REF_PREFIX)) nextValue = dict[nextValue.slice(6)] ?? nextValue;
|
|
68
|
+
return prevValue === d.prevValue && nextValue === d.nextValue ? d : {
|
|
69
|
+
...d,
|
|
70
|
+
prevValue,
|
|
71
|
+
nextValue
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function hydrate(render, dict) {
|
|
76
|
+
const { propsDifferences, stateDifferences, hookDifferences } = render.reason;
|
|
77
|
+
const newProps = hydrateDiffs(propsDifferences, dict);
|
|
78
|
+
const newState = hydrateDiffs(stateDifferences, dict);
|
|
79
|
+
const newHooks = hydrateDiffs(hookDifferences, dict);
|
|
80
|
+
if (newProps === propsDifferences && newState === stateDifferences && newHooks === hookDifferences) return render;
|
|
81
|
+
return {
|
|
82
|
+
...render,
|
|
83
|
+
reason: {
|
|
84
|
+
propsDifferences: newProps,
|
|
85
|
+
stateDifferences: newState,
|
|
86
|
+
hookDifferences: newHooks
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
9
91
|
//#region src/server/store/utils/read-jsonl.ts
|
|
10
92
|
function readJsonl(file) {
|
|
11
93
|
if (!existsSync(file)) return [];
|
|
12
|
-
|
|
94
|
+
const lines = readFileSync(file, "utf-8").split("\n").filter(Boolean);
|
|
95
|
+
if (lines.length === 0) return [];
|
|
96
|
+
let dict;
|
|
97
|
+
let startIdx = 0;
|
|
98
|
+
const first = JSON.parse(lines[0]);
|
|
99
|
+
if ("@@dict" in first) {
|
|
100
|
+
dict = first[DICT_KEY];
|
|
101
|
+
startIdx = 1;
|
|
102
|
+
}
|
|
103
|
+
const renders = lines.slice(startIdx).map((l) => JSON.parse(l));
|
|
104
|
+
if (!dict) return renders;
|
|
105
|
+
return renders.map((r) => hydrate(r, dict));
|
|
13
106
|
}
|
|
14
107
|
//#endregion
|
|
15
108
|
//#region src/server/store/utils/sanitize-project-id.ts
|
|
@@ -34,6 +127,7 @@ var RenderStore = class {
|
|
|
34
127
|
dir;
|
|
35
128
|
buffers = /* @__PURE__ */ new Map();
|
|
36
129
|
timers = /* @__PURE__ */ new Map();
|
|
130
|
+
dicts = /* @__PURE__ */ new Map();
|
|
37
131
|
constructor(dir) {
|
|
38
132
|
this.dir = dir ?? join(homedir(), ".wdyr-mcp", "renders");
|
|
39
133
|
mkdirSync(this.dir, { recursive: true });
|
|
@@ -52,7 +146,14 @@ var RenderStore = class {
|
|
|
52
146
|
buf.push(stored);
|
|
53
147
|
const existing = this.timers.get(projectId);
|
|
54
148
|
if (existing) clearTimeout(existing);
|
|
55
|
-
this.timers.set(projectId, setTimeout(() =>
|
|
149
|
+
this.timers.set(projectId, setTimeout(() => {
|
|
150
|
+
this.flushAsync(projectId).catch((err) => console.error(`[wdyr-mcp] flush error for ${projectId}:`, err));
|
|
151
|
+
}, FLUSH_DELAY_MS));
|
|
152
|
+
}
|
|
153
|
+
async flushAsync(projectId) {
|
|
154
|
+
await ensureReady();
|
|
155
|
+
if (projectId) this.flushProject(projectId);
|
|
156
|
+
else for (const id of this.buffers.keys()) this.flushProject(id);
|
|
56
157
|
}
|
|
57
158
|
flush(projectId) {
|
|
58
159
|
if (projectId) this.flushProject(projectId);
|
|
@@ -61,8 +162,17 @@ var RenderStore = class {
|
|
|
61
162
|
flushProject(projectId) {
|
|
62
163
|
const buf = this.buffers.get(projectId);
|
|
63
164
|
if (!buf || buf.length === 0) return;
|
|
64
|
-
|
|
65
|
-
|
|
165
|
+
let dict = this.dicts.get(projectId);
|
|
166
|
+
if (!dict) {
|
|
167
|
+
dict = {};
|
|
168
|
+
this.dicts.set(projectId, dict);
|
|
169
|
+
}
|
|
170
|
+
const dehydrated = buf.map((r) => dehydrate(r, dict));
|
|
171
|
+
const file = this.projectFile(projectId);
|
|
172
|
+
const existingLines = this.readDataLines(file);
|
|
173
|
+
const newLines = dehydrated.map((r) => JSON.stringify(r));
|
|
174
|
+
const allDataLines = [...existingLines, ...newLines];
|
|
175
|
+
writeFileSync(file, `${(Object.keys(dict).length > 0 ? [JSON.stringify({ [DICT_KEY]: dict }), ...allDataLines] : allDataLines).join("\n")}\n`);
|
|
66
176
|
buf.length = 0;
|
|
67
177
|
const timer = this.timers.get(projectId);
|
|
68
178
|
if (timer) {
|
|
@@ -70,6 +180,14 @@ var RenderStore = class {
|
|
|
70
180
|
this.timers.delete(projectId);
|
|
71
181
|
}
|
|
72
182
|
}
|
|
183
|
+
readDataLines(file) {
|
|
184
|
+
if (!existsSync(file)) return [];
|
|
185
|
+
return readFileSync(file, "utf-8").split("\n").filter((line) => {
|
|
186
|
+
if (!line) return false;
|
|
187
|
+
if (line.startsWith(`{"@@dict"`)) return false;
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
73
191
|
getAllRenders(projectId) {
|
|
74
192
|
this.flush(projectId);
|
|
75
193
|
if (projectId) return readJsonl(this.projectFile(projectId)).map(toResult);
|
|
@@ -81,6 +199,7 @@ var RenderStore = class {
|
|
|
81
199
|
clearRenders(projectId) {
|
|
82
200
|
if (projectId) {
|
|
83
201
|
this.buffers.delete(projectId);
|
|
202
|
+
this.dicts.delete(projectId);
|
|
84
203
|
const timer = this.timers.get(projectId);
|
|
85
204
|
if (timer) {
|
|
86
205
|
clearTimeout(timer);
|
|
@@ -92,6 +211,7 @@ var RenderStore = class {
|
|
|
92
211
|
for (const [id, timer] of this.timers) clearTimeout(timer);
|
|
93
212
|
this.buffers.clear();
|
|
94
213
|
this.timers.clear();
|
|
214
|
+
this.dicts.clear();
|
|
95
215
|
for (const f of this.jsonlFiles()) unlinkSync(join(this.dir, f));
|
|
96
216
|
}
|
|
97
217
|
}
|
|
@@ -99,10 +219,14 @@ var RenderStore = class {
|
|
|
99
219
|
this.flush();
|
|
100
220
|
const projects = /* @__PURE__ */ new Set();
|
|
101
221
|
for (const f of this.jsonlFiles()) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
222
|
+
const lines = readFileSync(join(this.dir, f), "utf-8").split("\n");
|
|
223
|
+
for (const line of lines) {
|
|
224
|
+
if (!line) continue;
|
|
225
|
+
const parsed = JSON.parse(line);
|
|
226
|
+
if ("@@dict" in parsed) continue;
|
|
227
|
+
projects.add(parsed.projectId);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
106
230
|
}
|
|
107
231
|
return [...projects];
|
|
108
232
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0x1f320.sh/why-did-you-render-mcp",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server that collects why-did-you-render data from browser and exposes it to coding agents",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
52
52
|
"ws": "^8.18.0",
|
|
53
|
+
"xxhash-wasm": "^1.1.0",
|
|
53
54
|
"zod": "^3.24.4"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|