Skip to content

Commit dfabe22

Browse files
Auto-enable diagnostics surface when @solidjs/diagnostics is installed
Installing the dev dependency is now the whole setup: the dev-serve diagnostics surface (bridge injection, /__solid/diagnostics endpoint, startup breadcrumb) detects the package with a node_modules walk from the project root and enables itself. The `diagnostics` option becomes an override — `true` forces it on (hard error if the package is missing), `false` opts out. Start mode resolves the same detection for its client-entry bridge import. No host guard on the endpoint: Vite's own DNS-rebinding host check (all supported versions, peer ^8) runs ahead of plugin middleware — verified empirically with a spoofed Host header. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 022d5c2 commit dfabe22

4 files changed

Lines changed: 79 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solidjs/vite-plugin': patch
3+
---
4+
5+
Auto-enable the agent diagnostics surface (dev serve only) when `@solidjs/diagnostics` is installed in the app — installing the dev dependency is now the whole setup. The `diagnostics` option becomes an override: `true` forces it on (erroring if the package is missing), `false` opts out entirely, omitted auto-detects. Start mode's generated/authored client entries follow the same detection for the bridge import.

src/diagnostics/index.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
/**
2-
* Agent diagnostics surface (`diagnostics: true`, dev serve only).
2+
* Agent diagnostics surface (dev serve only).
3+
*
4+
* Enabled automatically when the app has `@solidjs/diagnostics` installed
5+
* (the `diagnostics` option overrides: `true` forces it on and errors if
6+
* the package is missing, `false` opts out entirely).
37
*
48
* Three pieces:
59
* - an injected client module (virtual, imported by index.html or the
@@ -18,6 +22,7 @@
1822
* wire constants are re-declared here with types imported from the
1923
* package, so drift fails the plugin's own compile.
2024
*/
25+
import fs from 'fs';
2126
import path from 'path';
2227
import type { IncomingMessage, ServerResponse } from 'http';
2328
import type { Plugin } from 'vite';
@@ -40,6 +45,28 @@ const METHODS = ['begin', 'end', 'active', 'whyDidRun', 'costs'] as const satisf
4045
/** How long the endpoint waits for a page to answer before failing the call. */
4146
const RESPONSE_TIMEOUT_MS = 10_000;
4247

48+
/**
49+
* Whether the app has `@solidjs/diagnostics` installed — the auto-enable
50+
* signal. A plain node_modules walk (project root upward) rather than
51+
* module resolution: the package is ESM-only, so `require.resolve` can't
52+
* probe it, and this also matches hoisted installs. Yarn PnP has no
53+
* node_modules and isn't detected — `diagnostics: true` is the escape
54+
* hatch there.
55+
*/
56+
export function detectDiagnosticsPackage(root: string): boolean {
57+
let dir = path.resolve(root);
58+
while (true) {
59+
if (
60+
fs.existsSync(path.join(dir, 'node_modules', DIAGNOSTICS_PACKAGE, 'package.json'))
61+
) {
62+
return true;
63+
}
64+
const parent = path.dirname(dir);
65+
if (parent === dir) return false;
66+
dir = parent;
67+
}
68+
}
69+
4370
export function diagnosticsClientModuleCode(): string {
4471
// Runtime imports resolve to the APP's diagnostics package (see the
4572
// resolveId assist below) — the page speaks its own package's protocol.
@@ -103,9 +130,13 @@ function readJsonBody(req: IncomingMessage): Promise<unknown> {
103130
});
104131
}
105132

