@lensmcp/vite-plugin 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 -4
- package/lib/event-mappers.js +2 -169
- package/lib/plugin.js +12 -304
- package/lib/types.js +1 -1
- package/package.json +6 -6
package/index.js
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
// `envelopeToEvent` moved to @lensmcp/bridge when the bridge was decoupled
|
|
3
|
-
// from Vite; re-exported here for backwards compatibility.
|
|
4
|
-
export { envelopeToEvent } from '@lensmcp/bridge';
|
|
1
|
+
"use strict";export{lensmcpVitePlugin,handleClientEnvelope}from"./lib/plugin.js";export{envelopeToEvent}from"@lensmcp/bridge";
|
package/lib/event-mappers.js
CHANGED
|
@@ -1,169 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
/**
|
|
4
|
-
* Translate a single client-runtime envelope into the normalised
|
|
5
|
-
* `BaseEvent` consumed by the LensMCP event bus. The Vite plugin
|
|
6
|
-
* publishes these; reducers in `@lensmcp/<app>` materialise them into
|
|
7
|
-
* `runtime://browser/*` resources.
|
|
8
|
-
*/
|
|
9
|
-
export function envelopeToEvent(env, sessionId = SESSION_ID_PLACEHOLDER) {
|
|
10
|
-
// Per-tab identity stamped by the client runtime — keys browser://tabs.
|
|
11
|
-
const tab = env.tabId ? { tabId: env.tabId } : {};
|
|
12
|
-
switch (env.kind) {
|
|
13
|
-
case 'error':
|
|
14
|
-
return {
|
|
15
|
-
id: ulid(),
|
|
16
|
-
sessionId,
|
|
17
|
-
timestamp: env.at,
|
|
18
|
-
source: 'client-runtime',
|
|
19
|
-
category: 'runtime',
|
|
20
|
-
severity: 'error',
|
|
21
|
-
context: { sessionId, ...tab },
|
|
22
|
-
fingerprint: fingerprint({
|
|
23
|
-
kind: 'runtime',
|
|
24
|
-
identity: `error:${normaliseStackTopFrame(env.stack)}`,
|
|
25
|
-
file: env.source,
|
|
26
|
-
detail: env.message,
|
|
27
|
-
}),
|
|
28
|
-
title: env.message,
|
|
29
|
-
message: env.stack ?? env.message,
|
|
30
|
-
location: env.source
|
|
31
|
-
? { file: env.source, line: env.lineno, column: env.colno }
|
|
32
|
-
: undefined,
|
|
33
|
-
raw: env,
|
|
34
|
-
};
|
|
35
|
-
case 'unhandledrejection':
|
|
36
|
-
return {
|
|
37
|
-
id: ulid(),
|
|
38
|
-
sessionId,
|
|
39
|
-
timestamp: env.at,
|
|
40
|
-
source: 'client-runtime',
|
|
41
|
-
category: 'runtime',
|
|
42
|
-
severity: 'error',
|
|
43
|
-
context: { sessionId, ...tab },
|
|
44
|
-
fingerprint: fingerprint({
|
|
45
|
-
kind: 'runtime',
|
|
46
|
-
identity: `unhandledrejection:${env.reason.slice(0, 80)}`,
|
|
47
|
-
}),
|
|
48
|
-
title: 'Unhandled promise rejection',
|
|
49
|
-
message: env.reason,
|
|
50
|
-
raw: env,
|
|
51
|
-
};
|
|
52
|
-
case 'console': {
|
|
53
|
-
const severity = env.level === 'error' ? 'error' : env.level === 'warn' ? 'warning' : 'info';
|
|
54
|
-
const text = env.args.map(stringify).join(' ');
|
|
55
|
-
return {
|
|
56
|
-
id: ulid(),
|
|
57
|
-
sessionId,
|
|
58
|
-
timestamp: env.at,
|
|
59
|
-
source: 'client-runtime',
|
|
60
|
-
category: 'runtime',
|
|
61
|
-
severity,
|
|
62
|
-
context: { sessionId, ...tab },
|
|
63
|
-
fingerprint: fingerprint({
|
|
64
|
-
kind: 'runtime',
|
|
65
|
-
identity: `console:${env.level}:${text.slice(0, 80)}`,
|
|
66
|
-
}),
|
|
67
|
-
title: `console.${env.level}`,
|
|
68
|
-
message: text,
|
|
69
|
-
raw: env,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
case 'fetch': {
|
|
73
|
-
const failed = env.error !== undefined || (env.status !== undefined && env.status >= 400);
|
|
74
|
-
return {
|
|
75
|
-
id: ulid(),
|
|
76
|
-
sessionId,
|
|
77
|
-
timestamp: env.at,
|
|
78
|
-
source: 'client-runtime',
|
|
79
|
-
category: 'network',
|
|
80
|
-
severity: failed ? 'error' : 'info',
|
|
81
|
-
context: { sessionId, ...tab },
|
|
82
|
-
fingerprint: fingerprint({
|
|
83
|
-
kind: 'network',
|
|
84
|
-
identity: `${env.method}:${normaliseUrl(env.url)}:${env.status ?? 'err'}`,
|
|
85
|
-
}),
|
|
86
|
-
title: `${env.method} ${env.url}${env.status !== undefined ? ` → ${env.status}` : ''}`,
|
|
87
|
-
message: env.error,
|
|
88
|
-
relatedUrls: [env.url],
|
|
89
|
-
raw: env,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
case 'route':
|
|
93
|
-
return {
|
|
94
|
-
id: ulid(),
|
|
95
|
-
sessionId,
|
|
96
|
-
timestamp: env.at,
|
|
97
|
-
source: 'client-runtime',
|
|
98
|
-
category: 'runtime',
|
|
99
|
-
severity: 'info',
|
|
100
|
-
context: { sessionId, url: env.url, ...tab },
|
|
101
|
-
fingerprint: fingerprint({ kind: 'runtime', identity: `route:${env.url}` }),
|
|
102
|
-
title: `Route → ${env.url}`,
|
|
103
|
-
raw: env,
|
|
104
|
-
};
|
|
105
|
-
case 'ready':
|
|
106
|
-
return {
|
|
107
|
-
id: ulid(),
|
|
108
|
-
sessionId,
|
|
109
|
-
timestamp: env.at,
|
|
110
|
-
source: 'client-runtime',
|
|
111
|
-
category: 'runtime',
|
|
112
|
-
severity: 'info',
|
|
113
|
-
context: { sessionId, url: env.url, ...tab },
|
|
114
|
-
fingerprint: fingerprint({ kind: 'runtime', identity: 'client-ready' }),
|
|
115
|
-
title: 'Client runtime ready',
|
|
116
|
-
message: env.userAgent,
|
|
117
|
-
raw: env,
|
|
118
|
-
};
|
|
119
|
-
case 'publish':
|
|
120
|
-
// Pre-formed publish envelope from in-page instrumentation. Trust
|
|
121
|
-
// its source/category (the producer is our own injected code); the
|
|
122
|
-
// reducers gate on those, and the context carries the flow id so
|
|
123
|
-
// React renders correlate with the originating user action.
|
|
124
|
-
return {
|
|
125
|
-
id: ulid(),
|
|
126
|
-
sessionId,
|
|
127
|
-
timestamp: env.at,
|
|
128
|
-
source: env.source,
|
|
129
|
-
category: env.category,
|
|
130
|
-
severity: (env.severity ?? 'info'),
|
|
131
|
-
context: { sessionId, ...tab, ...(env.context ?? {}) },
|
|
132
|
-
fingerprint: env.fingerprint ?? fingerprint({ kind: 'publish', identity: env.title }),
|
|
133
|
-
title: env.title,
|
|
134
|
-
message: env.message,
|
|
135
|
-
raw: env.raw,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// --- helpers ---
|
|
140
|
-
function normaliseStackTopFrame(stack) {
|
|
141
|
-
if (!stack)
|
|
142
|
-
return 'unknown';
|
|
143
|
-
const lines = stack.split('\n').slice(0, 2);
|
|
144
|
-
for (const line of lines) {
|
|
145
|
-
const m = line.match(/at\s+(\S+)/);
|
|
146
|
-
if (m && m[1])
|
|
147
|
-
return m[1];
|
|
148
|
-
}
|
|
149
|
-
return 'unknown';
|
|
150
|
-
}
|
|
151
|
-
function normaliseUrl(url) {
|
|
152
|
-
try {
|
|
153
|
-
const u = new URL(url, 'http://x');
|
|
154
|
-
return `${u.pathname}`;
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return url;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function stringify(v) {
|
|
161
|
-
if (typeof v === 'string')
|
|
162
|
-
return v;
|
|
163
|
-
try {
|
|
164
|
-
return JSON.stringify(v);
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
return String(v);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
1
|
+
"use strict";var g=Object.defineProperty;var o=(t,e)=>g(t,"name",{value:e,configurable:!0});var f=Object.defineProperty,a=o((t,e)=>f(t,"name",{value:e,configurable:!0}),"o"),p=Object.defineProperty,u=a((t,e)=>p(t,"name",{value:e,configurable:!0}),"o");import{fingerprint as n,ulid as s}from"@lensmcp/core";const y="pending";export function envelopeToEvent(t,e=y){const r=t.tabId?{tabId:t.tabId}:{};switch(t.kind){case"error":return{id:s(),sessionId:e,timestamp:t.at,source:"client-runtime",category:"runtime",severity:"error",context:{sessionId:e,...r},fingerprint:n({kind:"runtime",identity:`error:${c(t.stack)}`,file:t.source,detail:t.message}),title:t.message,message:t.stack??t.message,location:t.source?{file:t.source,line:t.lineno,column:t.colno}:void 0,raw:t};case"unhandledrejection":return{id:s(),sessionId:e,timestamp:t.at,source:"client-runtime",category:"runtime",severity:"error",context:{sessionId:e,...r},fingerprint:n({kind:"runtime",identity:`unhandledrejection:${t.reason.slice(0,80)}`}),title:"Unhandled promise rejection",message:t.reason,raw:t};case"console":{const i=t.level==="error"?"error":t.level==="warn"?"warning":"info",m=t.args.map(d).join(" ");return{id:s(),sessionId:e,timestamp:t.at,source:"client-runtime",category:"runtime",severity:i,context:{sessionId:e,...r},fingerprint:n({kind:"runtime",identity:`console:${t.level}:${m.slice(0,80)}`}),title:`console.${t.level}`,message:m,raw:t}}case"fetch":{const i=t.error!==void 0||t.status!==void 0&&t.status>=400;return{id:s(),sessionId:e,timestamp:t.at,source:"client-runtime",category:"network",severity:i?"error":"info",context:{sessionId:e,...r},fingerprint:n({kind:"network",identity:`${t.method}:${l(t.url)}:${t.status??"err"}`}),title:`${t.method} ${t.url}${t.status!==void 0?` \u2192 ${t.status}`:""}`,message:t.error,relatedUrls:[t.url],raw:t}}case"route":return{id:s(),sessionId:e,timestamp:t.at,source:"client-runtime",category:"runtime",severity:"info",context:{sessionId:e,url:t.url,...r},fingerprint:n({kind:"runtime",identity:`route:${t.url}`}),title:`Route \u2192 ${t.url}`,raw:t};case"ready":return{id:s(),sessionId:e,timestamp:t.at,source:"client-runtime",category:"runtime",severity:"info",context:{sessionId:e,url:t.url,...r},fingerprint:n({kind:"runtime",identity:"client-ready"}),title:"Client runtime ready",message:t.userAgent,raw:t};case"publish":return{id:s(),sessionId:e,timestamp:t.at,source:t.source,category:t.category,severity:t.severity??"info",context:{sessionId:e,...r,...t.context??{}},fingerprint:t.fingerprint??n({kind:"publish",identity:t.title}),title:t.title,message:t.message,raw:t.raw}}}o(envelopeToEvent,"envelopeToEvent"),a(envelopeToEvent,"envelopeToEvent"),u(envelopeToEvent,"envelopeToEvent");function c(t){if(!t)return"unknown";const e=t.split(`
|
|
2
|
+
`).slice(0,2);for(const r of e){const i=r.match(/at\s+(\S+)/);if(i&&i[1])return i[1]}return"unknown"}o(c,"c"),a(c,"s"),u(c,"normaliseStackTopFrame");function l(t){try{return`${new URL(t,"http://x").pathname}`}catch{return t}}o(l,"l"),a(l,"m"),u(l,"normaliseUrl");function d(t){if(typeof t=="string")return t;try{return JSON.stringify(t)}catch{return String(t)}}o(d,"d"),a(d,"d"),u(d,"stringify");
|
package/lib/plugin.js
CHANGED
|
@@ -1,307 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
import { createBridgeServer, makePublisher, handleClientEnvelope as forwardClientEnvelope, } from '@lensmcp/bridge';
|
|
3
|
-
const CLIENT_VIRTUAL_ID = '/@lensmcp/client-runtime';
|
|
4
|
-
const RESOLVED_CLIENT_VIRTUAL_ID = '\0lensmcp-client-runtime';
|
|
5
|
-
/**
|
|
6
|
-
* Vite plugin that:
|
|
7
|
-
* 1. Stands up the LensMCP browser-event bridge (`@lensmcp/bridge`) on an
|
|
8
|
-
* ephemeral WebSocket port so the injected @lensmcp/client-runtime can
|
|
9
|
-
* post browser events back into the LensMCP event bus.
|
|
10
|
-
* 2. Injects a `<script type="module" src="/@lensmcp/client-runtime">`
|
|
11
|
-
* into served HTML.
|
|
12
|
-
* 3. Resolves that virtual module to the bundled client runtime
|
|
13
|
-
* (during Phase 1 — a minimal inline implementation; the real
|
|
14
|
-
* package import lands in Phase 2/3).
|
|
15
|
-
* 4. Emits HMR + module-graph events for the build reducer.
|
|
16
|
-
*
|
|
17
|
-
* Since the bridge was decoupled, this plugin is just ONE host adapter for
|
|
18
|
-
* it — webpack / Next.js / no-build hosts run the same bridge as a
|
|
19
|
-
* standalone sidecar (`lensmcp bridge`) on a fixed port instead.
|
|
20
|
-
*
|
|
21
|
-
* The plugin is *passive*: it never mutates app state or behaviour.
|
|
22
|
-
*/
|
|
23
|
-
export function lensmcpVitePlugin(options = {}, deps = {}) {
|
|
24
|
-
const enabled = options.enabled ?? true;
|
|
25
|
-
const wsHost = options.wsHost ?? '127.0.0.1';
|
|
26
|
-
const injectClient = options.injectClient ?? true;
|
|
27
|
-
let wssPort;
|
|
28
|
-
let bridge;
|
|
29
|
-
// Bus → env-sink → console publisher, shared by every hook below.
|
|
30
|
-
const publishEvent = makePublisher(deps);
|
|
31
|
-
const aliasValtio = options.aliasValtio ?? true;
|
|
32
|
-
const transformSource = options.transformSource ?? true;
|
|
33
|
-
return {
|
|
34
|
-
name: '@lensmcp/vite-plugin',
|
|
35
|
-
apply: enabled ? undefined : () => false,
|
|
36
|
-
enforce: 'pre',
|
|
37
|
-
wsPort: () => wssPort,
|
|
38
|
-
config() {
|
|
39
|
-
if (!enabled || !aliasValtio)
|
|
40
|
-
return;
|
|
41
|
-
// Zero-config (Phase 8): make `import { proxy } from 'valtio'`
|
|
42
|
-
// resolve to the instrumented drop-in, so existing Valtio code
|
|
43
|
-
// becomes observable without edits. Dev-only.
|
|
44
|
-
//
|
|
45
|
-
// EXACT match (`/^valtio$/`) only — a bare-string alias does prefix
|
|
46
|
-
// matching, which would also rewrite the drop-in's own
|
|
47
|
-
// `valtio/vanilla` + `valtio/react` imports and self-loop.
|
|
48
|
-
return {
|
|
49
|
-
resolve: {
|
|
50
|
-
alias: [
|
|
51
|
-
{ find: /^valtio$/, replacement: '@lensmcp/valtio-instrumentation' },
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
async configureServer(server) {
|
|
57
|
-
if (!enabled)
|
|
58
|
-
return;
|
|
59
|
-
// The bridge owns the WebSocket + envelope→event mapping; we just
|
|
60
|
-
// host it on an ephemeral port (or `options.wsPort` if pinned) and
|
|
61
|
-
// bake that port into the injected runtime at `load()` time.
|
|
62
|
-
bridge = await createBridgeServer({
|
|
63
|
-
host: wsHost,
|
|
64
|
-
port: options.wsPort ?? 0,
|
|
65
|
-
bus: deps.bus,
|
|
66
|
-
sessionId: deps.sessionId,
|
|
67
|
-
});
|
|
68
|
-
wssPort = bridge.port;
|
|
69
|
-
// Vite native error stream → our bus.
|
|
70
|
-
server.ws.on('vite:error', (data) => {
|
|
71
|
-
publishEvent(viteErrorToEvent(data, deps.sessionId));
|
|
72
|
-
});
|
|
73
|
-
},
|
|
74
|
-
resolveId(id) {
|
|
75
|
-
if (id === CLIENT_VIRTUAL_ID)
|
|
76
|
-
return RESOLVED_CLIENT_VIRTUAL_ID;
|
|
77
|
-
return null;
|
|
78
|
-
},
|
|
79
|
-
load(id) {
|
|
80
|
-
if (id !== RESOLVED_CLIENT_VIRTUAL_ID)
|
|
81
|
-
return null;
|
|
82
|
-
const port = wssPort ?? options.wsPort ?? 0;
|
|
83
|
-
return buildInlineClientRuntime({ host: wsHost, port });
|
|
84
|
-
},
|
|
85
|
-
// Zero-config source transform. Runs in our OWN transform hook at
|
|
86
|
-
// enforce:'pre', so it's independent of whichever compiler the React
|
|
87
|
-
// plugin uses — it runs on the raw JSX/TSX BEFORE @vitejs/plugin-react
|
|
88
|
-
// (Babel), @vitejs/plugin-react@6 (oxc, which dropped the `babel` hook)
|
|
89
|
-
// or @vitejs/plugin-react-swc (SWC) turn JSX into calls. The LensMCP
|
|
90
|
-
// output is plain JSX/TSX (LensmcpRoot/withFlow/traced* + data-* props),
|
|
91
|
-
// which every downstream compiler preserves (see swc-coexistence smoke).
|
|
92
|
-
// A native SWC/oxc rewrite would only be a perf optimisation.
|
|
93
|
-
async transform(code, id) {
|
|
94
|
-
if (!enabled || !transformSource)
|
|
95
|
-
return null;
|
|
96
|
-
const file = id.split('?')[0] ?? id;
|
|
97
|
-
if (file.includes('/node_modules/'))
|
|
98
|
-
return null;
|
|
99
|
-
if (!/\.[jt]sx$/.test(file))
|
|
100
|
-
return null;
|
|
101
|
-
const result = await runLensmcpBabel(code, file);
|
|
102
|
-
if (!result)
|
|
103
|
-
return null;
|
|
104
|
-
// Babel's source map is a valid Rollup map object at runtime; the
|
|
105
|
-
// cast just bridges the nominal type gap.
|
|
106
|
-
return { code: result.code, map: result.map };
|
|
107
|
-
},
|
|
108
|
-
transformIndexHtml() {
|
|
109
|
-
if (!injectClient || !enabled)
|
|
110
|
-
return;
|
|
111
|
-
return [
|
|
112
|
-
{
|
|
113
|
-
tag: 'script',
|
|
114
|
-
attrs: { type: 'module', src: CLIENT_VIRTUAL_ID },
|
|
115
|
-
injectTo: 'head-prepend',
|
|
116
|
-
},
|
|
117
|
-
];
|
|
118
|
-
},
|
|
119
|
-
handleHotUpdate(ctx) {
|
|
120
|
-
if (!enabled)
|
|
121
|
-
return;
|
|
122
|
-
const affected = ctx.modules
|
|
123
|
-
.map((m) => m.file ?? m.id ?? null)
|
|
124
|
-
.filter((f) => !!f);
|
|
125
|
-
publishEvent({
|
|
126
|
-
id: ulid(),
|
|
127
|
-
sessionId: deps.sessionId ?? 'pending',
|
|
128
|
-
timestamp: Date.now(),
|
|
129
|
-
source: 'vite',
|
|
130
|
-
category: 'build',
|
|
131
|
-
severity: 'info',
|
|
132
|
-
context: { sessionId: deps.sessionId ?? 'pending' },
|
|
133
|
-
fingerprint: fingerprint({
|
|
134
|
-
kind: 'hmr',
|
|
135
|
-
identity: ctx.file,
|
|
136
|
-
}),
|
|
137
|
-
title: `HMR ${ctx.file}`,
|
|
138
|
-
// The changed file, structured. The build reducer retracts bundler
|
|
139
|
-
// failures for exactly the files an update touched, so it needs them
|
|
140
|
-
// as data — it previously had to scrape the `HMR ` title prefix.
|
|
141
|
-
location: { file: ctx.file },
|
|
142
|
-
relatedFiles: affected,
|
|
143
|
-
raw: { kind: 'hmr', file: ctx.file },
|
|
144
|
-
});
|
|
145
|
-
},
|
|
146
|
-
generateBundle(_opts, bundle) {
|
|
147
|
-
if (!enabled)
|
|
148
|
-
return;
|
|
149
|
-
const chunks = [];
|
|
150
|
-
let totalBytes = 0;
|
|
151
|
-
for (const file of Object.values(bundle)) {
|
|
152
|
-
const item = file;
|
|
153
|
-
const name = item.fileName;
|
|
154
|
-
let sizeBytes;
|
|
155
|
-
if (item.type === 'chunk') {
|
|
156
|
-
sizeBytes = (item.code ?? '').length;
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
const src = item.source;
|
|
160
|
-
sizeBytes =
|
|
161
|
-
typeof src === 'string' ? src.length : src instanceof Uint8Array ? src.byteLength : 0;
|
|
162
|
-
}
|
|
163
|
-
totalBytes += sizeBytes;
|
|
164
|
-
const modules = [];
|
|
165
|
-
if (item.type === 'chunk' && item.modules) {
|
|
166
|
-
for (const [id, mod] of Object.entries(item.modules)) {
|
|
167
|
-
const ml = mod.renderedLength ?? mod.originalLength ?? 0;
|
|
168
|
-
modules.push({ id, sizeBytes: ml });
|
|
169
|
-
}
|
|
170
|
-
modules.sort((a, b) => b.sizeBytes - a.sizeBytes);
|
|
171
|
-
}
|
|
172
|
-
chunks.push({ name, sizeBytes, modules });
|
|
173
|
-
}
|
|
174
|
-
const report = { timestamp: Date.now(), totalBytes, chunks };
|
|
175
|
-
publishEvent({
|
|
176
|
-
id: ulid(),
|
|
177
|
-
sessionId: deps.sessionId ?? 'pending',
|
|
178
|
-
timestamp: Date.now(),
|
|
179
|
-
source: 'rollup',
|
|
180
|
-
category: 'build',
|
|
181
|
-
severity: 'info',
|
|
182
|
-
context: { sessionId: deps.sessionId ?? 'pending' },
|
|
183
|
-
fingerprint: fingerprint({ kind: 'bundle', identity: 'report' }),
|
|
184
|
-
title: `Bundle report: ${chunks.length} chunks, ${totalBytes} bytes`,
|
|
185
|
-
raw: { kind: 'bundle-report', report },
|
|
186
|
-
});
|
|
187
|
-
},
|
|
188
|
-
async closeBundle() {
|
|
189
|
-
if (bridge) {
|
|
190
|
-
await bridge.close();
|
|
191
|
-
bridge = undefined;
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
// --- helpers ---
|
|
197
|
-
/**
|
|
198
|
-
* Run the LensMCP Babel transform on one module's source. Lazily loads
|
|
199
|
-
* `@babel/core` + the plugin so they're only touched in dev when a
|
|
200
|
-
* `.jsx`/`.tsx` file is transformed. Returns null on parse error (let
|
|
201
|
-
* Vite proceed with the untransformed source).
|
|
202
|
-
*/
|
|
203
|
-
async function runLensmcpBabel(code, filename) {
|
|
204
|
-
try {
|
|
205
|
-
const [{ transformSync }, { lensmcpBabelPlugin }] = await Promise.all([
|
|
206
|
-
import('@babel/core'),
|
|
207
|
-
import('@lensmcp/react-instrumentation'),
|
|
208
|
-
]);
|
|
209
|
-
const result = transformSync(code, {
|
|
210
|
-
filename,
|
|
211
|
-
babelrc: false,
|
|
212
|
-
configFile: false,
|
|
213
|
-
sourceMaps: true,
|
|
214
|
-
parserOpts: { plugins: ['jsx', 'typescript'] },
|
|
215
|
-
plugins: [[lensmcpBabelPlugin, { rootDir: process.cwd(), production: false }]],
|
|
216
|
-
code: true,
|
|
217
|
-
});
|
|
218
|
-
if (!result?.code)
|
|
219
|
-
return null;
|
|
220
|
-
return { code: result.code, map: result.map };
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Parse one raw WebSocket message from the injected client runtime and
|
|
228
|
-
* publish the resulting event. Kept as a public export for backwards
|
|
229
|
-
* compatibility — it now delegates to `@lensmcp/bridge`. Exercised over a
|
|
230
|
-
* real WebSocket without booting Vite (see the react-bridge smoke).
|
|
231
|
-
*/
|
|
232
|
-
export function handleClientEnvelope(raw, deps) {
|
|
233
|
-
forwardClientEnvelope(raw, makePublisher(deps), deps.sessionId);
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Vite's `ErrorPayload.err` carries the offending module as `loc.file` (when
|
|
237
|
-
* the plugin reported a position) or `id` (the resolved module id, which may
|
|
238
|
-
* carry a `?t=` cache-buster). Both are stripped to a bare path.
|
|
239
|
-
*/
|
|
240
|
-
function errorLocation(err) {
|
|
241
|
-
const raw = err?.loc?.file ?? err?.id;
|
|
242
|
-
if (typeof raw !== 'string' || raw.length === 0)
|
|
243
|
-
return undefined;
|
|
244
|
-
const file = raw.split('?')[0] || raw;
|
|
245
|
-
return {
|
|
246
|
-
file,
|
|
247
|
-
...(typeof err?.loc?.line === 'number' ? { line: err.loc.line } : {}),
|
|
248
|
-
...(typeof err?.loc?.column === 'number' ? { column: err.loc.column } : {}),
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
function viteErrorToEvent(data, sessionId) {
|
|
252
|
-
const message = data.err?.message ?? 'Vite error';
|
|
253
|
-
const stack = data.err?.stack;
|
|
254
|
-
const loc = errorLocation(data.err);
|
|
255
|
-
return {
|
|
256
|
-
id: ulid(),
|
|
257
|
-
sessionId: sessionId ?? 'pending',
|
|
258
|
-
timestamp: Date.now(),
|
|
259
|
-
source: 'vite',
|
|
260
|
-
category: 'build',
|
|
261
|
-
severity: 'error',
|
|
262
|
-
context: { sessionId: sessionId ?? 'pending' },
|
|
263
|
-
// The FILE is part of the identity. Without it two different modules
|
|
264
|
-
// failing with the same message ("Failed to resolve import") collapsed
|
|
265
|
-
// into ONE entry, so retracting either would have silenced both — and a
|
|
266
|
-
// file-scoped clear could not be safe. It is also what lets a later HMR
|
|
267
|
-
// update for that file retract this error at all: until now nothing in
|
|
268
|
-
// this repo ever removed a build failure, so a fixed Vite error was
|
|
269
|
-
// reported for the life of the session.
|
|
270
|
-
fingerprint: fingerprint({
|
|
271
|
-
kind: 'vite',
|
|
272
|
-
identity: data.type ?? message,
|
|
273
|
-
...(loc ? { file: loc.file } : {}),
|
|
274
|
-
}),
|
|
275
|
-
title: message,
|
|
276
|
-
message: stack,
|
|
277
|
-
...(loc ? { location: loc } : {}),
|
|
278
|
-
raw: {
|
|
279
|
-
kind: 'build-diagnostic',
|
|
280
|
-
...(loc ? { file: loc.file } : {}),
|
|
281
|
-
...(data.err?.plugin ? { plugin: data.err.plugin } : {}),
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Minimal browser-side runtime that mirrors what `@lensmcp/client-runtime`
|
|
287
|
-
* will eventually own. Inlined here so Phase 1 has no cross-build
|
|
288
|
-
* coupling. Future: replace with a real import of the published
|
|
289
|
-
* package's browser bundle.
|
|
290
|
-
*/
|
|
291
|
-
function buildInlineClientRuntime(opts) {
|
|
292
|
-
return `// @lensmcp/client-runtime (inlined by @lensmcp/vite-plugin)
|
|
1
|
+
"use strict";var x=Object.defineProperty;var l=(e,t)=>x(e,"name",{value:t,configurable:!0});var P=Object.defineProperty,u=l((e,t)=>P(e,"name",{value:t,configurable:!0}),"l");import{fingerprint as v,ulid as b}from"@lensmcp/core";import{createBridgeServer as D,makePublisher as S,handleClientEnvelope as N}from"@lensmcp/bridge";const _="/@lensmcp/client-runtime",E="\0lensmcp-client-runtime";export function lensmcpVitePlugin(e={},t={}){const r=e.enabled??!0,p=e.wsHost??"127.0.0.1",o=e.injectClient??!0;let g,f;const y=S(t),L=e.aliasValtio??!0,T=e.transformSource??!0;return{name:"@lensmcp/vite-plugin",apply:r?void 0:()=>!1,enforce:"pre",wsPort:u(()=>g,"wsPort"),config(){if(!(!r||!L))return{resolve:{alias:[{find:/^valtio$/,replacement:"@lensmcp/valtio-instrumentation"}]}}},async configureServer(n){r&&(f=await D({host:p,port:e.wsPort??0,bus:t.bus,sessionId:t.sessionId}),g=f.port,n.ws.on("vite:error",s=>{y(C(s,t.sessionId))}))},resolveId(n){return n===_?E:null},load(n){if(n!==E)return null;const s=g??e.wsPort??0;return R({host:p,port:s})},async transform(n,s){if(!r||!T)return null;const i=s.split("?")[0]??s;if(i.includes("/node_modules/")||!/\.[jt]sx$/.test(i))return null;const c=await k(n,i);return c?{code:c.code,map:c.map}:null},transformIndexHtml(){if(!(!o||!r))return[{tag:"script",attrs:{type:"module",src:_},injectTo:"head-prepend"}]},handleHotUpdate(n){if(!r)return;const s=n.modules.map(i=>i.file??i.id??null).filter(i=>!!i);y({id:b(),sessionId:t.sessionId??"pending",timestamp:Date.now(),source:"vite",category:"build",severity:"info",context:{sessionId:t.sessionId??"pending"},fingerprint:v({kind:"hmr",identity:n.file}),title:`HMR ${n.file}`,location:{file:n.file},relatedFiles:s,raw:{kind:"hmr",file:n.file}})},generateBundle(n,s){if(!r)return;const i=[];let c=0;for(const A of Object.values(s)){const d=A,B=d.fileName;let m;if(d.type==="chunk")m=(d.code??"").length;else{const a=d.source;m=typeof a=="string"?a.length:a instanceof Uint8Array?a.byteLength:0}c+=m;const w=[];if(d.type==="chunk"&&d.modules){for(const[a,h]of Object.entries(d.modules)){const F=h.renderedLength??h.originalLength??0;w.push({id:a,sizeBytes:F})}w.sort((a,h)=>h.sizeBytes-a.sizeBytes)}i.push({name:B,sizeBytes:m,modules:w})}const O={timestamp:Date.now(),totalBytes:c,chunks:i};y({id:b(),sessionId:t.sessionId??"pending",timestamp:Date.now(),source:"rollup",category:"build",severity:"info",context:{sessionId:t.sessionId??"pending"},fingerprint:v({kind:"bundle",identity:"report"}),title:`Bundle report: ${i.length} chunks, ${c} bytes`,raw:{kind:"bundle-report",report:O}})},async closeBundle(){f&&(await f.close(),f=void 0)}}}l(lensmcpVitePlugin,"lensmcpVitePlugin"),u(lensmcpVitePlugin,"lensmcpVitePlugin");async function k(e,t){try{const[{transformSync:r},{lensmcpBabelPlugin:p}]=await Promise.all([import("@babel/core"),import("@lensmcp/react-instrumentation")]),o=r(e,{filename:t,babelrc:!1,configFile:!1,sourceMaps:!0,parserOpts:{plugins:["jsx","typescript"]},plugins:[[p,{rootDir:process.cwd(),production:!1}]],code:!0});return o?.code?{code:o.code,map:o.map}:null}catch{return null}}l(k,"B"),u(k,"runLensmcpBabel");export function handleClientEnvelope(e,t){N(e,S(t),t.sessionId)}l(handleClientEnvelope,"handleClientEnvelope"),u(handleClientEnvelope,"handleClientEnvelope");function I(e){const t=e?.loc?.file??e?.id;return typeof t!="string"||t.length===0?void 0:{file:t.split("?")[0]||t,...typeof e?.loc?.line=="number"?{line:e.loc.line}:{},...typeof e?.loc?.column=="number"?{column:e.loc.column}:{}}}l(I,"F"),u(I,"errorLocation");function C(e,t){const r=e.err?.message??"Vite error",p=e.err?.stack,o=I(e.err);return{id:b(),sessionId:t??"pending",timestamp:Date.now(),source:"vite",category:"build",severity:"error",context:{sessionId:t??"pending"},fingerprint:v({kind:"vite",identity:e.type??r,...o?{file:o.file}:{}}),title:r,message:p,...o?{location:o}:{},raw:{kind:"build-diagnostic",...o?{file:o.file}:{},...e.err?.plugin?{plugin:e.err.plugin}:{}}}}l(C,"D"),u(C,"viteErrorToEvent");function R(e){return`// @lensmcp/client-runtime (inlined by @lensmcp/vite-plugin)
|
|
293
2
|
(() => {
|
|
294
3
|
if (typeof window === 'undefined') return;
|
|
295
4
|
if (window.__LENSMCP_CLIENT__) return;
|
|
296
5
|
window.__LENSMCP_CLIENT__ = true;
|
|
297
6
|
|
|
298
|
-
// React DevTools hook shim/patch
|
|
7
|
+
// React DevTools hook shim/patch \u2014 this script is the FIRST module script in
|
|
299
8
|
// the document, so it runs BEFORE react-dom evaluates. react-dom only calls
|
|
300
9
|
// onCommitFiberRoot when a hook exists at its module init, so installing (or
|
|
301
10
|
// patching) it here is what gives @lensmcp/react-instrumentation per-commit
|
|
302
11
|
// fiber roots for per-component render attribution. When another hook is
|
|
303
12
|
// already present (React DevTools extension, react-refresh preamble) we
|
|
304
|
-
// CHAIN its onCommitFiberRoot
|
|
13
|
+
// CHAIN its onCommitFiberRoot \u2014 never replace it.
|
|
305
14
|
(() => {
|
|
306
15
|
try {
|
|
307
16
|
const subs = new Set();
|
|
@@ -330,16 +39,16 @@ function buildInlineClientRuntime(opts) {
|
|
|
330
39
|
setStrictMode() {},
|
|
331
40
|
};
|
|
332
41
|
}
|
|
333
|
-
} catch (e) { /* strictly passive
|
|
42
|
+
} catch (e) { /* strictly passive \u2014 never break the host page */ }
|
|
334
43
|
})();
|
|
335
44
|
|
|
336
|
-
const WS_URL = 'ws://${
|
|
45
|
+
const WS_URL = 'ws://${e.host}:${e.port}';
|
|
337
46
|
let ws = null;
|
|
338
47
|
let queue = [];
|
|
339
48
|
let openOnce = false;
|
|
340
49
|
|
|
341
50
|
// Per-TAB identity: sessionStorage is scoped to the tab (and survives
|
|
342
|
-
// reloads in it), so each browser tab gets a stable id
|
|
51
|
+
// reloads in it), so each browser tab gets a stable id \u2014 this is what
|
|
343
52
|
// lets browser://tabs show every open tab instead of one 'default'.
|
|
344
53
|
const TAB_ID = (() => {
|
|
345
54
|
try {
|
|
@@ -363,7 +72,7 @@ function buildInlineClientRuntime(opts) {
|
|
|
363
72
|
send({ kind: 'ready', userAgent: navigator.userAgent, url: location.href });
|
|
364
73
|
});
|
|
365
74
|
ws.addEventListener('close', () => { if (openOnce) setTimeout(open, 1000); });
|
|
366
|
-
ws.addEventListener('error', () => { /* ignore
|
|
75
|
+
ws.addEventListener('error', () => { /* ignore \u2014 close handler reconnects */ });
|
|
367
76
|
}
|
|
368
77
|
open();
|
|
369
78
|
|
|
@@ -394,10 +103,10 @@ function buildInlineClientRuntime(opts) {
|
|
|
394
103
|
const MAX_STACK = 4096;
|
|
395
104
|
function clipStr(s, n) {
|
|
396
105
|
if (typeof s !== 'string' || s.length <= n) return s;
|
|
397
|
-
return s.slice(0, n) + '
|
|
106
|
+
return s.slice(0, n) + '\u2026[lensmcp truncated: ' + (s.length - n) + ' more chars]';
|
|
398
107
|
}
|
|
399
108
|
// A rejected non-Error ({ status: 401 }, a Response) used to be flattened by
|
|
400
|
-
// String(reason) into "[object Object]"
|
|
109
|
+
// String(reason) into "[object Object]" \u2014 the reason was captured and told
|
|
401
110
|
// you nothing.
|
|
402
111
|
function describeReason(r) {
|
|
403
112
|
if (r === null || r === undefined) return String(r);
|
|
@@ -410,7 +119,7 @@ function buildInlineClientRuntime(opts) {
|
|
|
410
119
|
// This listener is registered in the CAPTURE phase, which means it also
|
|
411
120
|
// receives subresource load failures (<img>, <script>, <link>, <video>).
|
|
412
121
|
// Those arrive as a plain Event, NOT an ErrorEvent: no message, no
|
|
413
|
-
// filename, no lineno
|
|
122
|
+
// filename, no lineno \u2014 the only identity is the target element and its
|
|
414
123
|
// URL. Reading only the ErrorEvent fields (as this did) serialised every
|
|
415
124
|
// one of them to a bare { kind:'error', at, tabId } and lost the lot.
|
|
416
125
|
const t = e.target;
|
|
@@ -460,7 +169,7 @@ function buildInlineClientRuntime(opts) {
|
|
|
460
169
|
const start = Date.now();
|
|
461
170
|
// flow-fetch (@lensmcp/react-instrumentation) wraps fetch OUTSIDE this
|
|
462
171
|
// wrapper and publishes flow-correlated network events for the same
|
|
463
|
-
// request
|
|
172
|
+
// request \u2014 sending ours too double-counts every fetch in the timeline.
|
|
464
173
|
const flowFetchActive = !!window.__LENSMCP_FLOW_FETCH__;
|
|
465
174
|
try {
|
|
466
175
|
const res = await origFetch(input, init);
|
|
@@ -487,5 +196,4 @@ function buildInlineClientRuntime(opts) {
|
|
|
487
196
|
try { return JSON.parse(JSON.stringify(v)); } catch { return String(v); }
|
|
488
197
|
}
|
|
489
198
|
})();
|
|
490
|
-
|
|
491
|
-
}
|
|
199
|
+
`}l(R,"N"),u(R,"buildInlineClientRuntime");
|
package/lib/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
"use strict";export{};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/vite-plugin",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@babel/core": "^7.0.0",
|
|
18
|
-
"@lensmcp/bridge": "1.18.
|
|
19
|
-
"@lensmcp/core": "1.18.
|
|
20
|
-
"@lensmcp/protocol-types": "1.18.
|
|
21
|
-
"@lensmcp/react-instrumentation": "1.18.
|
|
22
|
-
"@lensmcp/valtio-instrumentation": "1.18.
|
|
18
|
+
"@lensmcp/bridge": "1.18.6",
|
|
19
|
+
"@lensmcp/core": "1.18.6",
|
|
20
|
+
"@lensmcp/protocol-types": "1.18.6",
|
|
21
|
+
"@lensmcp/react-instrumentation": "1.18.6",
|
|
22
|
+
"@lensmcp/valtio-instrumentation": "1.18.6",
|
|
23
23
|
"tslib": "^2.3.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|