From 8be16804949a43a14cf23472559c7ec34dabc013 Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Mon, 1 Jun 2026 19:48:12 -0400 Subject: [PATCH] chore(web): silence benign config-load dep warnings in npm run dev (#1408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- AGENTS.md | 4 +- clients/web/package.json | 2 +- .../web/server/quiet-config-warnings.d.mts | 8 +++ clients/web/server/quiet-config-warnings.mjs | 66 +++++++++++++++++++ .../test/server/quiet-config-warnings.test.ts | 54 +++++++++++++++ 5 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 clients/web/server/quiet-config-warnings.d.mts create mode 100644 clients/web/server/quiet-config-warnings.mjs create mode 100644 clients/web/src/test/server/quiet-config-warnings.test.ts diff --git a/AGENTS.md b/AGENTS.md index ac743b3e..386694e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/clients/web/package.json b/clients/web/package.json index 98d75583..a2f51301 100644 --- a/clients/web/package.json +++ b/clients/web/package.json @@ -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", diff --git a/clients/web/server/quiet-config-warnings.d.mts b/clients/web/server/quiet-config-warnings.d.mts new file mode 100644 index 00000000..2ba354db --- /dev/null +++ b/clients/web/server/quiet-config-warnings.d.mts @@ -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; + +/** True when `text` is a benign node-only UNRESOLVED_IMPORT warning. */ +export declare function isBenignWarning(text: string): boolean; diff --git a/clients/web/server/quiet-config-warnings.mjs b/clients/web/server/quiet-config-warnings.mjs new file mode 100644 index 00000000..871360bb --- /dev/null +++ b/clients/web/server/quiet-config-warnings.mjs @@ -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); +} diff --git a/clients/web/src/test/server/quiet-config-warnings.test.ts b/clients/web/src/test/server/quiet-config-warnings.test.ts new file mode 100644 index 00000000..b325cde0 --- /dev/null +++ b/clients/web/src/test/server/quiet-config-warnings.test.ts @@ -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("/"); + } + }); +});