what-devtools-mcp 0.11.3 → 0.11.4

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.
Files changed (2) hide show
  1. package/package.json +7 -2
  2. package/src/vite-plugin.js +69 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-devtools-mcp",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "MCP server bridging AI agents to live What Framework app state via WebSocket",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,5 +39,10 @@
39
39
  "bugs": {
40
40
  "url": "https://github.com/CelsianJs/what-framework/issues"
41
41
  },
42
- "homepage": "https://whatfw.com"
42
+ "homepage": "https://whatfw.com",
43
+ "peerDependenciesMeta": {
44
+ "what-devtools": {
45
+ "optional": true
46
+ }
47
+ }
43
48
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { readFileSync } from 'fs';
13
+ import { createRequire } from 'module';
13
14
  import { join } from 'path';
14
15
 
15
16
  function resolveToken(explicitToken) {
@@ -50,10 +51,63 @@ const BROWSER_BOOTSTRAP_URL = '/@id/__x00__' + VIRTUAL_BOOTSTRAP_ID;
50
51
  export const DISCOVERY_PATH = '/__what_mcp_discovery';
51
52
 
52
53
  export default function whatDevToolsMCP({ port = 9229, token = '' } = {}) {
54
+ // Defense-in-depth: `apply: 'serve'` is the primary guard — Vite excludes the
55
+ // whole plugin from `vite build`, so none of these hooks run for a production
56
+ // bundle. But `apply` can be defeated (a meta-framework that flattens plugin
57
+ // arrays and re-invokes hooks, a consumer that spreads this plugin into
58
+ // another plugin's returned list, or a future Vite change), and getting it
59
+ // wrong once ships devtools + a dev-server `<script src>` into production
60
+ // (observed on a real deploy: the prod page requested
61
+ // `virtual:what-devtools-mcp/bootstrap` and, with a dev server live on the
62
+ // machine, followed it to localhost). So we ALSO gate every injecting hook on
63
+ // the resolved Vite command: if we ever run under `command === 'build'`, we
64
+ // resolve/load/inject NOTHING. `command` stays undefined when configResolved
65
+ // isn't called (unit tests, manual `plugin.load(...)`) — treated as serve.
66
+ let command;
67
+ let projectRoot;
68
+ let devtoolsResolvable; // memoized: is the `what-devtools` peer installed?
69
+ const isBuild = () => command === 'build';
70
+
71
+ // The injected bootstrap imports `what-devtools` (an OPTIONAL peer dependency).
72
+ // If the consumer hasn't installed it, Vite's dev transform cannot resolve that
73
+ // bare import and the whole dev server crashes with a transform error. Detect
74
+ // availability up front and degrade gracefully: inject nothing, log once. Never
75
+ // crash `vite dev` just because the debugging peer is absent.
76
+ const devtoolsAvailable = () => {
77
+ if (devtoolsResolvable === undefined) {
78
+ try {
79
+ // Resolve from the consumer project root, where the peer would live.
80
+ createRequire(join(projectRoot || process.cwd(), 'noop.js')).resolve('what-devtools');
81
+ devtoolsResolvable = true;
82
+ } catch {
83
+ devtoolsResolvable = false;
84
+ }
85
+ }
86
+ return devtoolsResolvable;
87
+ };
88
+
89
+ // Injecting hooks are inactive during a production build OR when the devtools
90
+ // peer is missing — in both cases we resolve/load/inject NOTHING.
91
+ const inactive = () => isBuild() || !devtoolsAvailable();
92
+
53
93
  return {
54
94
  name: 'what-devtools-mcp',
55
95
  apply: 'serve',
56
96
 
97
+ // Capture the resolved command so the injecting hooks below can hard-refuse
98
+ // to run during a production build even if `apply: 'serve'` was bypassed.
99
+ configResolved(config) {
100
+ command = config.command;
101
+ projectRoot = config.root;
102
+ // One clear, non-fatal notice when the peer is absent during dev.
103
+ if (!isBuild() && !devtoolsAvailable()) {
104
+ console.info(
105
+ '[what-devtools-mcp] `what-devtools` is not installed — skipping DevTools/MCP dev injection. ' +
106
+ 'Run `npm i -D what-devtools` to enable live debugging.',
107
+ );
108
+ }
109
+ },
110
+
57
111
  // Node-side bridge probe, exposed same-origin to the browser client.
58
112
  // Responds { bridge: false } when no bridge is running (quietly), or
59
113
  // { bridge: true, token, wsPort } when it is — one round-trip discovery.
@@ -83,6 +137,7 @@ export default function whatDevToolsMCP({ port = 9229, token = '' } = {}) {
83
137
 
84
138
  // Resolve the virtual module so Vite knows we own it.
85
139
  resolveId(id) {
140
+ if (inactive()) return null; // never own the virtual module in a build or when the peer is missing
86
141
  if (id === VIRTUAL_BOOTSTRAP_ID || id === RESOLVED_BOOTSTRAP_ID) {
87
142
  return RESOLVED_BOOTSTRAP_ID;
88
143
  }
@@ -94,18 +149,30 @@ export default function whatDevToolsMCP({ port = 9229, token = '' } = {}) {
94
149
  // properly rewritten to dev-server URLs — unlike inline <script type=module>
95
150
  // tags injected via transformIndexHtml, which Vite does not transform.
96
151
  load(id) {
152
+ if (inactive()) return null; // never emit devtools source into a build or when the peer is missing
97
153
  if (id !== RESOLVED_BOOTSTRAP_ID) return null;
98
154
  const tokenValue = resolveToken(token);
155
+ // The side effects are gated behind `import.meta.env.DEV`. In a dev server
156
+ // that's statically `true`, so devtools install and connect normally. If
157
+ // this module ever ends up in a production bundle (e.g. a consumer imports
158
+ // the virtual id directly, or a bundler pulls it in despite the guards
159
+ // above), `import.meta.env.DEV` is statically `false`, the whole block is
160
+ // dead-code-eliminated, and the now-unused imports tree-shake away — so a
161
+ // prod bundle carries zero devtools/MCP code and can never open a
162
+ // connection to a local dev server.
99
163
  return [
100
164
  `import * as core from 'what-core';`,
101
165
  `import { installDevTools } from 'what-devtools';`,
102
166
  `import { connectDevToolsMCP } from 'what-devtools-mcp/client';`,
103
- `installDevTools(core);`,
104
- `connectDevToolsMCP({ port: ${port}, token: ${JSON.stringify(tokenValue)}, discoveryUrl: ${JSON.stringify(DISCOVERY_PATH)} });`,
167
+ `if (import.meta.env && import.meta.env.DEV) {`,
168
+ ` installDevTools(core);`,
169
+ ` connectDevToolsMCP({ port: ${port}, token: ${JSON.stringify(tokenValue)}, discoveryUrl: ${JSON.stringify(DISCOVERY_PATH)} });`,
170
+ `}`,
105
171
  ].join('\n');
106
172
  },
107
173
 
108
174
  transformIndexHtml() {
175
+ if (inactive()) return; // never inject the bootstrap <script> into a build or when the peer is missing
109
176
  // Inject a <script src> that points at the virtual module. The browser
110
177
  // fetches `/@id/__x00__virtual:what-devtools-mcp/bootstrap`, Vite serves
111
178
  // the transformed bootstrap (bare specifiers resolved), and everything