106-
export function solidDiagnostics(): Plugin {
133+
export function solidDiagnostics(mode: true | 'auto' = 'auto'): Plugin {
107134
let root = process.cwd();
108135
let base = '/';
136+
// Resolved at configResolved: explicit `true` is unconditional (missing
137+
// package becomes a hard error at bridge resolution); `'auto'` enables
138+
// only when the app has the package installed.
139+
let enabled = mode === true;
109140

110141
return {
111142
name: 'solid:diagnostics',
@@ -117,6 +148,7 @@ export function solidDiagnostics(): Plugin {
117148
configResolved(config) {
118149
root = config.root;
119150
base = config.base;
151+
if (mode === 'auto') enabled = detectDiagnosticsPackage(root);
120152
},
121153

122154
async resolveId(source, importer) {
@@ -131,9 +163,9 @@ export function solidDiagnostics(): Plugin {
131163
});
132164
if (!resolved || resolved.id.startsWith('__vite-optional-peer-dep:')) {
133165
this.error(
134-
`[@solidjs/vite-plugin] the diagnostics option requires ${DIAGNOSTICS_PACKAGE} ` +
166+
`[@solidjs/vite-plugin] the diagnostics surface requires ${DIAGNOSTICS_PACKAGE} ` +
135167
'installed in the app (it provides the in-page bridge). Install it as a ' +
136-
'development dependency or remove `diagnostics: true`.',
168+
'development dependency, or set `diagnostics: false` to opt out.',
137169
);
138170
}
139171
return resolved;
@@ -149,6 +181,7 @@ export function solidDiagnostics(): Plugin {
149181
// Plain (index.html) apps get the client module injected here;
150182
// start-mode apps import it from the generated client entry instead.
151183
transformIndexHtml() {
184+
if (!enabled) return undefined;
152185
return [
153186
{
154187
tag: 'script',
@@ -159,6 +192,11 @@ export function solidDiagnostics(): Plugin {
159192
},
160193

161194
configureServer(server) {
195+
// The whole surface (announcement, middleware, bridge injection) only
196+
// exists when enabled, so the discovery breadcrumb never lies about
197+
// a dead endpoint.
198+
if (!enabled) return;
199+
162200
// Announce the surface in the startup block. This is a discovery
163201
// channel: agents watching dev-server output learn the endpoint and
164202
// the skill documents without any project-level pointer (AGENTS.md).
@@ -171,8 +209,9 @@ export function solidDiagnostics(): Plugin {
171209
: DIAGNOSTICS_ENDPOINT;
172210
server.config.logger.info(
173211
` ➜ Solid diagnostics: ${endpoint} ` +
174-
`(GET status; POST {"method":"begin"|"end"|"whyDidRun"|"costs"})\n` +
175-
` ➜ Agent skills: node_modules/${DIAGNOSTICS_PACKAGE}/skills/agent-loops/SKILL.md, ` +
212+
`(GET status; POST {"method":"begin"|"end"|"whyDidRun"|"costs"})` +
213+
(mode === 'auto' ? ' — auto-enabled; `diagnostics: false` opts out' : '') +
214+
`\n ➜ Agent skills: node_modules/${DIAGNOSTICS_PACKAGE}/skills/agent-loops/SKILL.md, ` +
176215
`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`,
177216
);
178217
};
@@ -192,6 +231,10 @@ export function solidDiagnostics(): Plugin {
192231
entry.resolve(data);
193232
});
194233

234+
// No host/origin validation here: on all supported Vite versions
235+
// (peer range ^8) Vite's own DNS-rebinding host check runs ahead of
236+
// plugin middleware — verified: requests with a disallowed Host
237+
// header get Vite's 403 before reaching this handler.
195238
server.middlewares.use(DIAGNOSTICS_ENDPOINT, async (req, res) => {
196239
// The middleware mounts on the exact path; anything deeper is 404.
197240
if (req.url && req.url !== '/' && req.url !== '') {

src/index.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,17 @@ export interface Options {
225225
* Dev-serve only: expose Solid's diagnostic and attribution channels to
226226
* out-of-process consumers (agents, tests, curl). Injects a client module
227227
* that installs the in-page bridge from the app's own
228-
* `@solidjs/diagnostics` (which must be installed as a dev dependency),
229-
* and serves a `/__solid/diagnostics` endpoint on the dev server that
230-
* forwards capture control (`begin`/`end`), `whyDidRun`, and cost queries
231-
* to the page over the Vite WebSocket. No effect on builds or preview.
228+
* `@solidjs/diagnostics`, and serves a `/__solid/diagnostics` endpoint on
229+
* the dev server that forwards capture control (`begin`/`end`),
230+
* `whyDidRun`, and cost queries to the page over the Vite WebSocket. No
231+
* effect on builds or preview.
232232
*
233-
* @default false
233+
* Omitted (the default), the surface auto-enables when
234+
* `@solidjs/diagnostics` is installed in the app — installing the dev
235+
* dependency is the whole setup. `true` forces it on (erroring if the
236+
* package is missing); `false` opts out entirely.
237+
*
238+
* @default undefined (auto-detect)
234239
*/
235240
diagnostics?: boolean;
236241
/**
@@ -1270,7 +1275,7 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
12701275
serverComponents,
12711276
ssr: !!options.ssr,
12721277
styleFilter: filterDevStyles,
1273-
diagnostics: !!options.diagnostics,
1278+
diagnostics: options.diagnostics ?? 'auto',
12741279
onDocumentResolved(documentPath) {
12751280
// Normalize to forward slashes to match Vite's transform ids.
12761281
documentModuleId = documentPath ? documentPath.split(path.sep).join('/') : null;
@@ -1280,9 +1285,11 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
12801285
}
12811286

12821287
// Agent diagnostics endpoint + injected bridge (dev serve only — the
1283-
// plugin no-ops itself for builds and preview via `apply`).
1284-
if (options.diagnostics) {
1285-
plugins.push(solidDiagnostics());
1288+
// plugin no-ops itself for builds and preview via `apply`, and in the
1289+
// default auto mode additionally disables itself unless the app has
1290+
// `@solidjs/diagnostics` installed).
1291+
if (options.diagnostics !== false) {
1292+
plugins.push(solidDiagnostics(options.diagnostics === true ? true : 'auto'));
12861293
}
12871294

12881295
// Builder-mode (environments API) client-before-server build ordering.

src/ssr/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import {
5959
DEVTOOLS_PACKAGE,
6060
devtoolsMountModuleCode,
6161
} from '../devtools/index.js';
62-
import { DIAGNOSTICS_CLIENT_ID } from '../diagnostics/index.js';
62+
import { DIAGNOSTICS_CLIENT_ID, detectDiagnosticsPackage } from '../diagnostics/index.js';
6363
import {
6464
collectDevStyles,
6565
collectDevStyleSources,
@@ -434,7 +434,8 @@ export function startServe(
434434
serverComponents?: boolean;
435435
ssr?: boolean;
436436
styleFilter?: DevStyleFilter;
437-
diagnostics?: boolean;
437+
/** `'auto'` = enable when the app has `@solidjs/diagnostics` installed. */
438+
diagnostics?: boolean | 'auto';
438439
/**
439440
* Reports the resolved document shell path (absolute, or null when the
440441
* built-in virtual document is used) back to the main plugin, which
@@ -464,7 +465,9 @@ export function startServe(
464465
const serverComponents = !!internal.serverComponents;
465466
const errorBoundary = options.errorBoundary !== false;
466467
const styleFilter = internal.styleFilter;
467-
const diagnostics = !!internal.diagnostics;
468+
// `'auto'` resolves against the project root in configResolved, before
469+
// any of the (lazy) uses in entry codegen and the entry transform.
470+
let diagnostics = internal.diagnostics === true;
468471
let devtoolsEnabled = false;
469472
let devtoolsResolutions: Partial<
470473
Record<'client' | 'server', Promise<string | null>>
@@ -1266,6 +1269,9 @@ export function startServe(
12661269
root = config.root;
12671270
base = config.base;
12681271
isBuild = config.command === 'build';
1272+
if (internal.diagnostics === 'auto' && !isBuild) {
1273+
diagnostics = detectDiagnosticsPackage(root);
1274+
}
12691275
},
12701276
resolveId(source, importer, opts) {
12711277
if (source === HANDLER_ID) {

0 commit comments

Comments
 (0)