@lensmcp/react-instrumentation 1.18.4 → 1.18.6
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/index.js +1 -10
- package/lib/babel-plugin.js +1 -259
- package/lib/context.js +1 -6
- package/lib/fiber-renders.js +1 -237
- package/lib/flow-fetch.js +1 -143
- package/lib/hoc.js +1 -45
- package/lib/identity.js +1 -74
- package/lib/lensmcp-root.js +1 -59
- package/lib/publish.js +1 -236
- package/lib/trace-slot.js +1 -67
- package/lib/traced-hooks.js +1 -143
- package/lib/types.js +1 -1
- package/package.json +2 -2
package/lib/flow-fetch.js
CHANGED
|
@@ -1,143 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Flow-header propagation across the network boundary. When a request is
|
|
3
|
-
* made while a flow is active (inside `runInFlow`/`withFlow`), inject
|
|
4
|
-
* `x-lensmcp-flow-id` + `x-lensmcp-origin-node-id` headers so the backend
|
|
5
|
-
* (`TraceInterceptor` in `@lensmcp/nest-instrumentation`) stamps its
|
|
6
|
-
* server-request + span + db-query events with the SAME flowId. That is
|
|
7
|
-
* what lets `story.compile` and `graph.*` correlate a single user action
|
|
8
|
-
* all the way from the click to the DB query.
|
|
9
|
-
*
|
|
10
|
-
* Works in the browser and in Node (both have a global `fetch`). The
|
|
11
|
-
* header is read at *call time* (synchronous), so a `fetch()` invoked
|
|
12
|
-
* inside the synchronous portion of a flow is tagged even though the
|
|
13
|
-
* awaited continuation runs after the flow stack unwinds.
|
|
14
|
-
*/
|
|
15
|
-
import { activeFlow, beginBackgroundFlow, extendFlowWindow, publish } from './publish.js';
|
|
16
|
-
/**
|
|
17
|
-
* Monkeypatch `globalThis.fetch` to inject flow headers. Returns an
|
|
18
|
-
* uninstall function that restores the original. No-op (returns a no-op
|
|
19
|
-
* uninstaller) if there's no global fetch.
|
|
20
|
-
*/
|
|
21
|
-
let pageLoadFlowStarted = false;
|
|
22
|
-
export function installFlowFetch(options = {}) {
|
|
23
|
-
const g = globalThis;
|
|
24
|
-
if (typeof g.fetch !== 'function')
|
|
25
|
-
return () => undefined;
|
|
26
|
-
// installFlowFetch runs at ENTRY-MODULE eval (the babel transform injects
|
|
27
|
-
// it before the root render) — earlier than any mount effect. That makes
|
|
28
|
-
// it the one reliable place to declare the PAGE LOAD itself a flow, so the
|
|
29
|
-
// boot-time fetches (/me, list queries…) and the renders they trigger all
|
|
30
|
-
// stitch into one trace. A refresh without this produced NO flow at all.
|
|
31
|
-
if (!pageLoadFlowStarted && typeof location !== 'undefined' && typeof document !== 'undefined') {
|
|
32
|
-
pageLoadFlowStarted = true;
|
|
33
|
-
beginBackgroundFlow({ originType: 'page-load', originNodeId: `page:${location.pathname}` });
|
|
34
|
-
}
|
|
35
|
-
const flowHeader = options.flowHeader ?? 'x-lensmcp-flow-id';
|
|
36
|
-
const originHeader = options.originHeader ?? 'x-lensmcp-origin-node-id';
|
|
37
|
-
const original = g.fetch.bind(globalThis);
|
|
38
|
-
// Tell the injected client runtime's own (inner) fetch wrapper to stop
|
|
39
|
-
// emitting: from here on THIS wrapper publishes the network events, and
|
|
40
|
-
// both firing double-counts every request in the timeline.
|
|
41
|
-
globalThis['__LENSMCP_FLOW_FETCH__'] = true;
|
|
42
|
-
const patched = ((input, init) => {
|
|
43
|
-
// Sync stack (a wrapped handler) OR the causality window (page-load,
|
|
44
|
-
// post-response continuations) — both mean this fetch belongs to a flow.
|
|
45
|
-
const flow = activeFlow();
|
|
46
|
-
if (!flow?.flowId)
|
|
47
|
-
return original(input, init);
|
|
48
|
-
// Merge onto any caller-supplied headers (+ a Request's own headers).
|
|
49
|
-
const requestHeaders = typeof input === 'object' && input !== null && 'headers' in input
|
|
50
|
-
? input.headers
|
|
51
|
-
: undefined;
|
|
52
|
-
const headers = new Headers(init?.headers ?? requestHeaders ?? undefined);
|
|
53
|
-
if (!headers.has(flowHeader))
|
|
54
|
-
headers.set(flowHeader, flow.flowId);
|
|
55
|
-
if (flow.originNodeId && !headers.has(originHeader)) {
|
|
56
|
-
headers.set(originHeader, flow.originNodeId);
|
|
57
|
-
}
|
|
58
|
-
const result = original(input, { ...init, headers });
|
|
59
|
-
publishFetchLeg(flow, input, init, result);
|
|
60
|
-
return result;
|
|
61
|
-
});
|
|
62
|
-
g.fetch = patched;
|
|
63
|
-
return () => {
|
|
64
|
-
g.fetch = original;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Publish the flow's network leg: a start event (synchronous — stamped from
|
|
69
|
-
* the flow stack) and a completion event (asynchronous — the flow has
|
|
70
|
-
* unwound by then, so its context is attached explicitly). Together with
|
|
71
|
-
* the backend's server-request event (same flowId via the header) this is
|
|
72
|
-
* what lets a story read click → fetch → server → DB.
|
|
73
|
-
*/
|
|
74
|
-
function publishFetchLeg(flow, input, init, result) {
|
|
75
|
-
const url = typeof input === 'string'
|
|
76
|
-
? input
|
|
77
|
-
: input instanceof URL
|
|
78
|
-
? input.href
|
|
79
|
-
: (input.url ?? String(input));
|
|
80
|
-
const method = init?.method ??
|
|
81
|
-
(typeof input === 'object' && input !== null && 'method' in input
|
|
82
|
-
? (input.method ?? 'GET')
|
|
83
|
-
: 'GET');
|
|
84
|
-
const startedAt = Date.now();
|
|
85
|
-
const flowContext = {
|
|
86
|
-
flowId: flow.flowId,
|
|
87
|
-
...(flow.originType ? { originType: flow.originType } : {}),
|
|
88
|
-
...(flow.originNodeId ? { originNodeId: flow.originNodeId } : {}),
|
|
89
|
-
};
|
|
90
|
-
publish({
|
|
91
|
-
source: 'react',
|
|
92
|
-
category: 'network',
|
|
93
|
-
severity: 'info',
|
|
94
|
-
title: `fetch ${method} ${url}`,
|
|
95
|
-
fingerprint: `flow-fetch:${method}:${url}`,
|
|
96
|
-
raw: { kind: 'fetch-start', url, method },
|
|
97
|
-
});
|
|
98
|
-
// Observe on a branch — never alter the caller's promise chain.
|
|
99
|
-
void result.then((res) => {
|
|
100
|
-
// Re-open the causality window: the state update + re-render that
|
|
101
|
-
// consume this response land in the next ticks and belong to this flow.
|
|
102
|
-
extendFlowWindow(flow);
|
|
103
|
-
publish({
|
|
104
|
-
source: 'react',
|
|
105
|
-
category: 'network',
|
|
106
|
-
// A 4xx/5xx IS an error. This said `warning`, and the runtime reducer
|
|
107
|
-
// routes only `severity === 'error'` into
|
|
108
|
-
// `runtime://browser/failed-requests` — so every failed request in an
|
|
109
|
-
// app using flow-fetch was invisible to the lens. Measured on
|
|
110
|
-
// foodguard: 116 non-ok responses (98×401, 10×503, 8×502) captured in
|
|
111
|
-
// the event log, none of them reported.
|
|
112
|
-
severity: res.ok ? 'info' : 'error',
|
|
113
|
-
title: `fetch ${method} ${url} → ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`,
|
|
114
|
-
// The STATUS is part of the identity. Without it a 200 and a 401 to
|
|
115
|
-
// the same endpoint shared one fingerprint, so five distinct failures
|
|
116
|
-
// deduped into whatever happened to be seen first.
|
|
117
|
-
fingerprint: `flow-fetch:${method}:${url}:${res.status}`,
|
|
118
|
-
context: flowContext,
|
|
119
|
-
raw: {
|
|
120
|
-
kind: 'fetch-done',
|
|
121
|
-
url,
|
|
122
|
-
method,
|
|
123
|
-
status: res.status,
|
|
124
|
-
statusText: res.statusText,
|
|
125
|
-
ok: res.ok,
|
|
126
|
-
startedAt,
|
|
127
|
-
durationMs: Date.now() - startedAt,
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
}, (err) => {
|
|
131
|
-
extendFlowWindow(flow);
|
|
132
|
-
publish({
|
|
133
|
-
source: 'react',
|
|
134
|
-
category: 'network',
|
|
135
|
-
severity: 'error',
|
|
136
|
-
title: `fetch ${method} ${url} failed`,
|
|
137
|
-
message: err instanceof Error ? err.message : String(err),
|
|
138
|
-
fingerprint: `flow-fetch:${method}:${url}:error`,
|
|
139
|
-
context: flowContext,
|
|
140
|
-
raw: { kind: 'fetch-error', url, method, startedAt, durationMs: Date.now() - startedAt },
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
1
|
+
"use strict";var $=Object.defineProperty;var l=(o,e)=>$(o,"name",{value:e,configurable:!0});var m=Object.defineProperty,f=l((o,e)=>m(o,"name",{value:e,configurable:!0}),"f");import{activeFlow as T,beginBackgroundFlow as b,extendFlowWindow as g,publish as h}from"./publish.js";let w=!1;export function installFlowFetch(o={}){const e=globalThis;if(typeof e.fetch!="function")return()=>{};!w&&typeof location<"u"&&typeof document<"u"&&(w=!0,b({originType:"page-load",originNodeId:`page:${location.pathname}`}));const c=o.flowHeader??"x-lensmcp-flow-id",d=o.originHeader??"x-lensmcp-origin-node-id",r=e.fetch.bind(globalThis);globalThis.__LENSMCP_FLOW_FETCH__=!0;const n=f(((i,s)=>{const t=T();if(!t?.flowId)return r(i,s);const y=typeof i=="object"&&i!==null&&"headers"in i?i.headers:void 0,a=new Headers(s?.headers??y??void 0);a.has(c)||a.set(c,t.flowId),t.originNodeId&&!a.has(d)&&a.set(d,t.originNodeId);const u=r(i,{...s,headers:a});return p(t,i,s,u),u}),"patched");return e.fetch=n,()=>{e.fetch=r}}l(installFlowFetch,"installFlowFetch"),f(installFlowFetch,"installFlowFetch");function p(o,e,c,d){const r=typeof e=="string"?e:e instanceof URL?e.href:e.url??String(e),n=c?.method??(typeof e=="object"&&e!==null&&"method"in e?e.method??"GET":"GET"),i=Date.now(),s={flowId:o.flowId,...o.originType?{originType:o.originType}:{},...o.originNodeId?{originNodeId:o.originNodeId}:{}};h({source:"react",category:"network",severity:"info",title:`fetch ${n} ${r}`,fingerprint:`flow-fetch:${n}:${r}`,raw:{kind:"fetch-start",url:r,method:n}}),d.then(t=>{g(o),h({source:"react",category:"network",severity:t.ok?"info":"error",title:`fetch ${n} ${r} \u2192 ${t.status}${t.statusText?` ${t.statusText}`:""}`,fingerprint:`flow-fetch:${n}:${r}:${t.status}`,context:s,raw:{kind:"fetch-done",url:r,method:n,status:t.status,statusText:t.statusText,ok:t.ok,startedAt:i,durationMs:Date.now()-i}})},t=>{g(o),h({source:"react",category:"network",severity:"error",title:`fetch ${n} ${r} failed`,message:t instanceof Error?t.message:String(t),fingerprint:`flow-fetch:${n}:${r}:error`,context:s,raw:{kind:"fetch-error",url:r,method:n,startedAt:i,durationMs:Date.now()-i}})})}l(p,"m"),f(p,"publishFetchLeg");
|
package/lib/hoc.js
CHANGED
|
@@ -1,45 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Profiler, } from 'react';
|
|
3
|
-
import { publish } from './publish.js';
|
|
4
|
-
import { makeInstanceId } from './identity.js';
|
|
5
|
-
/**
|
|
6
|
-
* `withTraceComponent(MyComponent)` — opt-in tracer for projects that
|
|
7
|
-
* can't enable the global Babel transform. Wraps the component in a
|
|
8
|
-
* Profiler and emits a `render` event on every commit.
|
|
9
|
-
*/
|
|
10
|
-
export function withTraceComponent(Wrapped, opts = {}) {
|
|
11
|
-
const name = opts.componentName ?? Wrapped.displayName ?? Wrapped.name ?? 'TracedComponent';
|
|
12
|
-
const file = opts.source ?? '(unknown)';
|
|
13
|
-
const logicalId = `react:component:${file}:${name}`;
|
|
14
|
-
function Traced(props) {
|
|
15
|
-
let lastRenderId;
|
|
16
|
-
const onRender = (_id, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
|
17
|
-
const { instanceId } = makeInstanceId(logicalId);
|
|
18
|
-
const record = {
|
|
19
|
-
id: instanceId,
|
|
20
|
-
parentRenderId: lastRenderId,
|
|
21
|
-
componentInstanceId: instanceId,
|
|
22
|
-
componentLogicalId: logicalId,
|
|
23
|
-
componentName: name,
|
|
24
|
-
phase: phase === 'mount' ? 'mount' : 'update',
|
|
25
|
-
actualDurationMs: actualDuration,
|
|
26
|
-
baseDurationMs: baseDuration,
|
|
27
|
-
startTime,
|
|
28
|
-
commitTime,
|
|
29
|
-
why: [{ type: 'force', reason: 'withTraceComponent' }],
|
|
30
|
-
};
|
|
31
|
-
lastRenderId = instanceId;
|
|
32
|
-
publish({
|
|
33
|
-
source: 'react',
|
|
34
|
-
category: 'render',
|
|
35
|
-
severity: 'info',
|
|
36
|
-
title: `render ${name} (${phase})`,
|
|
37
|
-
fingerprint: `react-render:${logicalId}`,
|
|
38
|
-
raw: { kind: 'render', render: record },
|
|
39
|
-
});
|
|
40
|
-
};
|
|
41
|
-
return (_jsx(Profiler, { id: logicalId, onRender: onRender, children: _jsx(Wrapped, { ...props }) }));
|
|
42
|
-
}
|
|
43
|
-
Traced.displayName = `Traced(${name})`;
|
|
44
|
-
return Traced;
|
|
45
|
-
}
|
|
1
|
+
"use strict";var I=Object.defineProperty;var o=(e,n)=>I(e,"name",{value:n,configurable:!0});var T=Object.defineProperty,i=o((e,n)=>T(e,"name",{value:n,configurable:!0}),"t");import{jsx as d}from"react/jsx-runtime";import{Profiler as w}from"react";import{publish as $}from"./publish.js";import{makeInstanceId as g}from"./identity.js";export function withTraceComponent(e,n={}){const r=n.componentName??e.displayName??e.name??"TracedComponent",t=`react:component:${n.source??"(unknown)"}:${r}`;function a(p){let m;return d(w,{id:t,onRender:i((b,s,u,f,l,h)=>{const{instanceId:c}=g(t),y={id:c,parentRenderId:m,componentInstanceId:c,componentLogicalId:t,componentName:r,phase:s==="mount"?"mount":"update",actualDurationMs:u,baseDurationMs:f,startTime:l,commitTime:h,why:[{type:"force",reason:"withTraceComponent"}]};m=c,$({source:"react",category:"render",severity:"info",title:`render ${r} (${s})`,fingerprint:`react-render:${t}`,raw:{kind:"render",render:y}})},"onRender"),children:d(e,{...p})})}return o(a,"a"),i(a,"Traced"),a.displayName=`Traced(${r})`,a}o(withTraceComponent,"withTraceComponent"),i(withTraceComponent,"withTraceComponent");
|
package/lib/identity.js
CHANGED
|
@@ -1,74 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Identity helpers — produce stable `logicalId` and per-mount
|
|
3
|
-
* `instanceId` values for components, hooks, and slots. Mirrors the
|
|
4
|
-
* shapes documented in `planning/02-data-model.md#logicalid`.
|
|
5
|
-
*/
|
|
6
|
-
const generations = new Map();
|
|
7
|
-
let entropy = 0;
|
|
8
|
-
export function nextGeneration(logicalId) {
|
|
9
|
-
const g = (generations.get(logicalId) ?? 0) + 1;
|
|
10
|
-
generations.set(logicalId, g);
|
|
11
|
-
return g;
|
|
12
|
-
}
|
|
13
|
-
export function shortHash() {
|
|
14
|
-
entropy = (entropy + 1) % 0xffffffff;
|
|
15
|
-
return ((Date.now() & 0xfff) ^ entropy).toString(36).slice(-4);
|
|
16
|
-
}
|
|
17
|
-
export function componentLogicalId(file, name) {
|
|
18
|
-
return `react:component:${file}:${name}`;
|
|
19
|
-
}
|
|
20
|
-
export function hookLogicalId(file, componentName, hookName, index) {
|
|
21
|
-
return `react:hook:${file}:${componentName}:${hookName}:${index}`;
|
|
22
|
-
}
|
|
23
|
-
export function slotLogicalId(file, line, kind) {
|
|
24
|
-
return `react:slot:${file}:${line}:${kind}`;
|
|
25
|
-
}
|
|
26
|
-
export function makeInstanceId(logicalId) {
|
|
27
|
-
const generation = nextGeneration(logicalId);
|
|
28
|
-
return {
|
|
29
|
-
instanceId: `${logicalId}#${generation}#${shortHash()}`,
|
|
30
|
-
generation,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Cheap deterministic hash over an array of dependency values. Used by
|
|
35
|
-
* the traced hooks to compute `depsHash` so the reducer can decide
|
|
36
|
-
* whether a hook re-ran because of a dep change.
|
|
37
|
-
*/
|
|
38
|
-
export function depsHash(deps) {
|
|
39
|
-
if (!deps)
|
|
40
|
-
return 'no-deps';
|
|
41
|
-
let h = 5381;
|
|
42
|
-
for (const dep of deps) {
|
|
43
|
-
const repr = canonical(dep);
|
|
44
|
-
for (let i = 0; i < repr.length; i++) {
|
|
45
|
-
h = (h * 33) ^ repr.charCodeAt(i);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return (h >>> 0).toString(36);
|
|
49
|
-
}
|
|
50
|
-
function canonical(v) {
|
|
51
|
-
if (v === null)
|
|
52
|
-
return 'null';
|
|
53
|
-
if (v === undefined)
|
|
54
|
-
return 'undefined';
|
|
55
|
-
const t = typeof v;
|
|
56
|
-
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint') {
|
|
57
|
-
return `${t}:${String(v)}`;
|
|
58
|
-
}
|
|
59
|
-
if (t === 'function') {
|
|
60
|
-
const name = v.name ?? 'anonymous';
|
|
61
|
-
return `fn:${name}:${v.toString().length}`;
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
return `obj:${JSON.stringify(v)}`;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return `obj:circular`;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/** Test hook to reset generation counters between smoke iterations. */
|
|
71
|
-
export function resetIdentityState() {
|
|
72
|
-
generations.clear();
|
|
73
|
-
entropy = 0;
|
|
74
|
-
}
|
|
1
|
+
"use strict";var f=Object.defineProperty;var e=(t,n)=>f(t,"name",{value:n,configurable:!0});var l=Object.defineProperty,o=e((t,n)=>l(t,"name",{value:n,configurable:!0}),"e");const u=new Map;let c=0;export function nextGeneration(t){const n=(u.get(t)??0)+1;return u.set(t,n),n}e(nextGeneration,"nextGeneration"),o(nextGeneration,"nextGeneration");export function shortHash(){return c=(c+1)%4294967295,(Date.now()&4095^c).toString(36).slice(-4)}e(shortHash,"shortHash"),o(shortHash,"shortHash");export function componentLogicalId(t,n){return`react:component:${t}:${n}`}e(componentLogicalId,"componentLogicalId"),o(componentLogicalId,"componentLogicalId");export function hookLogicalId(t,n,r,i){return`react:hook:${t}:${n}:${r}:${i}`}e(hookLogicalId,"hookLogicalId"),o(hookLogicalId,"hookLogicalId");export function slotLogicalId(t,n,r){return`react:slot:${t}:${n}:${r}`}e(slotLogicalId,"slotLogicalId"),o(slotLogicalId,"slotLogicalId");export function makeInstanceId(t){const n=nextGeneration(t);return{instanceId:`${t}#${n}#${shortHash()}`,generation:n}}e(makeInstanceId,"makeInstanceId"),o(makeInstanceId,"makeInstanceId");export function depsHash(t){if(!t)return"no-deps";let n=5381;for(const r of t){const i=s(r);for(let a=0;a<i.length;a++)n=n*33^i.charCodeAt(a)}return(n>>>0).toString(36)}e(depsHash,"depsHash"),o(depsHash,"depsHash");function s(t){if(t===null)return"null";if(t===void 0)return"undefined";const n=typeof t;if(n==="string"||n==="number"||n==="boolean"||n==="bigint")return`${n}:${String(t)}`;if(n==="function")return`fn:${t.name??"anonymous"}:${t.toString().length}`;try{return`obj:${JSON.stringify(t)}`}catch{return"obj:circular"}}e(s,"a"),o(s,"canonical");export function resetIdentityState(){u.clear(),c=0}e(resetIdentityState,"resetIdentityState"),o(resetIdentityState,"resetIdentityState");
|
package/lib/lensmcp-root.js
CHANGED
|
@@ -1,59 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Profiler, useMemo, useRef, } from 'react';
|
|
3
|
-
import { LensmcpContext } from './context.js';
|
|
4
|
-
import { publish } from './publish.js';
|
|
5
|
-
import { makeInstanceId } from './identity.js';
|
|
6
|
-
import { installFiberRenderPublisher } from './fiber-renders.js';
|
|
7
|
-
// Per-component render attribution rides the client runtime's DevTools-hook
|
|
8
|
-
// bus (see fiber-renders.ts). Module-scope so importing LensmcpRoot (the
|
|
9
|
-
// babel autoWrapRoot path) is enough — no API change for consumers.
|
|
10
|
-
if (typeof window !== 'undefined')
|
|
11
|
-
installFiberRenderPublisher();
|
|
12
|
-
/**
|
|
13
|
-
* Wraps the React tree in a `<Profiler>` and a context provider so the
|
|
14
|
-
* hook helpers can tag their events with the right renderer / page /
|
|
15
|
-
* route. Drop-in:
|
|
16
|
-
*
|
|
17
|
-
* ReactDOM.createRoot(el).render(
|
|
18
|
-
* <LensmcpRoot page="home" route="/">
|
|
19
|
-
* <App />
|
|
20
|
-
* </LensmcpRoot>
|
|
21
|
-
* );
|
|
22
|
-
*/
|
|
23
|
-
export function LensmcpRoot(props) {
|
|
24
|
-
const rendererId = props.rendererId ?? 'lensmcp-root';
|
|
25
|
-
const ctxValue = useMemo(() => ({ rendererId, page: props.page, route: props.route }), [rendererId, props.page, props.route]);
|
|
26
|
-
const renderIdsRef = useRef(new Map());
|
|
27
|
-
const lastRenderIdRef = useRef(undefined);
|
|
28
|
-
const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
|
29
|
-
const logicalId = `render:${rendererId}:${id}`;
|
|
30
|
-
const { instanceId } = makeInstanceId(logicalId);
|
|
31
|
-
const renderRecord = {
|
|
32
|
-
id: instanceId,
|
|
33
|
-
parentRenderId: lastRenderIdRef.current,
|
|
34
|
-
componentInstanceId: instanceId,
|
|
35
|
-
componentLogicalId: logicalId,
|
|
36
|
-
componentName: id,
|
|
37
|
-
page: props.page,
|
|
38
|
-
route: props.route,
|
|
39
|
-
rendererId,
|
|
40
|
-
phase: phase === 'mount' ? 'mount' : 'update',
|
|
41
|
-
actualDurationMs: actualDuration,
|
|
42
|
-
baseDurationMs: baseDuration,
|
|
43
|
-
startTime,
|
|
44
|
-
commitTime,
|
|
45
|
-
why: [{ type: 'force', reason: 'profiler-onrender' }],
|
|
46
|
-
};
|
|
47
|
-
lastRenderIdRef.current = instanceId;
|
|
48
|
-
renderIdsRef.current.set(id, instanceId);
|
|
49
|
-
publish({
|
|
50
|
-
source: 'react',
|
|
51
|
-
category: 'render',
|
|
52
|
-
severity: 'info',
|
|
53
|
-
title: `render ${id} (${phase})`,
|
|
54
|
-
fingerprint: `react-render:${logicalId}`,
|
|
55
|
-
raw: { kind: 'render', render: renderRecord },
|
|
56
|
-
});
|
|
57
|
-
};
|
|
58
|
-
return (_jsx(LensmcpContext.Provider, { value: ctxValue, children: _jsx(Profiler, { id: rendererId, onRender: onRender, children: props.children }) }));
|
|
59
|
-
}
|
|
1
|
+
"use strict";var v=Object.defineProperty;var a=(e,r)=>v(e,"name",{value:r,configurable:!0});var y=Object.defineProperty,d=a((e,r)=>y(e,"name",{value:r,configurable:!0}),"d");import{jsx as c}from"react/jsx-runtime";import{Profiler as R,useMemo as w,useRef as m}from"react";import{LensmcpContext as $}from"./context.js";import{publish as M}from"./publish.js";import{makeInstanceId as x}from"./identity.js";import{installFiberRenderPublisher as L}from"./fiber-renders.js";typeof window<"u"&&L();export function LensmcpRoot(e){const r=e.rendererId??"lensmcp-root",p=w(()=>({rendererId:r,page:e.page,route:e.route}),[r,e.page,e.route]),u=m(new Map),i=m(void 0),f=d((n,s,l,g,I,h)=>{const t=`render:${r}:${n}`,{instanceId:o}=x(t),b={id:o,parentRenderId:i.current,componentInstanceId:o,componentLogicalId:t,componentName:n,page:e.page,route:e.route,rendererId:r,phase:s==="mount"?"mount":"update",actualDurationMs:l,baseDurationMs:g,startTime:I,commitTime:h,why:[{type:"force",reason:"profiler-onrender"}]};i.current=o,u.current.set(n,o),M({source:"react",category:"render",severity:"info",title:`render ${n} (${s})`,fingerprint:`react-render:${t}`,raw:{kind:"render",render:b}})},"onRender");return c($.Provider,{value:p,children:c(R,{id:r,onRender:f,children:e.children})})}a(LensmcpRoot,"LensmcpRoot"),d(LensmcpRoot,"LensmcpRoot");
|
package/lib/publish.js
CHANGED
|
@@ -1,236 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const MAX_QUEUE = 200;
|
|
3
|
-
const FLOW_STACK = [];
|
|
4
|
-
/**
|
|
5
|
-
* Causality window: the synchronous flow stack misses everything Valtio and
|
|
6
|
-
* React deliver asynchronously — subscriber notifications, Profiler commits,
|
|
7
|
-
* and the whole post-response wave (state update + re-render after a fetch).
|
|
8
|
-
* An interaction OPENS a short attribution window; fetch completion EXTENDS
|
|
9
|
-
* it; any publish that has no explicit flow and no sync stack inherits the
|
|
10
|
-
* window's flow. Single-user dev semantics: the most recent interaction owns
|
|
11
|
-
* ambient activity for a few seconds — exactly how a person reads the app.
|
|
12
|
-
*/
|
|
13
|
-
const FLOW_WINDOW_MS = 4000;
|
|
14
|
-
let windowFlow;
|
|
15
|
-
export function extendFlowWindow(spec) {
|
|
16
|
-
const s = spec ?? currentFlow() ?? windowFlow?.spec;
|
|
17
|
-
if (s)
|
|
18
|
-
windowFlow = { spec: s, until: Date.now() + FLOW_WINDOW_MS };
|
|
19
|
-
}
|
|
20
|
-
function windowedFlow() {
|
|
21
|
-
return windowFlow && Date.now() < windowFlow.until ? windowFlow.spec : undefined;
|
|
22
|
-
}
|
|
23
|
-
/** The flow that owns work happening NOW: sync stack first, else the window. */
|
|
24
|
-
export function activeFlow() {
|
|
25
|
-
return currentFlow() ?? windowedFlow();
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Start a flow with no synchronous handler to wrap — page loads, timers,
|
|
29
|
-
* websocket pushes. Opens the causality window (so the async work that
|
|
30
|
-
* follows inherits the flow) and publishes the origin event that makes the
|
|
31
|
-
* flow exist in the reducer.
|
|
32
|
-
*/
|
|
33
|
-
export function beginBackgroundFlow(origin) {
|
|
34
|
-
const spec = {
|
|
35
|
-
flowId: newFlowId(),
|
|
36
|
-
originType: origin.originType,
|
|
37
|
-
...(origin.originNodeId ? { originNodeId: origin.originNodeId } : {}),
|
|
38
|
-
};
|
|
39
|
-
windowFlow = { spec, until: Date.now() + FLOW_WINDOW_MS };
|
|
40
|
-
publish({
|
|
41
|
-
source: 'react',
|
|
42
|
-
category: 'runtime',
|
|
43
|
-
severity: 'info',
|
|
44
|
-
title: origin.originType,
|
|
45
|
-
fingerprint: `flow-origin:${origin.originNodeId ?? origin.originType}`,
|
|
46
|
-
raw: { kind: 'flow-origin', originNodeId: origin.originNodeId },
|
|
47
|
-
});
|
|
48
|
-
return spec;
|
|
49
|
-
}
|
|
50
|
-
export function setPublisher(fn) {
|
|
51
|
-
globalThis.__LENSMCP_PUBLISH__ = fn;
|
|
52
|
-
if (fn) {
|
|
53
|
-
while (QUEUE.length) {
|
|
54
|
-
const ev = QUEUE.shift();
|
|
55
|
-
if (ev) {
|
|
56
|
-
try {
|
|
57
|
-
fn(ev);
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
/* swallow */
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Push a flow onto the module-level stack for the duration of `fn`. Any
|
|
68
|
-
* `publish()` calls inside `fn` (including transitively triggered
|
|
69
|
-
* Valtio subscribers and React Profiler callbacks that fire synchronously)
|
|
70
|
-
* will inherit the flow context.
|
|
71
|
-
*
|
|
72
|
-
* NB: only synchronous propagation. For async chains (`fetch().then`,
|
|
73
|
-
* `setTimeout`, promises) the caller must re-enter the flow with
|
|
74
|
-
* `runInFlow` at the continuation site. The Phase 6.5 carryover adds
|
|
75
|
-
* automatic continuation tracking via an async-context shim.
|
|
76
|
-
*/
|
|
77
|
-
export function runInFlow(spec, fn) {
|
|
78
|
-
FLOW_STACK.push(spec);
|
|
79
|
-
windowFlow = { spec, until: Date.now() + FLOW_WINDOW_MS }; // interaction opens the causality window
|
|
80
|
-
try {
|
|
81
|
-
return fn();
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
FLOW_STACK.pop();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
export function currentFlow() {
|
|
88
|
-
return FLOW_STACK[FLOW_STACK.length - 1];
|
|
89
|
-
}
|
|
90
|
-
/** Marks a handler as already flow-wrapped so layered wrapping is a no-op. */
|
|
91
|
-
const FLOWED = Symbol.for('lensmcp.flowed');
|
|
92
|
-
/**
|
|
93
|
-
* INPUT-CHANGE taming (the "an AI streamed text into an input → hundreds of flows" fix). Two rules,
|
|
94
|
-
* applied ONLY to `originType: 'input-change'` (clicks/submits keep one-flow-per-invocation):
|
|
95
|
-
*
|
|
96
|
-
* 1. A PROGRAMMATIC change (the event arg is untrusted — `isTrusted === false`) is a CONSEQUENCE,
|
|
97
|
-
* never a user intent: it must not mint a flow. The handler runs as-is and its effects inherit the
|
|
98
|
-
* ambient flow (the click/submit that started the stream), which is the true causality; a live
|
|
99
|
-
* ambient window is extended so a long stream keeps attributing to its trigger.
|
|
100
|
-
* 2. A TRUSTED burst on the SAME element (typing) COALESCES: invocations within a sliding window reuse
|
|
101
|
-
* the burst's flow and publish NO second origin — one editing burst = one flow, not one per keystroke.
|
|
102
|
-
*/
|
|
103
|
-
const INPUT_COALESCE_MS = 2000;
|
|
104
|
-
const inputBursts = new Map();
|
|
105
|
-
/** The event's trust, wherever React put it (SyntheticEvent.nativeEvent or a raw DOM event). */
|
|
106
|
-
function eventTrust(arg) {
|
|
107
|
-
if (arg == null || typeof arg !== 'object')
|
|
108
|
-
return undefined;
|
|
109
|
-
const ev = arg;
|
|
110
|
-
const nested = ev.nativeEvent;
|
|
111
|
-
if (nested != null && typeof nested === 'object') {
|
|
112
|
-
const trusted = nested.isTrusted;
|
|
113
|
-
if (typeof trusted === 'boolean')
|
|
114
|
-
return trusted;
|
|
115
|
-
}
|
|
116
|
-
if (typeof ev.isTrusted === 'boolean')
|
|
117
|
-
return ev.isTrusted;
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
function pruneInputBursts(now) {
|
|
121
|
-
if (inputBursts.size <= 64)
|
|
122
|
-
return;
|
|
123
|
-
for (const [key, burst] of inputBursts)
|
|
124
|
-
if (now >= burst.until)
|
|
125
|
-
inputBursts.delete(key);
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Wrap a React event handler so every call runs inside a fresh flow.
|
|
129
|
-
* The flowId is generated per invocation; `originType` defaults to
|
|
130
|
-
* `'user-click'`. Pass `originNodeId` from the component identity
|
|
131
|
-
* (typically the `data-agent-component` value).
|
|
132
|
-
*
|
|
133
|
-
* The babel transform wraps `on*` JSX values blindly, so this must be
|
|
134
|
-
* total: non-function values (e.g. `onClick={maybeUndefined}`) pass
|
|
135
|
-
* through unchanged, and an already-flowed handler is returned as-is —
|
|
136
|
-
* layered components forwarding the same prop would otherwise nest a
|
|
137
|
-
* flow per layer and crash on the inner non-function (`handler is not
|
|
138
|
-
* a function`).
|
|
139
|
-
*/
|
|
140
|
-
export function withFlow(handler, spec = {}) {
|
|
141
|
-
if (typeof handler !== 'function')
|
|
142
|
-
return handler;
|
|
143
|
-
const existing = handler;
|
|
144
|
-
if (existing[FLOWED])
|
|
145
|
-
return handler;
|
|
146
|
-
const publishOrigin = () => {
|
|
147
|
-
// The origin event — published inside the flow so it carries the
|
|
148
|
-
// flowId. It guarantees the flow materialises in the reducer (which
|
|
149
|
-
// keys on the first event with a flowId) even when every downstream
|
|
150
|
-
// effect lands asynchronously, outside the synchronous flow window.
|
|
151
|
-
publish({
|
|
152
|
-
source: 'react',
|
|
153
|
-
category: 'runtime',
|
|
154
|
-
severity: 'info',
|
|
155
|
-
title: spec.originType ?? 'user-click',
|
|
156
|
-
fingerprint: `flow-origin:${spec.originNodeId ?? 'unknown'}`,
|
|
157
|
-
raw: { kind: 'flow-origin', originNodeId: spec.originNodeId },
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
|
-
const flowed = function flowed(...args) {
|
|
161
|
-
if (spec.originType === 'input-change') {
|
|
162
|
-
// Rule 1 — a programmatic (untrusted) input change never mints a flow; it inherits the ambient
|
|
163
|
-
// one (the interaction that caused it). Keep a LIVE window alive so a long stream stays attributed.
|
|
164
|
-
if (eventTrust(args[0]) === false) {
|
|
165
|
-
if (currentFlow() == null && windowedFlow() != null)
|
|
166
|
-
extendFlowWindow();
|
|
167
|
-
return handler(...args);
|
|
168
|
-
}
|
|
169
|
-
// Rule 2 — a trusted burst on the same element coalesces into ONE flow (sliding window).
|
|
170
|
-
const key = spec.originNodeId ?? 'input';
|
|
171
|
-
const now = Date.now();
|
|
172
|
-
const burst = inputBursts.get(key);
|
|
173
|
-
if (burst && now < burst.until) {
|
|
174
|
-
burst.until = now + INPUT_COALESCE_MS;
|
|
175
|
-
return runInFlow(burst.spec, () => handler(...args)); // same flow, no second origin event
|
|
176
|
-
}
|
|
177
|
-
const burstSpec = { flowId: newFlowId(), originType: 'input-change', originNodeId: spec.originNodeId };
|
|
178
|
-
inputBursts.set(key, { spec: burstSpec, until: now + INPUT_COALESCE_MS });
|
|
179
|
-
pruneInputBursts(now);
|
|
180
|
-
return runInFlow(burstSpec, () => {
|
|
181
|
-
publishOrigin();
|
|
182
|
-
return handler(...args);
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
const flowId = newFlowId();
|
|
186
|
-
return runInFlow({
|
|
187
|
-
flowId,
|
|
188
|
-
originType: spec.originType ?? 'user-click',
|
|
189
|
-
originNodeId: spec.originNodeId,
|
|
190
|
-
}, () => {
|
|
191
|
-
publishOrigin();
|
|
192
|
-
return handler(...args);
|
|
193
|
-
});
|
|
194
|
-
};
|
|
195
|
-
flowed[FLOWED] = true;
|
|
196
|
-
return flowed;
|
|
197
|
-
}
|
|
198
|
-
let flowSeq = 0;
|
|
199
|
-
function newFlowId() {
|
|
200
|
-
flowSeq = (flowSeq + 1) % 0xffff;
|
|
201
|
-
return `flow:${Date.now().toString(36)}:${flowSeq.toString(36)}`;
|
|
202
|
-
}
|
|
203
|
-
export function publish(env) {
|
|
204
|
-
// Sync stack first; otherwise the causality window — unless the caller
|
|
205
|
-
// already carries its own flow (e.g. fetch-done with closure context).
|
|
206
|
-
const flow = currentFlow() ?? (env.context?.flowId ? undefined : windowedFlow());
|
|
207
|
-
const stamped = flow == null
|
|
208
|
-
? env
|
|
209
|
-
: {
|
|
210
|
-
...env,
|
|
211
|
-
context: {
|
|
212
|
-
...(flow.flowId ? { flowId: flow.flowId } : {}),
|
|
213
|
-
...(flow.originType ? { originType: flow.originType } : {}),
|
|
214
|
-
...(flow.originNodeId ? { originNodeId: flow.originNodeId } : {}),
|
|
215
|
-
...(flow.causedByNodeId ? { causedByNodeId: flow.causedByNodeId } : {}),
|
|
216
|
-
...env.context,
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
const fn = globalThis.__LENSMCP_PUBLISH__;
|
|
220
|
-
if (fn) {
|
|
221
|
-
try {
|
|
222
|
-
fn(stamped);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
/* fall through to queue */
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (QUEUE.length >= MAX_QUEUE)
|
|
230
|
-
QUEUE.shift();
|
|
231
|
-
QUEUE.push(stamped);
|
|
232
|
-
}
|
|
233
|
-
/** Test hook — only useful in unit tests / smokes. */
|
|
234
|
-
export function drainQueue() {
|
|
235
|
-
return QUEUE.splice(0, QUEUE.length);
|
|
236
|
-
}
|
|
1
|
+
"use strict";var F=Object.defineProperty;var i=(e,o)=>F(e,"name",{value:o,configurable:!0});var v=Object.defineProperty,n=i((e,o)=>v(e,"name",{value:o,configurable:!0}),"t");const u=[],B=200,g=[],S=4e3;let l;export function extendFlowWindow(e){const o=e??currentFlow()??l?.spec;o&&(l={spec:o,until:Date.now()+4e3})}i(extendFlowWindow,"extendFlowWindow"),n(extendFlowWindow,"extendFlowWindow");function p(){return l&&Date.now()<l.until?l.spec:void 0}i(p,"w"),n(p,"windowedFlow");export function activeFlow(){return currentFlow()??p()}i(activeFlow,"activeFlow"),n(activeFlow,"activeFlow");export function beginBackgroundFlow(e){const o={flowId:w(),originType:e.originType,...e.originNodeId?{originNodeId:e.originNodeId}:{}};return l={spec:o,until:Date.now()+4e3},publish({source:"react",category:"runtime",severity:"info",title:e.originType,fingerprint:`flow-origin:${e.originNodeId??e.originType}`,raw:{kind:"flow-origin",originNodeId:e.originNodeId}}),o}i(beginBackgroundFlow,"beginBackgroundFlow"),n(beginBackgroundFlow,"beginBackgroundFlow");export function setPublisher(e){if(globalThis.__LENSMCP_PUBLISH__=e,e)for(;u.length;){const o=u.shift();if(o)try{e(o)}catch{}}}i(setPublisher,"setPublisher"),n(setPublisher,"setPublisher");export function runInFlow(e,o){g.push(e),l={spec:e,until:Date.now()+4e3};try{return o()}finally{g.pop()}}i(runInFlow,"runInFlow"),n(runInFlow,"runInFlow");export function currentFlow(){return g[g.length-1]}i(currentFlow,"currentFlow"),n(currentFlow,"currentFlow");const N=Symbol.for("lensmcp.flowed"),h=2e3,c=new Map;function T(e){if(e==null||typeof e!="object")return;const o=e,t=o.nativeEvent;if(t!=null&&typeof t=="object"){const r=t.isTrusted;if(typeof r=="boolean")return r}if(typeof o.isTrusted=="boolean")return o.isTrusted}i(T,"S"),n(T,"eventTrust");function b(e){if(!(c.size<=64))for(const[o,t]of c)e>=t.until&&c.delete(o)}i(b,"F"),n(b,"pruneInputBursts");export function withFlow(e,o={}){if(typeof e!="function"||e[N])return e;const t=n(()=>{publish({source:"react",category:"runtime",severity:"info",title:o.originType??"user-click",fingerprint:`flow-origin:${o.originNodeId??"unknown"}`,raw:{kind:"flow-origin",originNodeId:o.originNodeId}})},"publishOrigin"),r=n(function(...d){if(o.originType==="input-change"){if(T(d[0])===!1)return currentFlow()==null&&p()!=null&&extendFlowWindow(),e(...d);const I=o.originNodeId??"input",s=Date.now(),f=c.get(I);if(f&&s<f.until)return f.until=s+h,runInFlow(f.spec,()=>e(...d));const y={flowId:w(),originType:"input-change",originNodeId:o.originNodeId};return c.set(I,{spec:y,until:s+h}),b(s),runInFlow(y,()=>(t(),e(...d)))}const x=w();return runInFlow({flowId:x,originType:o.originType??"user-click",originNodeId:o.originNodeId},()=>(t(),e(...d)))},"flowed");return r[N]=!0,r}i(withFlow,"withFlow"),n(withFlow,"withFlow");let a=0;function w(){return a=(a+1)%65535,`flow:${Date.now().toString(36)}:${a.toString(36)}`}i(w,"g"),n(w,"newFlowId");export function publish(e){const o=currentFlow()??(e.context?.flowId?void 0:p()),t=o==null?e:{...e,context:{...o.flowId?{flowId:o.flowId}:{},...o.originType?{originType:o.originType}:{},...o.originNodeId?{originNodeId:o.originNodeId}:{},...o.causedByNodeId?{causedByNodeId:o.causedByNodeId}:{},...e.context}},r=globalThis.__LENSMCP_PUBLISH__;if(r)try{r(t);return}catch{}u.length>=200&&u.shift(),u.push(t)}i(publish,"publish"),n(publish,"publish");export function drainQueue(){return u.splice(0,u.length)}i(drainQueue,"drainQueue"),n(drainQueue,"drainQueue");
|
package/lib/trace-slot.js
CHANGED
|
@@ -1,67 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Children, Fragment, useEffect, useRef } from 'react';
|
|
3
|
-
import { publish } from './publish.js';
|
|
4
|
-
import { makeInstanceId } from './identity.js';
|
|
5
|
-
import { useLensmcpContext } from './context.js';
|
|
6
|
-
/**
|
|
7
|
-
* Slot tracker. Emits `slot-content-changed` when the type of the
|
|
8
|
-
* primary child changes. The Babel transform (T5.1) wraps JSX
|
|
9
|
-
* conditionals with `<TraceSlot id="file:line:conditional">{...}</TraceSlot>`.
|
|
10
|
-
*/
|
|
11
|
-
export function TraceSlot(props) {
|
|
12
|
-
const ctx = useLensmcpContext();
|
|
13
|
-
const lastTypeRef = useRef(undefined);
|
|
14
|
-
const slotInstanceRef = useRef(undefined);
|
|
15
|
-
if (!slotInstanceRef.current) {
|
|
16
|
-
slotInstanceRef.current = makeInstanceId(`react:slot:${props.id}`).instanceId;
|
|
17
|
-
}
|
|
18
|
-
const slotInstanceId = slotInstanceRef.current;
|
|
19
|
-
const currentType = describeChild(props.children);
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
if (lastTypeRef.current !== currentType) {
|
|
22
|
-
const fromComponent = lastTypeRef.current;
|
|
23
|
-
lastTypeRef.current = currentType;
|
|
24
|
-
publish({
|
|
25
|
-
source: 'react',
|
|
26
|
-
category: 'render',
|
|
27
|
-
severity: 'info',
|
|
28
|
-
title: `slot ${props.id}: ${fromComponent ?? '(none)'} → ${currentType}`,
|
|
29
|
-
fingerprint: `slot-change:${props.id}`,
|
|
30
|
-
raw: {
|
|
31
|
-
kind: 'slot-content-changed',
|
|
32
|
-
slot: {
|
|
33
|
-
slotLogicalId: `react:slot:${props.id}`,
|
|
34
|
-
slotInstanceId,
|
|
35
|
-
fromComponent,
|
|
36
|
-
toComponent: currentType,
|
|
37
|
-
componentInstanceId: ctx.componentInstanceId,
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}, [currentType, props.id, slotInstanceId, ctx.componentInstanceId]);
|
|
43
|
-
// Stable wrapper so React doesn't think the children's parent changed
|
|
44
|
-
// when the slot remounts — we want layout to feel transparent.
|
|
45
|
-
return _jsx(Fragment, { children: props.children });
|
|
46
|
-
}
|
|
47
|
-
function describeChild(children) {
|
|
48
|
-
const arr = Children.toArray(children);
|
|
49
|
-
const first = arr[0];
|
|
50
|
-
if (!first)
|
|
51
|
-
return '(empty)';
|
|
52
|
-
if (typeof first === 'string' || typeof first === 'number')
|
|
53
|
-
return '(text)';
|
|
54
|
-
if (typeof first === 'boolean')
|
|
55
|
-
return '(boolean)';
|
|
56
|
-
if (Array.isArray(first))
|
|
57
|
-
return '(array)';
|
|
58
|
-
if (typeof first === 'object' && first && 'type' in first) {
|
|
59
|
-
const t = first.type;
|
|
60
|
-
if (typeof t === 'string')
|
|
61
|
-
return t;
|
|
62
|
-
if (typeof t === 'function')
|
|
63
|
-
return t.displayName ?? t.name ?? 'AnonymousComponent';
|
|
64
|
-
return 'unknown';
|
|
65
|
-
}
|
|
66
|
-
return 'unknown';
|
|
67
|
-
}
|
|
1
|
+
"use strict";var m=Object.defineProperty;var o=(e,n)=>m(e,"name",{value:n,configurable:!0});var p=Object.defineProperty,a=o((e,n)=>p(e,"name",{value:n,configurable:!0}),"c");import{jsx as d}from"react/jsx-runtime";import{Children as l,Fragment as y,useEffect as I,useRef as u}from"react";import{publish as g}from"./publish.js";import{makeInstanceId as b}from"./identity.js";import{useLensmcpContext as h}from"./context.js";export function TraceSlot(e){const n=h(),t=u(void 0),c=u(void 0);c.current||(c.current=b(`react:slot:${e.id}`).instanceId);const i=c.current,r=f(e.children);return I(()=>{if(t.current!==r){const s=t.current;t.current=r,g({source:"react",category:"render",severity:"info",title:`slot ${e.id}: ${s??"(none)"} \u2192 ${r}`,fingerprint:`slot-change:${e.id}`,raw:{kind:"slot-content-changed",slot:{slotLogicalId:`react:slot:${e.id}`,slotInstanceId:i,fromComponent:s,toComponent:r,componentInstanceId:n.componentInstanceId}}})}},[r,e.id,i,n.componentInstanceId]),d(y,{children:e.children})}o(TraceSlot,"TraceSlot"),a(TraceSlot,"TraceSlot");function f(e){const n=l.toArray(e)[0];if(!n)return"(empty)";if(typeof n=="string"||typeof n=="number")return"(text)";if(typeof n=="boolean")return"(boolean)";if(Array.isArray(n))return"(array)";if(typeof n=="object"&&n&&"type"in n){const t=n.type;return typeof t=="string"?t:typeof t=="function"?t.displayName??t.name??"AnonymousComponent":"unknown"}return"unknown"}o(f,"g"),a(f,"describeChild");
|