chore(web): silence benign config-load dep warnings in npm run dev (#1408)

* chore(web): silence benign config-load dep warnings in `npm run dev`

`vite dev` printed three UNRESOLVED_IMPORT warnings at startup (chokidar,
atomically, @napi-rs/keyring). They originate from Vite's config bundler
(Rolldown): the Hono plugin statically imports the node-only dev backend
(core/mcp/remote/node/server.ts), so loading vite.config.ts bundles it and
Rolldown walks its node-only deps. Those deps are correctly externalized — the
browser never loads them and Node resolves them at runtime — so the warnings are
pure noise.

They can't be filtered the usual ways: the static import is required (the
monorepo has no node_modules under core/, so the aliases must resolve the deps
at bundle time — ssrLoadModule / `--configLoader runner` both fail to resolve
them), and the warnings are written directly to the process streams during
config load, bypassing customLogger, logLevel, and rollupOptions.onwarn.

Fix: a tiny `--import` hook (server/quiet-config-warnings.mjs) that drops only
those specific benign warning writes, wired into the `dev` script. Scoped to
`npm run dev` — build, tests, and prod are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(web): harden dev warning filter per review

- Tighten BENIGN_PAIRS to repo-relative path fragments (not basenames) so an
  unrelated future UNRESOLVED_IMPORT warning mentioning the same dep + a
  same-named file isn't silently dropped. Verified Rolldown prints the full
  repo-relative path. (review #1)
- Add a note that the matcher tracks Rolldown's message format and degrades to a
  safe no-op (warnings return) if that format changes. (review #2)
- Export isBenignWarning/BENIGN_PAIRS, guard the stream patching behind
  !VITEST, add a .d.mts so the pure helper is importable from TS, and add a unit
  test locking the signature pairs and the over-match guards. (review #3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-06-01 19:48:12 -04:00 • committed by GitHub
commit 8be1680494
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 132 additions and 2 deletions

View file

@ -16,7 +16,9 @@ inspector/
│ │ │ # web-server-config.ts (env parsing + initial-config payload + banner),
│ │ │ # sandbox-controller.ts (MCP Apps sandbox HTTP server),
│ │ │ # inject-auth-token.ts (embeds the API token into served index.html),
│ │ │ # vite-base-config.ts (shared optimizeDeps exclusions)
│ │ │ # vite-base-config.ts (shared optimizeDeps exclusions),
│ │ │ # quiet-config-warnings.mjs (--import hook for `npm run dev`: drops benign
│ │ │ # node-only UNRESOLVED_IMPORT warnings Rolldown prints at config load)
│ │ └── static/ # sandbox_proxy.html (served by sandbox-controller for MCP Apps tab)
│ ├── cli/ # CLI client
│ ├── tui/ # TUI client

View file

@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "node --import ./server/quiet-config-warnings.mjs ./node_modules/vite/bin/vite.js",
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier --write src",

View file

@ -0,0 +1,8 @@
/** Type declarations for the dev-only `quiet-config-warnings.mjs` hook so its
* pure helpers can be imported (and unit-tested) from TypeScript. */
/** (package-quote, repo-relative-source-path) pairs of benign warnings to drop. */
export declare const BENIGN_PAIRS: ReadonlyArray<readonly [string, string]>;
/** True when `text` is a benign node-only UNRESOLVED_IMPORT warning. */
export declare function isBenignWarning(text: string): boolean;

View file

@ -0,0 +1,66 @@
/**
* Drops the benign "UNRESOLVED_IMPORT" warnings Vite's config bundler (Rolldown)
* prints at `vite dev` startup for node-only backend deps that the Hono plugin
* pulls in (`chokidar`, `atomically`, `@napi-rs/keyring`).
*
* Why a stream filter and not a Vite hook: the dev backend
* (`core/mcp/remote/node/server.ts`) is statically imported by the Hono plugin,
* so loading vite.config.ts bundles it and Rolldown walks its node-only deps.
* Those deps are correctly externalized (the browser never loads them; Node
* resolves them at runtime), but Rolldown still warns. The warnings are printed
* directly to the process streams during config loading — before any Vite
* logger exists — so `customLogger`, `logLevel`, and `rollupOptions.onwarn` all
* miss them. Each warning (header + code frame) is emitted as a single write,
* so we drop any write whose text carries the unresolved-import signature for
* one of these known-benign (package, source-file) pairs. Installed via
* `node --import` ahead of the Vite CLI; only affects the dev script.
*
* This matcher is tied to Rolldown's current warning format (the `'pkg'` quote
* style and the repo-relative source path it prints). If these warnings ever
* reappear, check whether Rolldown changed its message format. The failure mode
* is safe: a format change just makes the filter a no-op (the warnings return),
* it never drops anything else.
*/
// (package, source-file) pairs. The file is a repo-relative path fragment — not
// just a basename — so an unrelated future warning that happens to mention the
// same dep and a same-named file isn't silently dropped.
export const BENIGN_PAIRS = [
["'chokidar'", "core/mcp/remote/node/server.ts"],
["'atomically'", "core/storage/store-io.ts"],
["'@napi-rs/keyring'", "core/auth/node/secret-store.ts"],
];
export function isBenignWarning(text) {
if (!text.includes("UNRESOLVED_IMPORT")) return false;
return BENIGN_PAIRS.some(
([dep, file]) => text.includes(dep) && text.includes(file),
);
}
function patch(stream) {
const original = stream.write.bind(stream);
stream.write = (chunk, encoding, callback) => {
const text =
typeof chunk === "string"
? chunk
: Buffer.isBuffer(chunk)
? chunk.toString("utf8")
: "";
if (text && isBenignWarning(text)) {
// Pretend the write succeeded so callers/promises don't stall.
if (typeof encoding === "function") encoding();
else if (typeof callback === "function") callback();
return true;
}
return original(chunk, encoding, callback);
};
}
// Guard so importing this module (e.g. from a unit test for isBenignWarning)
// doesn't monkey-patch the test runner's streams. Under `npm run dev` VITEST is
// unset, so the patch installs as intended.
if (!process.env.VITEST) {
patch(process.stdout);
patch(process.stderr);
}

View file

@ -0,0 +1,54 @@
import { describe, it, expect } from "vitest";
import {
isBenignWarning,
BENIGN_PAIRS,
} from "../../../server/quiet-config-warnings.mjs";
// A representative Rolldown config-load warning block (header + code frame), as
// captured from `vite dev` startup.
function warning(dep: string, file: string): string {
return (
`${file} (8:36) [UNRESOLVED_IMPORT] Warning: Could not resolve ${dep} in ${file}\n` +
` ╭─[ ${file}:8:37 ]\n` +
` 8 │ import { readFile } from ${dep};\n` +
` ╰─ Module not found, treating it as an external dependency\n`
);
}
describe("quiet-config-warnings / isBenignWarning", () => {
it("matches each benign node-only dep warning", () => {
for (const [dep, file] of BENIGN_PAIRS) {
expect(isBenignWarning(warning(dep, file))).toBe(true);
}
});
it("ignores non-UNRESOLVED output", () => {
expect(isBenignWarning("VITE v8.0.0 ready in 603 ms")).toBe(false);
expect(
isBenignWarning("Sandbox (MCP Apps): http://localhost:61146/sandbox"),
).toBe(false);
});
it("does not drop an UNRESOLVED_IMPORT for a different (real) dependency", () => {
expect(
isBenignWarning(
"src/App.tsx (1:0) [UNRESOLVED_IMPORT] Warning: Could not resolve 'some-real-pkg' in src/App.tsx",
),
).toBe(false);
});
it("requires the matching source path, not just the package name", () => {
// Same dep, but in an unrelated file → not silenced (guards over-matching).
expect(
isBenignWarning(
"src/elsewhere.ts (1:0) [UNRESOLVED_IMPORT] Warning: Could not resolve 'chokidar' in src/elsewhere.ts",
),
).toBe(false);
});
it("uses repo-relative path fragments (not bare basenames)", () => {
for (const [, file] of BENIGN_PAIRS) {
expect(file).toContain("/");
}
});
});