@0x1f320.sh/why-did-you-render-mcp 1.0.0-dev.9 → 1.0.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/dist/client/index.cjs +1 -230
- package/dist/client/index.d.cts +3 -5
- package/dist/client/index.d.ts +3 -5
- package/dist/client/index.js +1 -229
- package/dist/server/index.js +11 -572
- package/package.json +10 -7
package/dist/client/index.cjs
CHANGED
|
@@ -1,230 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,
|
|
2
|
-
//#region src/client/utils/describe-value.ts
|
|
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
|
-
}
|
|
50
|
-
function serialize(value, seen, depth) {
|
|
51
|
-
if (value === null) return null;
|
|
52
|
-
if (value === void 0) return null;
|
|
53
|
-
if (typeof value === "function") return {
|
|
54
|
-
type: "function",
|
|
55
|
-
name: value.name || "anonymous"
|
|
56
|
-
};
|
|
57
|
-
if (typeof value === "boolean") return value;
|
|
58
|
-
if (typeof value === "number") {
|
|
59
|
-
if (Number.isNaN(value)) return "NaN";
|
|
60
|
-
if (!Number.isFinite(value)) return value > 0 ? "Infinity" : "-Infinity";
|
|
61
|
-
if (Object.is(value, -0)) return "-0";
|
|
62
|
-
return value;
|
|
63
|
-
}
|
|
64
|
-
if (typeof value === "string") return value;
|
|
65
|
-
if (typeof value === "bigint") return value.toString();
|
|
66
|
-
if (typeof value === "symbol") return value.toString();
|
|
67
|
-
if (seen.has(value)) return "[Circular]";
|
|
68
|
-
if (depth >= MAX_DEPTH) return "[MaxDepth]";
|
|
69
|
-
seen.add(value);
|
|
70
|
-
if (isReactElement(value)) return serializeReactElement(value, seen, depth);
|
|
71
|
-
if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1));
|
|
72
|
-
const ctorName = Object.getPrototypeOf(value)?.constructor?.name;
|
|
73
|
-
if (ctorName && ctorName !== "Object") {
|
|
74
|
-
if (value instanceof Date) return value.toISOString();
|
|
75
|
-
if (value instanceof RegExp) return String(value);
|
|
76
|
-
if (value instanceof Map) {
|
|
77
|
-
const entries = {};
|
|
78
|
-
for (const [k, v] of value.entries()) entries[String(k)] = serialize(v, seen, depth + 1);
|
|
79
|
-
return {
|
|
80
|
-
type: "Map",
|
|
81
|
-
entries
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
if (value instanceof Set) return {
|
|
85
|
-
type: "Set",
|
|
86
|
-
values: [...value].map((v) => serialize(v, seen, depth + 1))
|
|
87
|
-
};
|
|
88
|
-
if (value instanceof Promise) return "Promise";
|
|
89
|
-
if (value instanceof Error) return {
|
|
90
|
-
type: "Error",
|
|
91
|
-
name: value.name,
|
|
92
|
-
message: value.message
|
|
93
|
-
};
|
|
94
|
-
if (typeof Node !== "undefined" && value instanceof Node && value instanceof Element) {
|
|
95
|
-
const attrs = {};
|
|
96
|
-
for (const attr of value.attributes) attrs[attr.name] = attr.value;
|
|
97
|
-
return {
|
|
98
|
-
type: "dom",
|
|
99
|
-
tagName: value.tagName.toLowerCase(),
|
|
100
|
-
attrs
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
type: "class",
|
|
105
|
-
name: ctorName
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
const result = {};
|
|
109
|
-
for (const key of Object.keys(value)) result[key] = serialize(value[key], seen, depth + 1);
|
|
110
|
-
return result;
|
|
111
|
-
}
|
|
112
|
-
function describeValue(value) {
|
|
113
|
-
return serialize(value, /* @__PURE__ */ new WeakSet(), 0);
|
|
114
|
-
}
|
|
115
|
-
//#endregion
|
|
116
|
-
//#region src/client/utils/sanitize-differences.ts
|
|
117
|
-
function sanitizeDifferences(diffs) {
|
|
118
|
-
if (!Array.isArray(diffs)) return false;
|
|
119
|
-
return diffs.map((diff) => ({
|
|
120
|
-
pathString: diff.pathString,
|
|
121
|
-
diffType: diff.diffType,
|
|
122
|
-
prevValue: describeValue(diff.prevValue),
|
|
123
|
-
nextValue: describeValue(diff.nextValue)
|
|
124
|
-
}));
|
|
125
|
-
}
|
|
126
|
-
//#endregion
|
|
127
|
-
//#region src/client/utils/sanitize-reason.ts
|
|
128
|
-
function sanitizeReason(reason) {
|
|
129
|
-
return {
|
|
130
|
-
propsDifferences: sanitizeDifferences(reason.propsDifferences),
|
|
131
|
-
stateDifferences: sanitizeDifferences(reason.stateDifferences),
|
|
132
|
-
hookDifferences: sanitizeDifferences(reason.hookDifferences)
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
//#endregion
|
|
136
|
-
//#region src/client/index.ts
|
|
137
|
-
const DEFAULT_WS_URL = "ws://localhost:4649";
|
|
138
|
-
const PREFIX_STYLE = "color: #38bdf8; font-weight: bold";
|
|
139
|
-
const RESET_STYLE = "color: inherit; font-weight: normal";
|
|
140
|
-
function log(message) {
|
|
141
|
-
console.log(`%c[WDYR MCP]%c ${message}`, PREFIX_STYLE, RESET_STYLE);
|
|
142
|
-
}
|
|
143
|
-
function patchDevToolsHook(onCommit) {
|
|
144
|
-
if (!globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
145
|
-
supportsFiber: true,
|
|
146
|
-
inject() {},
|
|
147
|
-
onCommitFiberRoot() {},
|
|
148
|
-
onCommitFiberUnmount() {}
|
|
149
|
-
};
|
|
150
|
-
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
151
|
-
const original = hook.onCommitFiberRoot.bind(hook);
|
|
152
|
-
hook.onCommitFiberRoot = (...args) => {
|
|
153
|
-
onCommit();
|
|
154
|
-
return original(...args);
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function buildOptions(opts) {
|
|
158
|
-
const wsUrl = opts?.wsUrl ?? DEFAULT_WS_URL;
|
|
159
|
-
const projectId = opts?.projectId ?? globalThis.location?.origin ?? "default";
|
|
160
|
-
const MAX_QUEUE_SIZE = 1e3;
|
|
161
|
-
const BASE_DELAY = 1e3;
|
|
162
|
-
const MAX_DELAY = 3e4;
|
|
163
|
-
let ws = null;
|
|
164
|
-
let queue = [];
|
|
165
|
-
let commitId = 0;
|
|
166
|
-
let retryDelay = BASE_DELAY;
|
|
167
|
-
patchDevToolsHook(() => {
|
|
168
|
-
commitId++;
|
|
169
|
-
});
|
|
170
|
-
function connect() {
|
|
171
|
-
ws = new WebSocket(wsUrl);
|
|
172
|
-
ws.addEventListener("open", () => {
|
|
173
|
-
log(`Connected to ${wsUrl}`);
|
|
174
|
-
retryDelay = BASE_DELAY;
|
|
175
|
-
for (const msg of queue) ws?.send(JSON.stringify(msg));
|
|
176
|
-
queue = [];
|
|
177
|
-
});
|
|
178
|
-
ws.addEventListener("close", () => {
|
|
179
|
-
ws = null;
|
|
180
|
-
setTimeout(connect, retryDelay);
|
|
181
|
-
retryDelay = Math.min(retryDelay * 2, MAX_DELAY);
|
|
182
|
-
});
|
|
183
|
-
ws.addEventListener("error", () => {
|
|
184
|
-
log(`Connection failed (${wsUrl}). Retrying in ${retryDelay / 1e3}s...`);
|
|
185
|
-
ws?.close();
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
connect();
|
|
189
|
-
function send(msg) {
|
|
190
|
-
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
191
|
-
else {
|
|
192
|
-
if (queue.length >= MAX_QUEUE_SIZE) queue.shift();
|
|
193
|
-
queue.push(msg);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
let pendingBatch = null;
|
|
197
|
-
let flushScheduled = false;
|
|
198
|
-
function flushBatch() {
|
|
199
|
-
flushScheduled = false;
|
|
200
|
-
if (!pendingBatch || pendingBatch.reports.length === 0) return;
|
|
201
|
-
send({
|
|
202
|
-
type: "render-batch",
|
|
203
|
-
projectId,
|
|
204
|
-
commitId: pendingBatch.commitId,
|
|
205
|
-
payload: pendingBatch.reports
|
|
206
|
-
});
|
|
207
|
-
pendingBatch = null;
|
|
208
|
-
}
|
|
209
|
-
return { notifier(info) {
|
|
210
|
-
const report = {
|
|
211
|
-
displayName: info.displayName,
|
|
212
|
-
reason: sanitizeReason(info.reason),
|
|
213
|
-
hookName: info.hookName
|
|
214
|
-
};
|
|
215
|
-
if (pendingBatch && pendingBatch.commitId === commitId) pendingBatch.reports.push(report);
|
|
216
|
-
else {
|
|
217
|
-
if (pendingBatch) flushBatch();
|
|
218
|
-
pendingBatch = {
|
|
219
|
-
commitId,
|
|
220
|
-
reports: [report]
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
if (!flushScheduled) {
|
|
224
|
-
flushScheduled = true;
|
|
225
|
-
queueMicrotask(flushBatch);
|
|
226
|
-
}
|
|
227
|
-
} };
|
|
228
|
-
}
|
|
229
|
-
//#endregion
|
|
230
|
-
exports.buildOptions = buildOptions;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`socket.io-client`),l=require(`error-stack-parser`);l=s(l);let u=require(`@jridgewell/trace-mapping`);const d=new Map;function f(e){let t=d.get(e);if(t)return t;let n=(async()=>{try{let t=await fetch(`${e}.map`);return t.ok?new u.TraceMap(await t.json()):null}catch{return null}})();return d.set(e,n),n}async function p(e,t,n){let r=await f(e);if(!r)return{path:e,line:t};let i=(0,u.originalPositionFor)(r,{line:t,column:n});return i.source?{path:i.source,line:i.line??t}:{path:e,line:t}}const m=[`whyDidYouRender`,`react-dom`,`react.development`,`react.production`,`scheduler.`,`installHook`,`console.`],h=[`trackHookChanges`,`WDYRFunctionalComponent`,`Object.notifier`,`notifier`,`console.trace`],g=new Set(`renderWithHooks.mountIndeterminateComponent.updateFunctionComponent.updateForwardRef.updateMemoComponent.updateSimpleMemoComponent.beginWork.beginWork$1.completeWork.completeUnitOfWork.performUnitOfWork.runWithFiberInDEV.callComponentInDEV.workLoopSync.workLoopConcurrent.renderRootSync.renderRootConcurrent.performWorkOnRoot.performSyncWorkOnRoot.performConcurrentWorkOnRoot.commitRoot.commitRootImpl.commitMutationEffects.commitMutationEffectsOnFiber.commitLayoutEffects.commitLayoutEffectOnFiber.flushPassiveEffects.flushPassiveEffectsImpl.flushSyncWorkAcrossRoots_impl.processRootScheduleInMicrotask.scheduleUpdateOnFiber.ensureRootIsScheduled.react_stack_bottom_frame.dispatchSetState.dispatchReducerAction.dispatchAction.mountState.updateState.mountReducer.updateReducer.mountMemo.updateMemo.mountEffect.updateEffect.mountLayoutEffect.updateLayoutEffect.mountRef.updateRef.flushWork.performWorkUntilDeadline`.split(`.`));function _(e){return m.some(t=>e.includes(t))}function v(e){return h.some(t=>e===t)}function y(e){return e.replace(/WDYR$/,``)}function b(e){let t=e.startsWith(`new `)?e.slice(4):e,n=t.lastIndexOf(`.`);return y(n>=0?t.slice(n+1):t)}function x(e){return/^use[A-Z]/.test(e)}function S(e){let t;try{t=l.default.parse(e)}catch{return[]}let n=[];for(let e of t){let t=e.fileName??``;if(!t||_(t))continue;let r=e.functionName;if(!r||v(r))continue;let i=b(r);i&&(g.has(i)||n.push({type:x(i)?`hook`:`component`,name:i,file:t,line:e.lineNumber??0,column:e.columnNumber??0}))}return n}async function C(e){let t=S(e);return t.length===0?[]:Promise.all(t.map(async e=>({type:e.type,name:e.name,location:await p(e.file,e.line,e.column)})))}const w=Symbol.for(`react.element`),T=Symbol.for(`react.transitional.element`),E=Symbol.for(`react.memo`),D=Symbol.for(`react.forward_ref`);function O(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.$$typeof===w||t.$$typeof===T||t.$$typeof===60103}function k(e){let t=!1,n=!1,r=e;for(let e=0;e<5&&!(typeof r!=`object`||!r);e++){let e=r;if(e.$$typeof===E)t=!0,r=e.type;else if(e.$$typeof===D)n=!0,r=e.render;else break}let i=`Unknown`;return typeof r==`string`?i=r:typeof r==`function`&&(i=r.displayName||r.name||`Anonymous`),{name:i,memo:t,forwardRef:n}}function A(e,t,n){let r=k(e.type),i={};if(e.props&&typeof e.props==`object`)for(let r of Object.keys(e.props))r!==`children`&&(i[r]=j(e.props[r],t,n+1));return{type:`react-node`,component:r,props:i}}function j(e,t,n){if(e==null)return null;if(typeof e==`function`)return{type:`function`,name:e.name||`anonymous`};if(typeof e==`boolean`)return e;if(typeof e==`number`)return Number.isNaN(e)?`NaN`:Number.isFinite(e)?Object.is(e,-0)?`-0`:e:e>0?`Infinity`:`-Infinity`;if(typeof e==`string`)return e;if(typeof e==`bigint`||typeof e==`symbol`)return e.toString();if(t.has(e))return`[Circular]`;if(n>=8)return`[MaxDepth]`;if(t.add(e),O(e))return A(e,t,n);if(Array.isArray(e))return e.map(e=>j(e,t,n+1));let r=Object.getPrototypeOf(e)?.constructor?.name;if(r&&r!==`Object`){if(e instanceof Date)return e.toISOString();if(e instanceof RegExp)return String(e);if(e instanceof Map){let r={};for(let[i,a]of e.entries())r[String(i)]=j(a,t,n+1);return{type:`Map`,entries:r}}if(e instanceof Set)return{type:`Set`,values:[...e].map(e=>j(e,t,n+1))};if(e instanceof Promise)return`Promise`;if(e instanceof Error)return{type:`Error`,name:e.name,message:e.message};if(typeof Node<`u`&&e instanceof Node&&e instanceof Element){let t={};for(let n of e.attributes)t[n.name]=n.value;return{type:`dom`,tagName:e.tagName.toLowerCase(),attrs:t}}return{type:`class`,name:r}}let i={};for(let r of Object.keys(e))i[r]=j(e[r],t,n+1);return i}function M(e){return j(e,new WeakSet,0)}function N(e){return Array.isArray(e)?e.map(e=>({pathString:e.pathString,diffType:e.diffType,prevValue:M(e.prevValue),nextValue:M(e.nextValue)})):!1}function P(e){return{propsDifferences:N(e.propsDifferences),stateDifferences:N(e.stateDifferences),hookDifferences:N(e.hookDifferences)}}function F(e){console.log(`%c[WDYR MCP]%c ${e}`,`color: #38bdf8; font-weight: bold`,`color: inherit; font-weight: normal`)}function I(e){globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__||(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__={supportsFiber:!0,inject(){},onCommitFiberRoot(){},onCommitFiberUnmount(){}});let t=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,n=t.onCommitFiberRoot.bind(t);t.onCommitFiberRoot=(...t)=>(e(),n(...t))}function L(e){let t={};return e.include&&(t.include=e.include.map(e=>e.source)),e.exclude&&(t.exclude=e.exclude.map(e=>e.source)),e.trackAllPureComponents!=null&&(t.trackAllPureComponents=e.trackAllPureComponents),e.trackHooks!=null&&(t.trackHooks=e.trackHooks),e.trackExtraHooks&&(t.trackExtraHooks=e.trackExtraHooks.map(([,e])=>e)),e.logOnDifferentValues!=null&&(t.logOnDifferentValues=e.logOnDifferentValues),e.logOwnerReasons!=null&&(t.logOwnerReasons=e.logOwnerReasons),t}function R(e){let{wsUrl:t,projectId:n,notifier:r,...i}=e??{},a=t??`http://localhost:4649`,o=n??globalThis.location?.origin??`default`,s=0;I(()=>{s++});let l=(0,c.io)(a,{reconnection:!0,reconnectionDelay:1e3,reconnectionDelayMax:3e4,transports:[`websocket`]});l.on(`connect`,()=>{F(`Connected to ${a}`),e&&l.emit(`config`,L(e),o)}),l.on(`disconnect`,()=>{F(`Disconnected, reconnecting...`)});let u=null,d=!1;async function f(){if(d=!1,!u||u.items.length===0)return;let e=u;u=null;let t=await Promise.all(e.items.map(async({info:e,error:t})=>{let n=await C(t);return{displayName:e.displayName,reason:P(e.reason),hookName:e.hookName,...n.length>0&&{stackFrames:n}}}));l.emit(`render-batch`,t,o,e.commitId)}return{...i,notifier(e){let t=Error();u&&u.commitId===s?u.items.push({info:e,error:t}):(u&&f(),u={commitId:s,items:[{info:e,error:t}]}),d||(d=!0,queueMicrotask(f)),r&&r(e)}}}exports.buildOptions=R;
|
package/dist/client/index.d.cts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WhyDidYouRenderOptions } from "@welldone-software/why-did-you-render";
|
|
2
2
|
|
|
3
3
|
//#region src/client/index.d.ts
|
|
4
4
|
sideEffect();
|
|
5
|
-
interface ClientOptions {
|
|
5
|
+
interface ClientOptions extends WhyDidYouRenderOptions {
|
|
6
6
|
wsUrl?: string;
|
|
7
7
|
projectId?: string;
|
|
8
8
|
}
|
|
9
|
-
declare function buildOptions(opts?: ClientOptions):
|
|
10
|
-
notifier(info: UpdateInfo): void;
|
|
11
|
-
};
|
|
9
|
+
declare function buildOptions(opts?: ClientOptions): WhyDidYouRenderOptions;
|
|
12
10
|
//#endregion
|
|
13
11
|
export { ClientOptions, buildOptions };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WhyDidYouRenderOptions } from "@welldone-software/why-did-you-render";
|
|
2
2
|
|
|
3
3
|
//#region src/client/index.d.ts
|
|
4
4
|
sideEffect();
|
|
5
|
-
interface ClientOptions {
|
|
5
|
+
interface ClientOptions extends WhyDidYouRenderOptions {
|
|
6
6
|
wsUrl?: string;
|
|
7
7
|
projectId?: string;
|
|
8
8
|
}
|
|
9
|
-
declare function buildOptions(opts?: ClientOptions):
|
|
10
|
-
notifier(info: UpdateInfo): void;
|
|
11
|
-
};
|
|
9
|
+
declare function buildOptions(opts?: ClientOptions): WhyDidYouRenderOptions;
|
|
12
10
|
//#endregion
|
|
13
11
|
export { ClientOptions, buildOptions };
|
package/dist/client/index.js
CHANGED
|
@@ -1,229 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
49
|
-
function serialize(value, seen, depth) {
|
|
50
|
-
if (value === null) return null;
|
|
51
|
-
if (value === void 0) return null;
|
|
52
|
-
if (typeof value === "function") return {
|
|
53
|
-
type: "function",
|
|
54
|
-
name: value.name || "anonymous"
|
|
55
|
-
};
|
|
56
|
-
if (typeof value === "boolean") return value;
|
|
57
|
-
if (typeof value === "number") {
|
|
58
|
-
if (Number.isNaN(value)) return "NaN";
|
|
59
|
-
if (!Number.isFinite(value)) return value > 0 ? "Infinity" : "-Infinity";
|
|
60
|
-
if (Object.is(value, -0)) return "-0";
|
|
61
|
-
return value;
|
|
62
|
-
}
|
|
63
|
-
if (typeof value === "string") return value;
|
|
64
|
-
if (typeof value === "bigint") return value.toString();
|
|
65
|
-
if (typeof value === "symbol") return value.toString();
|
|
66
|
-
if (seen.has(value)) return "[Circular]";
|
|
67
|
-
if (depth >= MAX_DEPTH) return "[MaxDepth]";
|
|
68
|
-
seen.add(value);
|
|
69
|
-
if (isReactElement(value)) return serializeReactElement(value, seen, depth);
|
|
70
|
-
if (Array.isArray(value)) return value.map((item) => serialize(item, seen, depth + 1));
|
|
71
|
-
const ctorName = Object.getPrototypeOf(value)?.constructor?.name;
|
|
72
|
-
if (ctorName && ctorName !== "Object") {
|
|
73
|
-
if (value instanceof Date) return value.toISOString();
|
|
74
|
-
if (value instanceof RegExp) return String(value);
|
|
75
|
-
if (value instanceof Map) {
|
|
76
|
-
const entries = {};
|
|
77
|
-
for (const [k, v] of value.entries()) entries[String(k)] = serialize(v, seen, depth + 1);
|
|
78
|
-
return {
|
|
79
|
-
type: "Map",
|
|
80
|
-
entries
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
if (value instanceof Set) return {
|
|
84
|
-
type: "Set",
|
|
85
|
-
values: [...value].map((v) => serialize(v, seen, depth + 1))
|
|
86
|
-
};
|
|
87
|
-
if (value instanceof Promise) return "Promise";
|
|
88
|
-
if (value instanceof Error) return {
|
|
89
|
-
type: "Error",
|
|
90
|
-
name: value.name,
|
|
91
|
-
message: value.message
|
|
92
|
-
};
|
|
93
|
-
if (typeof Node !== "undefined" && value instanceof Node && value instanceof Element) {
|
|
94
|
-
const attrs = {};
|
|
95
|
-
for (const attr of value.attributes) attrs[attr.name] = attr.value;
|
|
96
|
-
return {
|
|
97
|
-
type: "dom",
|
|
98
|
-
tagName: value.tagName.toLowerCase(),
|
|
99
|
-
attrs
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
type: "class",
|
|
104
|
-
name: ctorName
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
const result = {};
|
|
108
|
-
for (const key of Object.keys(value)) result[key] = serialize(value[key], seen, depth + 1);
|
|
109
|
-
return result;
|
|
110
|
-
}
|
|
111
|
-
function describeValue(value) {
|
|
112
|
-
return serialize(value, /* @__PURE__ */ new WeakSet(), 0);
|
|
113
|
-
}
|
|
114
|
-
//#endregion
|
|
115
|
-
//#region src/client/utils/sanitize-differences.ts
|
|
116
|
-
function sanitizeDifferences(diffs) {
|
|
117
|
-
if (!Array.isArray(diffs)) return false;
|
|
118
|
-
return diffs.map((diff) => ({
|
|
119
|
-
pathString: diff.pathString,
|
|
120
|
-
diffType: diff.diffType,
|
|
121
|
-
prevValue: describeValue(diff.prevValue),
|
|
122
|
-
nextValue: describeValue(diff.nextValue)
|
|
123
|
-
}));
|
|
124
|
-
}
|
|
125
|
-
//#endregion
|
|
126
|
-
//#region src/client/utils/sanitize-reason.ts
|
|
127
|
-
function sanitizeReason(reason) {
|
|
128
|
-
return {
|
|
129
|
-
propsDifferences: sanitizeDifferences(reason.propsDifferences),
|
|
130
|
-
stateDifferences: sanitizeDifferences(reason.stateDifferences),
|
|
131
|
-
hookDifferences: sanitizeDifferences(reason.hookDifferences)
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
//#endregion
|
|
135
|
-
//#region src/client/index.ts
|
|
136
|
-
const DEFAULT_WS_URL = "ws://localhost:4649";
|
|
137
|
-
const PREFIX_STYLE = "color: #38bdf8; font-weight: bold";
|
|
138
|
-
const RESET_STYLE = "color: inherit; font-weight: normal";
|
|
139
|
-
function log(message) {
|
|
140
|
-
console.log(`%c[WDYR MCP]%c ${message}`, PREFIX_STYLE, RESET_STYLE);
|
|
141
|
-
}
|
|
142
|
-
function patchDevToolsHook(onCommit) {
|
|
143
|
-
if (!globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
144
|
-
supportsFiber: true,
|
|
145
|
-
inject() {},
|
|
146
|
-
onCommitFiberRoot() {},
|
|
147
|
-
onCommitFiberUnmount() {}
|
|
148
|
-
};
|
|
149
|
-
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
150
|
-
const original = hook.onCommitFiberRoot.bind(hook);
|
|
151
|
-
hook.onCommitFiberRoot = (...args) => {
|
|
152
|
-
onCommit();
|
|
153
|
-
return original(...args);
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
function buildOptions(opts) {
|
|
157
|
-
const wsUrl = opts?.wsUrl ?? DEFAULT_WS_URL;
|
|
158
|
-
const projectId = opts?.projectId ?? globalThis.location?.origin ?? "default";
|
|
159
|
-
const MAX_QUEUE_SIZE = 1e3;
|
|
160
|
-
const BASE_DELAY = 1e3;
|
|
161
|
-
const MAX_DELAY = 3e4;
|
|
162
|
-
let ws = null;
|
|
163
|
-
let queue = [];
|
|
164
|
-
let commitId = 0;
|
|
165
|
-
let retryDelay = BASE_DELAY;
|
|
166
|
-
patchDevToolsHook(() => {
|
|
167
|
-
commitId++;
|
|
168
|
-
});
|
|
169
|
-
function connect() {
|
|
170
|
-
ws = new WebSocket(wsUrl);
|
|
171
|
-
ws.addEventListener("open", () => {
|
|
172
|
-
log(`Connected to ${wsUrl}`);
|
|
173
|
-
retryDelay = BASE_DELAY;
|
|
174
|
-
for (const msg of queue) ws?.send(JSON.stringify(msg));
|
|
175
|
-
queue = [];
|
|
176
|
-
});
|
|
177
|
-
ws.addEventListener("close", () => {
|
|
178
|
-
ws = null;
|
|
179
|
-
setTimeout(connect, retryDelay);
|
|
180
|
-
retryDelay = Math.min(retryDelay * 2, MAX_DELAY);
|
|
181
|
-
});
|
|
182
|
-
ws.addEventListener("error", () => {
|
|
183
|
-
log(`Connection failed (${wsUrl}). Retrying in ${retryDelay / 1e3}s...`);
|
|
184
|
-
ws?.close();
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
connect();
|
|
188
|
-
function send(msg) {
|
|
189
|
-
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
190
|
-
else {
|
|
191
|
-
if (queue.length >= MAX_QUEUE_SIZE) queue.shift();
|
|
192
|
-
queue.push(msg);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
let pendingBatch = null;
|
|
196
|
-
let flushScheduled = false;
|
|
197
|
-
function flushBatch() {
|
|
198
|
-
flushScheduled = false;
|
|
199
|
-
if (!pendingBatch || pendingBatch.reports.length === 0) return;
|
|
200
|
-
send({
|
|
201
|
-
type: "render-batch",
|
|
202
|
-
projectId,
|
|
203
|
-
commitId: pendingBatch.commitId,
|
|
204
|
-
payload: pendingBatch.reports
|
|
205
|
-
});
|
|
206
|
-
pendingBatch = null;
|
|
207
|
-
}
|
|
208
|
-
return { notifier(info) {
|
|
209
|
-
const report = {
|
|
210
|
-
displayName: info.displayName,
|
|
211
|
-
reason: sanitizeReason(info.reason),
|
|
212
|
-
hookName: info.hookName
|
|
213
|
-
};
|
|
214
|
-
if (pendingBatch && pendingBatch.commitId === commitId) pendingBatch.reports.push(report);
|
|
215
|
-
else {
|
|
216
|
-
if (pendingBatch) flushBatch();
|
|
217
|
-
pendingBatch = {
|
|
218
|
-
commitId,
|
|
219
|
-
reports: [report]
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
if (!flushScheduled) {
|
|
223
|
-
flushScheduled = true;
|
|
224
|
-
queueMicrotask(flushBatch);
|
|
225
|
-
}
|
|
226
|
-
} };
|
|
227
|
-
}
|
|
228
|
-
//#endregion
|
|
229
|
-
export { buildOptions };
|
|
1
|
+
import{io as e}from"socket.io-client";import t from"error-stack-parser";import{TraceMap as n,originalPositionFor as r}from"@jridgewell/trace-mapping";const i=new Map;function a(e){let t=i.get(e);if(t)return t;let r=(async()=>{try{let t=await fetch(`${e}.map`);return t.ok?new n(await t.json()):null}catch{return null}})();return i.set(e,r),r}async function o(e,t,n){let i=await a(e);if(!i)return{path:e,line:t};let o=r(i,{line:t,column:n});return o.source?{path:o.source,line:o.line??t}:{path:e,line:t}}const s=[`whyDidYouRender`,`react-dom`,`react.development`,`react.production`,`scheduler.`,`installHook`,`console.`],c=[`trackHookChanges`,`WDYRFunctionalComponent`,`Object.notifier`,`notifier`,`console.trace`],l=new Set(`renderWithHooks.mountIndeterminateComponent.updateFunctionComponent.updateForwardRef.updateMemoComponent.updateSimpleMemoComponent.beginWork.beginWork$1.completeWork.completeUnitOfWork.performUnitOfWork.runWithFiberInDEV.callComponentInDEV.workLoopSync.workLoopConcurrent.renderRootSync.renderRootConcurrent.performWorkOnRoot.performSyncWorkOnRoot.performConcurrentWorkOnRoot.commitRoot.commitRootImpl.commitMutationEffects.commitMutationEffectsOnFiber.commitLayoutEffects.commitLayoutEffectOnFiber.flushPassiveEffects.flushPassiveEffectsImpl.flushSyncWorkAcrossRoots_impl.processRootScheduleInMicrotask.scheduleUpdateOnFiber.ensureRootIsScheduled.react_stack_bottom_frame.dispatchSetState.dispatchReducerAction.dispatchAction.mountState.updateState.mountReducer.updateReducer.mountMemo.updateMemo.mountEffect.updateEffect.mountLayoutEffect.updateLayoutEffect.mountRef.updateRef.flushWork.performWorkUntilDeadline`.split(`.`));function u(e){return s.some(t=>e.includes(t))}function d(e){return c.some(t=>e===t)}function f(e){return e.replace(/WDYR$/,``)}function p(e){let t=e.startsWith(`new `)?e.slice(4):e,n=t.lastIndexOf(`.`);return f(n>=0?t.slice(n+1):t)}function m(e){return/^use[A-Z]/.test(e)}function h(e){let n;try{n=t.parse(e)}catch{return[]}let r=[];for(let e of n){let t=e.fileName??``;if(!t||u(t))continue;let n=e.functionName;if(!n||d(n))continue;let i=p(n);i&&(l.has(i)||r.push({type:m(i)?`hook`:`component`,name:i,file:t,line:e.lineNumber??0,column:e.columnNumber??0}))}return r}async function g(e){let t=h(e);return t.length===0?[]:Promise.all(t.map(async e=>({type:e.type,name:e.name,location:await o(e.file,e.line,e.column)})))}const _=Symbol.for(`react.element`),v=Symbol.for(`react.transitional.element`),y=Symbol.for(`react.memo`),b=Symbol.for(`react.forward_ref`);function x(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.$$typeof===_||t.$$typeof===v||t.$$typeof===60103}function S(e){let t=!1,n=!1,r=e;for(let e=0;e<5&&!(typeof r!=`object`||!r);e++){let e=r;if(e.$$typeof===y)t=!0,r=e.type;else if(e.$$typeof===b)n=!0,r=e.render;else break}let i=`Unknown`;return typeof r==`string`?i=r:typeof r==`function`&&(i=r.displayName||r.name||`Anonymous`),{name:i,memo:t,forwardRef:n}}function C(e,t,n){let r=S(e.type),i={};if(e.props&&typeof e.props==`object`)for(let r of Object.keys(e.props))r!==`children`&&(i[r]=w(e.props[r],t,n+1));return{type:`react-node`,component:r,props:i}}function w(e,t,n){if(e==null)return null;if(typeof e==`function`)return{type:`function`,name:e.name||`anonymous`};if(typeof e==`boolean`)return e;if(typeof e==`number`)return Number.isNaN(e)?`NaN`:Number.isFinite(e)?Object.is(e,-0)?`-0`:e:e>0?`Infinity`:`-Infinity`;if(typeof e==`string`)return e;if(typeof e==`bigint`||typeof e==`symbol`)return e.toString();if(t.has(e))return`[Circular]`;if(n>=8)return`[MaxDepth]`;if(t.add(e),x(e))return C(e,t,n);if(Array.isArray(e))return e.map(e=>w(e,t,n+1));let r=Object.getPrototypeOf(e)?.constructor?.name;if(r&&r!==`Object`){if(e instanceof Date)return e.toISOString();if(e instanceof RegExp)return String(e);if(e instanceof Map){let r={};for(let[i,a]of e.entries())r[String(i)]=w(a,t,n+1);return{type:`Map`,entries:r}}if(e instanceof Set)return{type:`Set`,values:[...e].map(e=>w(e,t,n+1))};if(e instanceof Promise)return`Promise`;if(e instanceof Error)return{type:`Error`,name:e.name,message:e.message};if(typeof Node<`u`&&e instanceof Node&&e instanceof Element){let t={};for(let n of e.attributes)t[n.name]=n.value;return{type:`dom`,tagName:e.tagName.toLowerCase(),attrs:t}}return{type:`class`,name:r}}let i={};for(let r of Object.keys(e))i[r]=w(e[r],t,n+1);return i}function T(e){return w(e,new WeakSet,0)}function E(e){return Array.isArray(e)?e.map(e=>({pathString:e.pathString,diffType:e.diffType,prevValue:T(e.prevValue),nextValue:T(e.nextValue)})):!1}function D(e){return{propsDifferences:E(e.propsDifferences),stateDifferences:E(e.stateDifferences),hookDifferences:E(e.hookDifferences)}}function O(e){console.log(`%c[WDYR MCP]%c ${e}`,`color: #38bdf8; font-weight: bold`,`color: inherit; font-weight: normal`)}function k(e){globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__||(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__={supportsFiber:!0,inject(){},onCommitFiberRoot(){},onCommitFiberUnmount(){}});let t=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,n=t.onCommitFiberRoot.bind(t);t.onCommitFiberRoot=(...t)=>(e(),n(...t))}function A(e){let t={};return e.include&&(t.include=e.include.map(e=>e.source)),e.exclude&&(t.exclude=e.exclude.map(e=>e.source)),e.trackAllPureComponents!=null&&(t.trackAllPureComponents=e.trackAllPureComponents),e.trackHooks!=null&&(t.trackHooks=e.trackHooks),e.trackExtraHooks&&(t.trackExtraHooks=e.trackExtraHooks.map(([,e])=>e)),e.logOnDifferentValues!=null&&(t.logOnDifferentValues=e.logOnDifferentValues),e.logOwnerReasons!=null&&(t.logOwnerReasons=e.logOwnerReasons),t}function j(t){let{wsUrl:n,projectId:r,notifier:i,...a}=t??{},o=n??`http://localhost:4649`,s=r??globalThis.location?.origin??`default`,c=0;k(()=>{c++});let l=e(o,{reconnection:!0,reconnectionDelay:1e3,reconnectionDelayMax:3e4,transports:[`websocket`]});l.on(`connect`,()=>{O(`Connected to ${o}`),t&&l.emit(`config`,A(t),s)}),l.on(`disconnect`,()=>{O(`Disconnected, reconnecting...`)});let u=null,d=!1;async function f(){if(d=!1,!u||u.items.length===0)return;let e=u;u=null;let t=await Promise.all(e.items.map(async({info:e,error:t})=>{let n=await g(t);return{displayName:e.displayName,reason:D(e.reason),hookName:e.hookName,...n.length>0&&{stackFrames:n}}}));l.emit(`render-batch`,t,s,e.commitId)}return{...a,notifier(e){let t=Error();u&&u.commitId===c?u.items.push({info:e,error:t}):(u&&f(),u={commitId:c,items:[{info:e,error:t}]}),d||(d=!0,queueMicrotask(f)),i&&i(e)}}}export{j as buildOptions};
|
package/dist/server/index.js
CHANGED
|
@@ -1,573 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
91
|
-
//#region src/server/store/utils/read-jsonl.ts
|
|
92
|
-
function readJsonl(file) {
|
|
93
|
-
if (!existsSync(file)) return [];
|
|
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));
|
|
106
|
-
}
|
|
107
|
-
//#endregion
|
|
108
|
-
//#region src/server/store/utils/sanitize-project-id.ts
|
|
109
|
-
function sanitizeProjectId(projectId) {
|
|
110
|
-
return projectId.replaceAll(/[^a-zA-Z0-9_.-]/g, "_");
|
|
111
|
-
}
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region src/server/store/utils/to-result.ts
|
|
114
|
-
function toResult(stored) {
|
|
115
|
-
return {
|
|
116
|
-
project: stored.projectId,
|
|
117
|
-
displayName: stored.displayName,
|
|
118
|
-
reason: stored.reason,
|
|
119
|
-
...stored.hookName != null && { hookName: stored.hookName },
|
|
120
|
-
...stored.commitId != null && { commitId: stored.commitId }
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
//#endregion
|
|
124
|
-
//#region src/server/store/render-store.ts
|
|
125
|
-
const FLUSH_DELAY_MS = 200;
|
|
126
|
-
const NOCOMMIT = "nocommit";
|
|
127
|
-
var RenderStore = class {
|
|
128
|
-
dir;
|
|
129
|
-
buffers = /* @__PURE__ */ new Map();
|
|
130
|
-
timers = /* @__PURE__ */ new Map();
|
|
131
|
-
dicts = /* @__PURE__ */ new Map();
|
|
132
|
-
bufferMeta = /* @__PURE__ */ new Map();
|
|
133
|
-
constructor(dir) {
|
|
134
|
-
this.dir = dir ?? join(homedir(), ".wdyr-mcp", "renders");
|
|
135
|
-
mkdirSync(this.dir, { recursive: true });
|
|
136
|
-
}
|
|
137
|
-
addRender(report, projectId, commitId) {
|
|
138
|
-
const stored = {
|
|
139
|
-
...report,
|
|
140
|
-
projectId,
|
|
141
|
-
...commitId != null && { commitId }
|
|
142
|
-
};
|
|
143
|
-
const bk = this.bufferKey(projectId, commitId);
|
|
144
|
-
let buf = this.buffers.get(bk);
|
|
145
|
-
if (!buf) {
|
|
146
|
-
buf = [];
|
|
147
|
-
this.buffers.set(bk, buf);
|
|
148
|
-
this.bufferMeta.set(bk, {
|
|
149
|
-
projectId,
|
|
150
|
-
commitId
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
buf.push(stored);
|
|
154
|
-
const existing = this.timers.get(bk);
|
|
155
|
-
if (existing) clearTimeout(existing);
|
|
156
|
-
this.timers.set(bk, setTimeout(() => {
|
|
157
|
-
this.flushAsync(projectId, commitId).catch((err) => console.error(`[wdyr-mcp] flush error for ${bk}:`, err));
|
|
158
|
-
}, FLUSH_DELAY_MS));
|
|
159
|
-
}
|
|
160
|
-
async flushAsync(projectId, commitId) {
|
|
161
|
-
await ensureReady();
|
|
162
|
-
this.flush(projectId, commitId);
|
|
163
|
-
}
|
|
164
|
-
flush(projectId, commitId) {
|
|
165
|
-
if (projectId != null && commitId !== void 0) this.flushBuffer(this.bufferKey(projectId, commitId));
|
|
166
|
-
else if (projectId != null) for (const bk of this.bufferKeysForProject(projectId)) this.flushBuffer(bk);
|
|
167
|
-
else for (const bk of [...this.buffers.keys()]) this.flushBuffer(bk);
|
|
168
|
-
}
|
|
169
|
-
flushBuffer(bk) {
|
|
170
|
-
const buf = this.buffers.get(bk);
|
|
171
|
-
if (!buf || buf.length === 0) return;
|
|
172
|
-
const meta = this.bufferMeta.get(bk);
|
|
173
|
-
if (!meta) return;
|
|
174
|
-
let dict = this.dicts.get(bk);
|
|
175
|
-
if (!dict) {
|
|
176
|
-
dict = {};
|
|
177
|
-
this.dicts.set(bk, dict);
|
|
178
|
-
}
|
|
179
|
-
const dehydrated = buf.map((r) => dehydrate(r, dict));
|
|
180
|
-
const file = this.commitFile(meta.projectId, meta.commitId);
|
|
181
|
-
const existingLines = this.readDataLines(file);
|
|
182
|
-
const newLines = dehydrated.map((r) => JSON.stringify(r));
|
|
183
|
-
const allDataLines = [...existingLines, ...newLines];
|
|
184
|
-
writeFileSync(file, `${(Object.keys(dict).length > 0 ? [JSON.stringify({ [DICT_KEY]: dict }), ...allDataLines] : allDataLines).join("\n")}\n`);
|
|
185
|
-
buf.length = 0;
|
|
186
|
-
const timer = this.timers.get(bk);
|
|
187
|
-
if (timer) {
|
|
188
|
-
clearTimeout(timer);
|
|
189
|
-
this.timers.delete(bk);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
readDataLines(file) {
|
|
193
|
-
if (!existsSync(file)) return [];
|
|
194
|
-
return readFileSync(file, "utf-8").split("\n").filter((line) => {
|
|
195
|
-
if (!line) return false;
|
|
196
|
-
if (line.startsWith(`{"@@dict"`)) return false;
|
|
197
|
-
return true;
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
getAllRenders(projectId) {
|
|
201
|
-
this.flush(projectId);
|
|
202
|
-
if (projectId) return this.projectFiles(projectId).flatMap((f) => readJsonl(join(this.dir, f)).map(toResult));
|
|
203
|
-
return this.jsonlFiles().flatMap((f) => readJsonl(join(this.dir, f)).map(toResult));
|
|
204
|
-
}
|
|
205
|
-
getRendersByComponent(componentName, projectId) {
|
|
206
|
-
return this.getAllRenders(projectId).filter((r) => r.displayName === componentName);
|
|
207
|
-
}
|
|
208
|
-
clearRenders(projectId) {
|
|
209
|
-
if (projectId) {
|
|
210
|
-
for (const bk of this.bufferKeysForProject(projectId)) {
|
|
211
|
-
this.buffers.delete(bk);
|
|
212
|
-
this.dicts.delete(bk);
|
|
213
|
-
this.bufferMeta.delete(bk);
|
|
214
|
-
const timer = this.timers.get(bk);
|
|
215
|
-
if (timer) {
|
|
216
|
-
clearTimeout(timer);
|
|
217
|
-
this.timers.delete(bk);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
for (const f of this.projectFiles(projectId)) unlinkSync(join(this.dir, f));
|
|
221
|
-
} else {
|
|
222
|
-
for (const [, timer] of this.timers) clearTimeout(timer);
|
|
223
|
-
this.buffers.clear();
|
|
224
|
-
this.timers.clear();
|
|
225
|
-
this.dicts.clear();
|
|
226
|
-
this.bufferMeta.clear();
|
|
227
|
-
for (const f of this.jsonlFiles()) unlinkSync(join(this.dir, f));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
getProjects() {
|
|
231
|
-
this.flush();
|
|
232
|
-
const projects = /* @__PURE__ */ new Set();
|
|
233
|
-
const seen = /* @__PURE__ */ new Set();
|
|
234
|
-
for (const f of this.jsonlFiles()) {
|
|
235
|
-
const parsed = this.parseFilename(f);
|
|
236
|
-
if (!parsed) continue;
|
|
237
|
-
if (seen.has(parsed.projectSanitized)) continue;
|
|
238
|
-
seen.add(parsed.projectSanitized);
|
|
239
|
-
const lines = readFileSync(join(this.dir, f), "utf-8").split("\n");
|
|
240
|
-
for (const line of lines) {
|
|
241
|
-
if (!line) continue;
|
|
242
|
-
const obj = JSON.parse(line);
|
|
243
|
-
if ("@@dict" in obj) continue;
|
|
244
|
-
projects.add(obj.projectId);
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return [...projects];
|
|
249
|
-
}
|
|
250
|
-
getCommitIds(projectId) {
|
|
251
|
-
this.flush(projectId);
|
|
252
|
-
const files = projectId ? this.projectFiles(projectId) : this.jsonlFiles();
|
|
253
|
-
const ids = /* @__PURE__ */ new Set();
|
|
254
|
-
for (const f of files) {
|
|
255
|
-
const parsed = this.parseFilename(f);
|
|
256
|
-
if (parsed?.commitId != null) ids.add(parsed.commitId);
|
|
257
|
-
}
|
|
258
|
-
return [...ids].sort((a, b) => a - b);
|
|
259
|
-
}
|
|
260
|
-
getRendersByCommit(commitId, projectId) {
|
|
261
|
-
if (projectId) {
|
|
262
|
-
this.flush(projectId, commitId);
|
|
263
|
-
return readJsonl(this.commitFile(projectId, commitId)).map(toResult);
|
|
264
|
-
}
|
|
265
|
-
this.flush();
|
|
266
|
-
const suffix = `_commit_${commitId}.jsonl`;
|
|
267
|
-
return this.jsonlFiles().filter((f) => f.endsWith(suffix)).flatMap((f) => readJsonl(join(this.dir, f)).map(toResult));
|
|
268
|
-
}
|
|
269
|
-
getSummary(projectId) {
|
|
270
|
-
const renders = this.getAllRenders(projectId);
|
|
271
|
-
const summary = {};
|
|
272
|
-
for (const r of renders) {
|
|
273
|
-
summary[r.project] ??= {};
|
|
274
|
-
const project = summary[r.project];
|
|
275
|
-
project[r.displayName] = (project[r.displayName] ?? 0) + 1;
|
|
276
|
-
}
|
|
277
|
-
return summary;
|
|
278
|
-
}
|
|
279
|
-
bufferKey(projectId, commitId) {
|
|
280
|
-
return `${projectId}\0${commitId ?? NOCOMMIT}`;
|
|
281
|
-
}
|
|
282
|
-
bufferKeysForProject(projectId) {
|
|
283
|
-
const prefix = `${projectId}\0`;
|
|
284
|
-
return [...this.buffers.keys()].filter((bk) => bk.startsWith(prefix));
|
|
285
|
-
}
|
|
286
|
-
commitFile(projectId, commitId) {
|
|
287
|
-
const sanitized = sanitizeProjectId(projectId);
|
|
288
|
-
const suffix = commitId != null ? `_commit_${commitId}` : `_${NOCOMMIT}`;
|
|
289
|
-
return join(this.dir, `${sanitized}${suffix}.jsonl`);
|
|
290
|
-
}
|
|
291
|
-
projectFiles(projectId) {
|
|
292
|
-
const prefix = sanitizeProjectId(projectId);
|
|
293
|
-
return readdirSync(this.dir).filter((f) => f.startsWith(prefix) && f.endsWith(".jsonl"));
|
|
294
|
-
}
|
|
295
|
-
parseFilename(filename) {
|
|
296
|
-
if (!filename.endsWith(".jsonl")) return null;
|
|
297
|
-
const base = filename.slice(0, -6);
|
|
298
|
-
const commitMatch = base.match(/^(.+)_commit_(\d+)$/);
|
|
299
|
-
if (commitMatch) return {
|
|
300
|
-
projectSanitized: commitMatch[1],
|
|
301
|
-
commitId: Number(commitMatch[2])
|
|
302
|
-
};
|
|
303
|
-
const nocommitMatch = base.match(/^(.+)_nocommit$/);
|
|
304
|
-
if (nocommitMatch) return { projectSanitized: nocommitMatch[1] };
|
|
305
|
-
return { projectSanitized: base };
|
|
306
|
-
}
|
|
307
|
-
jsonlFiles() {
|
|
308
|
-
return readdirSync(this.dir).filter((f) => f.endsWith(".jsonl"));
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
//#endregion
|
|
312
|
-
//#region src/server/store/index.ts
|
|
313
|
-
const store = new RenderStore();
|
|
314
|
-
//#endregion
|
|
315
|
-
//#region src/server/tools/utils/resolve-project.ts
|
|
316
|
-
/**
|
|
317
|
-
* Resolves the project to use. If a project is explicitly given, use it.
|
|
318
|
-
* If only one project exists, auto-select it. If multiple exist, return
|
|
319
|
-
* a message asking the agent to disambiguate.
|
|
320
|
-
*/
|
|
321
|
-
function resolveProject(project) {
|
|
322
|
-
if (project) return { projectId: project };
|
|
323
|
-
const projects = store.getProjects();
|
|
324
|
-
if (projects.length === 0) return { projectId: void 0 };
|
|
325
|
-
if (projects.length === 1) return { projectId: projects[0] };
|
|
326
|
-
return {
|
|
327
|
-
projectId: void 0,
|
|
328
|
-
error: [
|
|
329
|
-
"Multiple projects are recording render data. Ask the user which project they are working on (e.g. their dev server URL like http://localhost:3000).",
|
|
330
|
-
"",
|
|
331
|
-
"Active projects:",
|
|
332
|
-
...projects.map((p) => `- ${p}`)
|
|
333
|
-
].join("\n")
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
//#endregion
|
|
337
|
-
//#region src/server/tools/utils/text-result.ts
|
|
338
|
-
function textResult(text) {
|
|
339
|
-
return { content: [{
|
|
340
|
-
type: "text",
|
|
341
|
-
text
|
|
342
|
-
}] };
|
|
343
|
-
}
|
|
344
|
-
//#endregion
|
|
345
|
-
//#region src/server/tools/clear-renders.ts
|
|
346
|
-
function register$5(server) {
|
|
347
|
-
server.registerTool("clear_renders", {
|
|
348
|
-
title: "Clear Renders",
|
|
349
|
-
description: "Clears collected render data. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.",
|
|
350
|
-
inputSchema: { project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.") }
|
|
351
|
-
}, async ({ project }) => {
|
|
352
|
-
const resolved = resolveProject(project);
|
|
353
|
-
if (resolved.error) return textResult(resolved.error);
|
|
354
|
-
store.clearRenders(resolved.projectId);
|
|
355
|
-
return textResult(resolved.projectId ? `Render data cleared for ${resolved.projectId}.` : "All render data cleared.");
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
//#endregion
|
|
359
|
-
//#region src/server/tools/get-commits.ts
|
|
360
|
-
function register$4(server) {
|
|
361
|
-
server.registerTool("get_commits", {
|
|
362
|
-
title: "Get Commits",
|
|
363
|
-
description: "Returns a list of React commit IDs that have recorded render data for a project. Use these IDs with get_renders_by_commit to inspect individual commits.",
|
|
364
|
-
inputSchema: { project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.") }
|
|
365
|
-
}, async ({ project }) => {
|
|
366
|
-
const resolved = resolveProject(project);
|
|
367
|
-
if (resolved.error) return textResult(resolved.error);
|
|
368
|
-
const commitIds = store.getCommitIds(resolved.projectId);
|
|
369
|
-
if (commitIds.length === 0) return textResult("No commits recorded yet. Make sure the browser is connected and triggering re-renders.");
|
|
370
|
-
return textResult(JSON.stringify(commitIds));
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
//#endregion
|
|
374
|
-
//#region src/server/tools/get-projects.ts
|
|
375
|
-
function register$3(server) {
|
|
376
|
-
server.registerTool("get_projects", {
|
|
377
|
-
title: "Get Projects",
|
|
378
|
-
description: "Returns a list of project identifiers (browser origin URLs) that have recorded render data.",
|
|
379
|
-
inputSchema: {}
|
|
380
|
-
}, async () => {
|
|
381
|
-
const projects = store.getProjects();
|
|
382
|
-
if (projects.length === 0) return textResult("No projects have recorded render data yet.");
|
|
383
|
-
return textResult(`Active projects:\n${projects.map((p) => `- ${p}`).join("\n")}`);
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
//#endregion
|
|
387
|
-
//#region src/server/tools/get-render-summary.ts
|
|
388
|
-
function register$2(server) {
|
|
389
|
-
server.registerTool("get_render_summary", {
|
|
390
|
-
title: "Get Render Summary",
|
|
391
|
-
description: "Returns a summary of unnecessary re-renders grouped by component name with counts. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.",
|
|
392
|
-
inputSchema: { project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.") }
|
|
393
|
-
}, async ({ project }) => {
|
|
394
|
-
const resolved = resolveProject(project);
|
|
395
|
-
if (resolved.error) return textResult(resolved.error);
|
|
396
|
-
const summary = store.getSummary(resolved.projectId);
|
|
397
|
-
if (Object.keys(summary).length === 0) return textResult("No unnecessary renders recorded yet.");
|
|
398
|
-
const lines = [];
|
|
399
|
-
for (const [projectId, components] of Object.entries(summary)) {
|
|
400
|
-
lines.push(`[${projectId}]`);
|
|
401
|
-
for (const [name, count] of Object.entries(components)) lines.push(` ${name}: ${count} re-render(s)`);
|
|
402
|
-
}
|
|
403
|
-
return textResult(`Unnecessary re-render summary:\n\n${lines.join("\n")}`);
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
//#endregion
|
|
407
|
-
//#region src/server/tools/get-renders-by-commit.ts
|
|
408
|
-
function register$1(server) {
|
|
409
|
-
server.registerTool("get_renders_by_commit", {
|
|
410
|
-
title: "Get Renders by Commit",
|
|
411
|
-
description: "Returns all unnecessary re-renders for a specific React commit ID. Use get_commits first to discover available commit IDs.",
|
|
412
|
-
inputSchema: {
|
|
413
|
-
commitId: z.number().describe("The React commit ID to filter by."),
|
|
414
|
-
project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.")
|
|
415
|
-
}
|
|
416
|
-
}, async ({ commitId, project }) => {
|
|
417
|
-
const resolved = resolveProject(project);
|
|
418
|
-
if (resolved.error) return textResult(resolved.error);
|
|
419
|
-
const renders = store.getRendersByCommit(commitId, resolved.projectId);
|
|
420
|
-
if (renders.length === 0) return textResult(`No renders recorded for commit ${commitId}.`);
|
|
421
|
-
return textResult(JSON.stringify(renders, null, 2));
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
//#endregion
|
|
425
|
-
//#region src/server/tools/get-unnecessary-renders.ts
|
|
426
|
-
function register(server) {
|
|
427
|
-
server.registerTool("get_unnecessary_renders", {
|
|
428
|
-
title: "Get Unnecessary Renders",
|
|
429
|
-
description: "Returns all unnecessary re-renders collected from the browser. If multiple projects are active and no project is specified, the tool will ask you to disambiguate by asking the user for their dev server URL.",
|
|
430
|
-
inputSchema: {
|
|
431
|
-
component: z.string().optional().describe("Filter by component name. Omit to get all renders."),
|
|
432
|
-
project: z.string().optional().describe("Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.")
|
|
433
|
-
}
|
|
434
|
-
}, async ({ component, project }) => {
|
|
435
|
-
const resolved = resolveProject(project);
|
|
436
|
-
if (resolved.error) return textResult(resolved.error);
|
|
437
|
-
const renders = component ? store.getRendersByComponent(component, resolved.projectId) : store.getAllRenders(resolved.projectId);
|
|
438
|
-
if (renders.length === 0) return textResult(component ? `No unnecessary renders recorded for "${component}".` : "No unnecessary renders recorded yet. Make sure the browser is connected and triggering re-renders.");
|
|
439
|
-
return textResult(JSON.stringify(renders, null, 2));
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
//#endregion
|
|
443
|
-
//#region src/server/tools/index.ts
|
|
444
|
-
function registerTools(server) {
|
|
445
|
-
register(server);
|
|
446
|
-
register$2(server);
|
|
447
|
-
register$4(server);
|
|
448
|
-
register$1(server);
|
|
449
|
-
register$3(server);
|
|
450
|
-
register$5(server);
|
|
451
|
-
}
|
|
452
|
-
//#endregion
|
|
453
|
-
//#region src/server/liveness.ts
|
|
454
|
-
const DEFAULT_INTERVAL_MS = 3e4;
|
|
455
|
-
var HeartbeatManager = class {
|
|
456
|
-
connections = /* @__PURE__ */ new Map();
|
|
457
|
-
timer;
|
|
458
|
-
constructor(wss, store, intervalMs = DEFAULT_INTERVAL_MS) {
|
|
459
|
-
this.wss = wss;
|
|
460
|
-
this.store = store;
|
|
461
|
-
this.timer = setInterval(() => this.check(), intervalMs);
|
|
462
|
-
}
|
|
463
|
-
trackConnection(ws) {
|
|
464
|
-
this.connections.set(ws, {
|
|
465
|
-
isAlive: true,
|
|
466
|
-
projectId: null
|
|
467
|
-
});
|
|
468
|
-
ws.on("pong", () => {
|
|
469
|
-
const meta = this.connections.get(ws);
|
|
470
|
-
if (meta) meta.isAlive = true;
|
|
471
|
-
});
|
|
472
|
-
ws.on("close", () => {
|
|
473
|
-
this.connections.delete(ws);
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
setProjectId(ws, projectId) {
|
|
477
|
-
const meta = this.connections.get(ws);
|
|
478
|
-
if (meta) meta.projectId = projectId;
|
|
479
|
-
}
|
|
480
|
-
stop() {
|
|
481
|
-
clearInterval(this.timer);
|
|
482
|
-
this.connections.clear();
|
|
483
|
-
}
|
|
484
|
-
check() {
|
|
485
|
-
for (const [ws, meta] of this.connections) {
|
|
486
|
-
if (!meta.isAlive) {
|
|
487
|
-
this.connections.delete(ws);
|
|
488
|
-
ws.terminate();
|
|
489
|
-
if (meta.projectId && !this.hasOtherConnection(ws, meta.projectId)) {
|
|
490
|
-
console.error(`[wdyr-mcp] client for ${meta.projectId} is dead, clearing render data`);
|
|
491
|
-
this.store.clearRenders(meta.projectId);
|
|
492
|
-
}
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
meta.isAlive = false;
|
|
496
|
-
ws.ping();
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
hasOtherConnection(deadWs, projectId) {
|
|
500
|
-
for (const [ws, meta] of this.connections) if (ws !== deadWs && meta.projectId === projectId) return true;
|
|
501
|
-
return false;
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
//#endregion
|
|
505
|
-
//#region src/server/ws.ts
|
|
506
|
-
function createWsServer(port) {
|
|
507
|
-
const wss = new WebSocketServer({
|
|
508
|
-
port,
|
|
509
|
-
host: "127.0.0.1"
|
|
510
|
-
});
|
|
511
|
-
const heartbeat = new HeartbeatManager(wss, store);
|
|
512
|
-
wss.on("error", (err) => {
|
|
513
|
-
if (err.code === "EADDRINUSE") console.error(`[wdyr-mcp] Port ${port} already in use, another instance owns the WS server. Skipping.`);
|
|
514
|
-
else console.error("[wdyr-mcp] WS server error:", err);
|
|
515
|
-
});
|
|
516
|
-
wss.on("listening", () => {
|
|
517
|
-
console.error(`[wdyr-mcp] WebSocket server listening on ws://localhost:${port}`);
|
|
518
|
-
});
|
|
519
|
-
wss.on("connection", (ws) => {
|
|
520
|
-
console.error(`[wdyr-mcp] browser connected (ws://localhost:${port})`);
|
|
521
|
-
heartbeat.trackConnection(ws);
|
|
522
|
-
ws.on("message", (raw) => {
|
|
523
|
-
try {
|
|
524
|
-
const msg = JSON.parse(String(raw));
|
|
525
|
-
const projectId = msg.projectId ?? "default";
|
|
526
|
-
heartbeat.setProjectId(ws, projectId);
|
|
527
|
-
if (msg.type === "render") store.addRender(msg.payload, projectId, msg.commitId);
|
|
528
|
-
else if (msg.type === "render-batch") for (const report of msg.payload) store.addRender(report, projectId, msg.commitId);
|
|
529
|
-
} catch {
|
|
530
|
-
console.error("[wdyr-mcp] invalid message received");
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
ws.on("close", () => {
|
|
534
|
-
console.error("[wdyr-mcp] browser disconnected");
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
wss.on("close", () => {
|
|
538
|
-
heartbeat.stop();
|
|
539
|
-
});
|
|
540
|
-
return wss;
|
|
541
|
-
}
|
|
542
|
-
//#endregion
|
|
543
|
-
//#region src/server/index.ts
|
|
544
|
-
const DEFAULT_WS_PORT = 4649;
|
|
545
|
-
const server = new McpServer({
|
|
546
|
-
name: "why-did-you-render",
|
|
547
|
-
version: "0.0.0"
|
|
548
|
-
});
|
|
549
|
-
registerTools(server);
|
|
550
|
-
async function main() {
|
|
551
|
-
const wss = createWsServer(Number(process.env.WDYR_WS_PORT) || DEFAULT_WS_PORT);
|
|
552
|
-
const transport = new StdioServerTransport();
|
|
553
|
-
await server.connect(transport);
|
|
554
|
-
console.error("[wdyr-mcp] MCP server running on stdio");
|
|
555
|
-
let shuttingDown = false;
|
|
556
|
-
async function shutdown() {
|
|
557
|
-
if (shuttingDown) return;
|
|
558
|
-
shuttingDown = true;
|
|
559
|
-
console.error("[wdyr-mcp] Shutting down…");
|
|
560
|
-
wss?.close();
|
|
561
|
-
await server.close();
|
|
562
|
-
process.exit(0);
|
|
563
|
-
}
|
|
564
|
-
process.stdin.on("end", shutdown);
|
|
565
|
-
process.on("SIGTERM", shutdown);
|
|
566
|
-
process.on("SIGINT", shutdown);
|
|
567
|
-
}
|
|
568
|
-
main().catch((error) => {
|
|
569
|
-
console.error("[wdyr-mcp] Fatal error:", error);
|
|
570
|
-
process.exit(1);
|
|
571
|
-
});
|
|
572
|
-
//#endregion
|
|
573
|
-
export {};
|
|
2
|
+
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as n}from"zod";import{existsSync as r,mkdirSync as i,readFileSync as a,readdirSync as o,unlinkSync as s,writeFileSync as c}from"node:fs";import{homedir as l}from"node:os";import{join as u}from"node:path";import d from"xxhash-wasm";import f from"node:http";import{Server as p}from"socket.io";const m=`@@dict`,h=`@@ref:`;let g;const _=d().then(e=>{g=e.h64ToString});function v(){return _}function y(e){return g(JSON.stringify(e))}function b(e){return typeof e==`object`&&!!e}function x(e,t){return e?e.map(e=>{let{prevValue:n,nextValue:r}=e;if(b(n)){let e=y(n);t[e]??=n,n=`${h}${e}`}if(b(r)){let e=y(r);t[e]??=r,r=`${h}${e}`}return n===e.prevValue&&r===e.nextValue?e:{...e,prevValue:n,nextValue:r}}):!1}function S(e,t){let{propsDifferences:n,stateDifferences:r,hookDifferences:i}=e.reason,a=x(n,t),o=x(r,t),s=x(i,t);return a===n&&o===r&&s===i?e:{...e,reason:{propsDifferences:a,stateDifferences:o,hookDifferences:s}}}function C(e,t){return e?e.map(e=>{let{prevValue:n,nextValue:r}=e;return typeof n==`string`&&n.startsWith(h)&&(n=t[n.slice(6)]??n),typeof r==`string`&&r.startsWith(h)&&(r=t[r.slice(6)]??r),n===e.prevValue&&r===e.nextValue?e:{...e,prevValue:n,nextValue:r}}):!1}function w(e,t){let{propsDifferences:n,stateDifferences:r,hookDifferences:i}=e.reason,a=C(n,t),o=C(r,t),s=C(i,t);return a===n&&o===r&&s===i?e:{...e,reason:{propsDifferences:a,stateDifferences:o,hookDifferences:s}}}function T(e){if(!r(e))return[];let t=a(e,`utf-8`).split(`
|
|
3
|
+
`).filter(Boolean);if(t.length===0)return[];let n,i=0,o=JSON.parse(t[0]);`@@dict`in o&&(n=o[m],i=1);let s=t.slice(i).map(e=>JSON.parse(e));return n?s.map(e=>w(e,n)):s}function E(e){return e.replaceAll(/[^a-zA-Z0-9_.-]/g,`_`)}function D(e){return{project:e.projectId,displayName:e.displayName,reason:e.reason,...e.hookName!=null&&{hookName:e.hookName},...e.commitId!=null&&{commitId:e.commitId},...e.timestamp!=null&&{timestamp:e.timestamp},...e.stackFrames!=null&&e.stackFrames.length>0&&{stackFrames:e.stackFrames}}}const O=`nocommit`,k=new class{dir;buffers=new Map;timers=new Map;dicts=new Map;bufferMeta=new Map;trackedComponents=new Map;wdyrConfigs=new Map;constructor(e){this.dir=e??u(l(),`.wdyr-mcp`,`renders`),i(this.dir,{recursive:!0})}addRender(e,t,n){let r={...e,projectId:t,timestamp:Date.now(),...n!=null&&{commitId:n}},i=this.bufferKey(t,n),a=this.buffers.get(i);a||(a=[],this.buffers.set(i,a),this.bufferMeta.set(i,{projectId:t,commitId:n})),a.push(r);let o=this.timers.get(i);o&&clearTimeout(o),this.timers.set(i,setTimeout(()=>{this.flushAsync(t,n).catch(e=>console.error(`[wdyr-mcp] flush error for ${i}:`,e))},200))}async flushAsync(e,t){await v(),this.flush(e,t)}flush(e,t){if(e!=null&&t!==void 0)this.flushBuffer(this.bufferKey(e,t));else if(e!=null)for(let t of this.bufferKeysForProject(e))this.flushBuffer(t);else for(let e of[...this.buffers.keys()])this.flushBuffer(e)}flushBuffer(e){let t=this.buffers.get(e);if(!t||t.length===0)return;let n=this.bufferMeta.get(e);if(!n)return;let r=this.dicts.get(e);r||(r={},this.dicts.set(e,r));let i=t.map(e=>S(e,r)),a=this.commitFile(n.projectId,n.commitId),o=this.readDataLines(a),s=i.map(e=>JSON.stringify(e)),l=[...o,...s];c(a,`${(Object.keys(r).length>0?[JSON.stringify({[m]:r}),...l]:l).join(`
|
|
4
|
+
`)}\n`),t.length=0;let u=this.timers.get(e);u&&(clearTimeout(u),this.timers.delete(e))}readDataLines(e){return r(e)?a(e,`utf-8`).split(`
|
|
5
|
+
`).filter(e=>!(!e||e.startsWith(`{"@@dict"`))):[]}getAllRenders(e){return this.flush(e),e?this.projectFiles(e).flatMap(e=>T(u(this.dir,e)).map(D)):this.jsonlFiles().flatMap(e=>T(u(this.dir,e)).map(D))}getRendersByComponent(e,t){return this.getAllRenders(t).filter(t=>t.displayName===e)}clearRenders(e){if(e){for(let t of this.bufferKeysForProject(e)){this.buffers.delete(t),this.dicts.delete(t),this.bufferMeta.delete(t);let e=this.timers.get(t);e&&(clearTimeout(e),this.timers.delete(t))}for(let t of this.projectFiles(e))s(u(this.dir,t))}else{for(let[,e]of this.timers)clearTimeout(e);this.buffers.clear(),this.timers.clear(),this.dicts.clear(),this.bufferMeta.clear();for(let e of this.jsonlFiles())s(u(this.dir,e))}}clearRendersByComponent(e,t){this.flush(t);let n=t?this.projectFiles(t):this.jsonlFiles(),r=0;for(let t of n){let n=u(this.dir,t),i=T(n),a=i.length,o=i.filter(t=>t.displayName!==e);r+=a-o.length,o.length===0?(s(n),this.clearBuffersForFile(t)):o.length<a&&(this.rewriteFile(n,o),this.clearBuffersForFile(t))}return r}clearRendersByCommit(e,t){this.flush(t);let n=t?this.projectFiles(t):this.jsonlFiles(),r=0;for(let t of n){let n=this.parseFilename(t);n?.commitId!=null&&n.commitId<e&&(s(u(this.dir,t)),this.clearBuffersForFile(t),r++)}return r}getProjects(){this.flush();let e=new Set,t=new Set;for(let n of this.jsonlFiles()){let r=this.parseFilename(n);if(!r||t.has(r.projectSanitized))continue;t.add(r.projectSanitized);let i=a(u(this.dir,n),`utf-8`).split(`
|
|
6
|
+
`);for(let t of i){if(!t)continue;let n=JSON.parse(t);if(!(`@@dict`in n)){e.add(n.projectId);break}}}return[...e]}getCommitIds(e){this.flush(e);let t=e?this.projectFiles(e):this.jsonlFiles(),n=new Set;for(let e of t){let t=this.parseFilename(e);t?.commitId!=null&&n.add(t.commitId)}return[...n].sort((e,t)=>e-t)}getCommits(e){this.flush(e);let t=e?this.projectFiles(e):this.jsonlFiles(),n=[];for(let e of t){let t=this.parseFilename(e);if(t?.commitId==null)continue;let r=T(u(this.dir,e));r.length!==0&&n.push({commitId:t.commitId,timestamp:r.find(e=>e.timestamp!=null)?.timestamp??null,renderCount:r.length,components:[...new Set(r.map(e=>e.displayName))]})}return n.sort((e,t)=>e.commitId-t.commitId)}getRendersByCommit(e,t){if(t)return this.flush(t,e),T(this.commitFile(t,e)).map(D);this.flush();let n=`_commit_${e}.jsonl`;return this.jsonlFiles().filter(e=>e.endsWith(n)).flatMap(e=>T(u(this.dir,e)).map(D))}getSummary(e){let t=this.getAllRenders(e),n={};for(let e of t){n[e.project]??={};let t=n[e.project];t[e.displayName]??={count:0,reasons:{props:0,state:0,hooks:0}};let r=t[e.displayName];r.count++,Array.isArray(e.reason.propsDifferences)&&r.reasons.props++,Array.isArray(e.reason.stateDifferences)&&r.reasons.state++,Array.isArray(e.reason.hookDifferences)&&r.reasons.hooks++}return n}getSummaryByCommit(e){let t=this.getAllRenders(e),n={};for(let e of t){if(e.commitId==null)continue;n[e.project]??={},n[e.project][e.commitId]??={};let t=n[e.project][e.commitId];t[e.displayName]??={count:0,reasons:{props:0,state:0,hooks:0}};let r=t[e.displayName];r.count++,Array.isArray(e.reason.propsDifferences)&&r.reasons.props++,Array.isArray(e.reason.stateDifferences)&&r.reasons.state++,Array.isArray(e.reason.hookDifferences)&&r.reasons.hooks++}return n}setTrackedComponents(e,t){this.trackedComponents.set(t,e)}getTrackedComponents(e){let t={},n=e?[e]:this.getProjects();for(let e of n){let n=[...new Set(this.getAllRenders(e).map(e=>e.displayName))];t[e]={registered:this.trackedComponents.get(e)??[],observed:n}}return t}setWdyrConfig(e,t){this.wdyrConfigs.set(t,e)}getWdyrConfig(e){let t={};if(e){let n=this.wdyrConfigs.get(e);n&&(t[e]=n)}else for(let[e,n]of this.wdyrConfigs)t[e]=n;return t}rewriteFile(e,t){c(e,`${t.map(e=>JSON.stringify(e)).join(`
|
|
7
|
+
`)}\n`)}clearBuffersForFile(e){let t=this.parseFilename(e);if(t)for(let[e,n]of this.bufferMeta){if(E(n.projectId)!==t.projectSanitized||!(t.commitId==null?n.commitId==null:n.commitId===t.commitId))continue;this.buffers.delete(e),this.dicts.delete(e),this.bufferMeta.delete(e);let r=this.timers.get(e);r&&(clearTimeout(r),this.timers.delete(e))}}bufferKey(e,t){return`${e}\0${t??O}`}bufferKeysForProject(e){let t=`${e}\0`;return[...this.buffers.keys()].filter(e=>e.startsWith(t))}commitFile(e,t){let n=E(e),r=t==null?`_${O}`:`_commit_${t}`;return u(this.dir,`${n}${r}.jsonl`)}projectFiles(e){let t=E(e);return o(this.dir).filter(e=>e.startsWith(t)&&e.endsWith(`.jsonl`))}parseFilename(e){if(!e.endsWith(`.jsonl`))return null;let t=e.slice(0,-6),n=t.match(/^(.+)_commit_(\d+)$/);if(n)return{projectSanitized:n[1],commitId:Number(n[2])};let r=t.match(/^(.+)_nocommit$/);return r?{projectSanitized:r[1]}:{projectSanitized:t}}jsonlFiles(){return o(this.dir).filter(e=>e.endsWith(`.jsonl`))}};function A(e){if(e)return{projectId:e};let t=k.getProjects();return t.length===0?{projectId:void 0}:t.length===1?{projectId:t[0]}:{projectId:void 0,error:[`Multiple projects are recording render data. Ask the user which project they are working on (e.g. their dev server URL like http://localhost:3000).`,``,`Active projects:`,...t.map(e=>`- ${e}`)].join(`
|
|
8
|
+
`)}}function j(e){return{content:[{type:`text`,text:e}]}}function M(e){e.registerTool(`clear_renders`,{title:`Clear Renders`,description:`Clears collected render data. Supports filtering by component name or by commit ID threshold. When no filter is given, clears all data. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.`,inputSchema:{project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`),component:n.string().optional().describe(`Clear only renders for this component (by displayName).`),beforeCommit:n.number().optional().describe(`Clear all renders from commits with an ID strictly less than this value.`)}},async({project:e,component:t,beforeCommit:n})=>{let r=A(e);return r.error?j(r.error):t?j(`Cleared ${k.clearRendersByComponent(t,r.projectId)} render(s) for component "${t}".`):n==null?(k.clearRenders(r.projectId),j(r.projectId?`Render data cleared for ${r.projectId}.`:`All render data cleared.`)):j(`Cleared renders from ${k.clearRendersByCommit(n,r.projectId)} commit file(s) before commit #${n}.`)})}function N(e){e.registerTool(`get_commits`,{title:`Get Commits`,description:`Returns a list of React commit IDs that have recorded render data for a project. Use these IDs with get_renders_by_commit to inspect individual commits.`,inputSchema:{project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`)}},async({project:e})=>{let t=A(e);if(t.error)return j(t.error);let n=k.getCommits(t.projectId);return n.length===0?j(`No commits recorded yet. Make sure the browser is connected and triggering re-renders.`):j(JSON.stringify(n,null,2))})}function P(e){e.registerTool(`get_projects`,{title:`Get Projects`,description:`Returns a list of project identifiers (browser origin URLs) that have recorded render data.`,inputSchema:{}},async()=>{let e=k.getProjects();return e.length===0?j(`No projects have recorded render data yet.`):j(`Active projects:\n${e.map(e=>`- ${e}`).join(`
|
|
9
|
+
`)}`)})}function F(e){e.registerTool(`get_render_summary`,{title:`Get Render Summary`,description:`Returns a summary of re-renders grouped by component name with counts. Use groupBy: 'commit' to get per-commit breakdowns instead of a single aggregate. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.`,inputSchema:{project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`),groupBy:n.enum([`commit`]).optional().describe(`Group results by commit. When set to 'commit', returns per-commit render summaries instead of a single aggregate.`)}},async({project:e,groupBy:t})=>{let n=A(e);return n.error?j(n.error):t===`commit`?R(n.projectId):L(n.projectId)})}function I(e){let t=[];return e.props>0&&t.push(`props: ${e.props}`),e.state>0&&t.push(`state: ${e.state}`),e.hooks>0&&t.push(`hooks: ${e.hooks}`),t.length>0?` — ${t.join(`, `)}`:``}function L(e){let t=k.getSummary(e);if(Object.keys(t).length===0)return j(`No renders recorded yet.`);let n=[];for(let[e,r]of Object.entries(t)){n.push(`[${e}]`);for(let[e,{count:t,reasons:i}]of Object.entries(r))n.push(` ${e}: ${t} re-render(s)${I(i)}`)}return j(`Re-render summary:\n\n${n.join(`
|
|
10
|
+
`)}`)}function R(e){let t=k.getSummaryByCommit(e);if(Object.keys(t).length===0)return j(`No renders with commit IDs recorded yet.`);let n=[];for(let[e,r]of Object.entries(t)){n.push(`[${e}]`);let t=Object.keys(r).map(Number).sort((e,t)=>e-t);for(let e of t){let t=r[e],i=Object.values(t).reduce((e,t)=>e+t.count,0);n.push(` Commit #${e} (${i} re-render(s)):`);for(let[e,{count:r,reasons:i}]of Object.entries(t))n.push(` ${e}: ${r}${I(i)}`)}}return j(`Re-render summary (by commit):\n\n${n.join(`
|
|
11
|
+
`)}`)}function z(e){e.registerTool(`get_renders_by_commit`,{title:`Get Renders by Commit`,description:`Returns all re-renders for a specific React commit ID, including stack traces that show the hook chain and component tree that triggered each render. Use the stackFrames field to locate the exact source file and line. Use get_commits first to discover available commit IDs.`,inputSchema:{commitId:n.number().describe(`The React commit ID to filter by.`),component:n.string().optional().describe(`Filter by component name. Omit to get all renders.`),project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`)}},async({commitId:e,component:t,project:n})=>{let r=A(n);if(r.error)return j(r.error);let i=k.getRendersByCommit(e,r.projectId);return t&&(i=i.filter(e=>e.displayName===t)),i.length===0?j(t?`No renders recorded for component "${t}" in commit ${e}.`:`No renders recorded for commit ${e}.`):j(JSON.stringify(i,null,2))})}function B(e){e.registerTool(`get_renders`,{title:`Get Renders`,description:`Returns all re-renders collected from the browser, including stack traces that show the hook chain and component tree that triggered each render. Use the stackFrames field to locate the exact source file and line. If multiple projects are active and no project is specified, the tool will ask you to disambiguate by asking the user for their dev server URL.`,inputSchema:{component:n.string().optional().describe(`Filter by component name. Omit to get all renders.`),project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`)}},async({component:e,project:t})=>{let n=A(t);if(n.error)return j(n.error);let r=e?k.getRendersByComponent(e,n.projectId):k.getAllRenders(n.projectId);return r.length===0?j(e?`No renders recorded for "${e}".`:`No renders recorded yet. Make sure the browser is connected and triggering re-renders.`):j(JSON.stringify(r,null,2))})}function V(e){let t=[];if(e.include?.length){t.push(` include:`);for(let n of e.include)t.push(` - /${n}/`)}if(e.exclude?.length){t.push(` exclude:`);for(let n of e.exclude)t.push(` - /${n}/`)}if(e.trackAllPureComponents!=null&&t.push(` trackAllPureComponents: ${e.trackAllPureComponents}`),e.trackHooks!=null&&t.push(` trackHooks: ${e.trackHooks}`),e.trackExtraHooks?.length){t.push(` trackExtraHooks:`);for(let n of e.trackExtraHooks)t.push(` - ${n}`)}return e.logOnDifferentValues!=null&&t.push(` logOnDifferentValues: ${e.logOnDifferentValues}`),e.logOwnerReasons!=null&&t.push(` logOwnerReasons: ${e.logOwnerReasons}`),t}function H(e){e.registerTool(`get_tracked_components`,{title:`Get Tracked Components`,description:`Returns the why-did-you-render configuration for the connected project, including include/exclude filters and tracking options. Also shows components observed in render data. If multiple projects are active and no project is specified, the tool will ask you to disambiguate.`,inputSchema:{project:n.string().optional().describe(`Project identifier (the browser's origin URL, e.g. http://localhost:3000). Omit to auto-detect.`)}},async({project:e})=>{let t=A(e);if(t.error)return j(t.error);let n=k.getWdyrConfig(t.projectId),r=k.getTrackedComponents(t.projectId),i=Object.keys(n).length>0,a=Object.keys(r).length>0;if(!i&&!a)return j(`No configuration or tracked components found. Make sure the browser is connected and triggering re-renders.`);let o=[],s=new Set([...Object.keys(n),...Object.keys(r)]);for(let e of s){o.push(`[${e}]`);let t=n[e];if(t){o.push(`Configuration:`);let e=V(t);e.length>0?o.push(...e):o.push(` (default options)`)}let i=r[e];if(i?.observed.length){o.push(`Observed in renders:`);for(let e of i.observed)o.push(` - ${e}`)}}return j(o.join(`
|
|
12
|
+
`))})}function U(e){B(e),F(e),N(e),z(e),P(e),H(e),M(e)}function W(e){let t=f.createServer(),n=new p(t,{cors:{origin:`*`},serveClient:!1,transports:[`websocket`],maxHttpBufferSize:5e7});return n.on(`connection`,t=>{console.error(`[wdyr-mcp] browser connected (http://localhost:${e})`),t.data.projectId=null,t.on(`render`,(e,n,r)=>{t.data.projectId=n,k.addRender(e,n,r)}),t.on(`render-batch`,(e,n,r)=>{t.data.projectId=n;for(let t of e)k.addRender(t,n,r)}),t.on(`register`,(e,n)=>{t.data.projectId=n,k.setTrackedComponents(e,n)}),t.on(`config`,(e,n)=>{t.data.projectId=n,k.setWdyrConfig(e,n)}),t.on(`disconnect`,()=>{console.error(`[wdyr-mcp] browser disconnected`);let e=t.data.projectId;e&&([...n.sockets.sockets.values()].some(n=>n.id!==t.id&&n.data.projectId===e)||(console.error(`[wdyr-mcp] last client for ${e} disconnected, clearing render data`),k.clearRenders(e)))})}),t.on(`error`,t=>{t.code===`EADDRINUSE`?console.error(`[wdyr-mcp] Port ${e} already in use, another instance owns the WS server. Skipping.`):console.error(`[wdyr-mcp] server error:`,t)}),t.listen(e,`127.0.0.1`,()=>{console.error(`[wdyr-mcp] socket.io server listening on http://localhost:${e}`)}),n}const G=new e({name:`why-did-you-render`,version:`0.0.0`});U(G);async function K(){let e=W(Number(process.env.WDYR_WS_PORT)||4649),n=new t;await G.connect(n),console.error(`[wdyr-mcp] MCP server running on stdio`);let r=!1;async function i(){r||(r=!0,console.error(`[wdyr-mcp] Shutting down…`),e?.close(),await G.close(),process.exit(0))}process.stdin.on(`end`,i),process.on(`SIGTERM`,i),process.on(`SIGINT`,i)}K().catch(e=>{console.error(`[wdyr-mcp] Fatal error:`,e),process.exit(1)});export{};
|
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
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
@@ -34,9 +34,7 @@
|
|
|
34
34
|
"require": "./dist/client/index.cjs"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
-
"files": [
|
|
38
|
-
"dist"
|
|
39
|
-
],
|
|
37
|
+
"files": ["dist"],
|
|
40
38
|
"scripts": {
|
|
41
39
|
"build": "tsdown",
|
|
42
40
|
"dev": "tsdown --watch",
|
|
@@ -48,17 +46,20 @@
|
|
|
48
46
|
"test:coverage": "vitest run --coverage"
|
|
49
47
|
},
|
|
50
48
|
"dependencies": {
|
|
49
|
+
"@jridgewell/trace-mapping": "^0.3.31",
|
|
51
50
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
52
|
-
"
|
|
51
|
+
"error-stack-parser": "^2.1.4",
|
|
52
|
+
"socket.io": "^4.8.3",
|
|
53
|
+
"socket.io-client": "^4.8.3",
|
|
53
54
|
"xxhash-wasm": "^1.1.0",
|
|
54
55
|
"zod": "^3.24.4"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@biomejs/biome": "^1.9.4",
|
|
58
59
|
"@semantic-release/changelog": "^6.0.3",
|
|
60
|
+
"@semantic-release/exec": "^7.1.0",
|
|
59
61
|
"@semantic-release/git": "^10.0.1",
|
|
60
62
|
"@types/node": "^22.14.1",
|
|
61
|
-
"@types/ws": "^8.18.0",
|
|
62
63
|
"@vitest/coverage-v8": "^4.1.2",
|
|
63
64
|
"semantic-release": "^25.0.3",
|
|
64
65
|
"tsdown": "^0.12.4",
|
|
@@ -72,5 +73,7 @@
|
|
|
72
73
|
"publishConfig": {
|
|
73
74
|
"access": "public"
|
|
74
75
|
},
|
|
75
|
-
"pnpm": {
|
|
76
|
+
"pnpm": {
|
|
77
|
+
"ignoredBuiltDependencies": ["@biomejs/biome"]
|
|
78
|
+
}
|
|
76
79
|
}
|