* fix: load @napi-rs/keyring lazily so an unsupported platform degrades `@napi-rs/keyring` ships one prebuilt binary per platform triple and throws on import where it has none. On Android / Termux there is no `@napi-rs/keyring-android-arm64`, so the static top-level import in `core/auth/node/secret-store.ts` threw during module evaluation and the Inspector exited at startup with "Cannot find native binding" — before any of the keychain-unavailable handling in that same file could run. Replaces the static import with a cached dynamic one. The outcome is cached, not just the module, so a box without a binary doesn't re-attempt (and re-throw) resolution on every secret operation — `expectedSecretFields` means that would otherwise be once per server per `GET /api/servers` — and so `set` can name the underlying cause in its `KeychainUnavailableError`. An unloadable package now folds into the same degradation contract as an unreachable keychain: `get` returns null, `delete` and `deleteAllForServer` no-op, and `set` throws `KeychainUnavailableError` (not double-wrapped — the load failure is already typed by the time the catch sees it). The class doc comment now enumerates all three ways "unavailable" can arrive, since each escaped the contract at some point. The shared `vi.mock` stub can't express this — it models a keyring that loads — so the new tests build on `vi.resetModules()` + `vi.doMock` with a throwing factory and re-import the module, covering the load failure, each method under it, and the load-once caching. Verified the dynamic import survives bundling in all three clients (cli, tui, and the web runner) rather than being inlined back to a static one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XAR2Nr9kXbrywFNUVoTe9F * fix: steer a missing native binding to a reinstall hint `KeychainUnavailableError` gave the Linux libsecret / gnome-keyring advice for every cause, including the one where the keychain is fine and the @napi-rs/keyring platform package is what's missing — an unsupported triple (#1905) or npm's optional-deps bug dropping it on a supported one (npm/cli#4828, the Windows report on #1852). Detect the loader's "Cannot find native binding" phrasing and point those users at a reinstall / npx-cache clear instead. Ported from the superseded #1943, which fixed this alongside the same lazy-load change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3 * fix: name a bad keyring namespace instead of "is not a constructor" The lazy import accepted whatever the resolution produced. The named exports arrive through CJS interop, so a resolution that stopped yielding them — a default-only export upstream, a bundler changing interop, a platform where named-export detection fails — would hand us `undefined`, and `new mod.AsyncEntry(...)` would throw a TypeError inside the very try that implements graceful degradation. Shape-check the resolved module and treat a bad shape as unavailable. Scope, because it is narrower than it first appears: this does NOT prevent a bad shape from emptying the secret list. `get` returns null either way — that is its read-tolerance contract, and the TypeError lands in the same catch a dead keychain does. What changes is the diagnosis. Without the check the only signal is `set` reporting "keyring.mod.AsyncEntry is not a constructor", which reads like an Inspector bug; with it, `set` names the real problem once, at the load boundary, in the same actionable 503 as every other unavailability. Catching the silent-empty-list case itself would need a round-trip against the unmocked package, which nothing in the suite does today. The member access sits inside the try because reading a missing export is not always a harmless undefined — a Proxy-backed namespace can throw, as vitest's module mocks do — and letting that escape would reject the cached promise, which is otherwise guaranteed to always resolve. Of the four new tests only the cause-message one fails without the guard; the rest pin the surrounding contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3 * docs: correct the test header's count of unavailability entry points The shape check added a fourth way "unavailable" can arrive, and the enumeration in secret-store.ts was updated to match, but the test file's header still said three — so it read as an exhaustive list that silently omitted the case a whole describe below it covers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3 * fix: give the wrong-shape cause its own remediation hint The shape check added a cause that fell through to the libsecret advice — so a packaging mismatch told the user to install a keyring daemon, which cannot help, and on Windows or macOS points somewhere that does not exist. That is the same wrong-advice bug the native-binding branch was added to fix, reintroduced by the guard itself. Hint selection moves into `hintFor`, where libsecret is the explicit fallback rather than the default, and the shape failure becomes a distinct `KeyringModuleShapeError` matched by type — it is our own error, so there is no reason to re-parse text we wrote (the native-binding branch matches a string only because that message comes from napi-rs). Also retitles the wrong-shape test: it claimed the guard avoids "returning null secrets", but `get` returns null either way by the read-tolerance contract. The title now says what actually differs — `set` hard-fails — matching the correction already made in the comments. Both raised by Copilot review. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3 * fix: give both shape-check failure paths the packaging hint The shape check can fail two ways — the members are absent, or reading them throws — and only the first carried KeyringModuleShapeError. The catch path returned the raw error, so a Proxy-backed namespace throwing on access fell through to the libsecret advice: the same wrong-remedy bug as the previous two commits, one branch further along. Both paths now carry the type. KeyringModuleShapeError takes a detail string plus a standard `cause`, so the throwing path reads "its exports could not be read: <original>" and keeps the underlying failure legible while still earning the packaging hint. Three instances of one mistake in a row (a new cause added without deciding which remediation it inherits) says the shape was wrong, not just the branch: making libsecret an explicit fallback in `hintFor` did not stop me adding a second exit that skipped it. The tests now pin the invariant directly — one asserts both construction paths produce the same hint — so the next cause that forgets is a failing test rather than a reviewer's catch. Raised by Copilot review. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
75 lines
3 KiB
TypeScript
75 lines
3 KiB
TypeScript
/**
|
|
* Shared Vite config (build externals + optimizeDeps). Consumed by `vite.config.ts`
|
|
* and by `start-vite-dev-server.ts` so the in-process Vite starter can apply the
|
|
* same node-only exclusions without re-loading `vite.config.ts` (Node can't
|
|
* import .ts directly).
|
|
*
|
|
* Aliases and the vitest project setup live in `vite.config.ts` itself — only
|
|
* the parts needed by both the CLI runner and `vite dev` belong here.
|
|
*/
|
|
|
|
import { rmSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE = [
|
|
"@modelcontextprotocol/client/stdio",
|
|
// `atomically` is reached only through `core/storage/store-io.ts`,
|
|
// which is imported by `core/mcp/remote/node/server.ts` (the Hono
|
|
// app). The module never lands in the browser graph; excluding it
|
|
// keeps Vite's dev-time scanner from chasing it through the plugin's
|
|
// node-only import chain.
|
|
"atomically",
|
|
// `chokidar` is only loaded inside `core/mcp/remote/node/server.ts`
|
|
// when the lazy mcp.json watcher starts. It transitively imports
|
|
// `readdirp` and core node fs/os modules; excluding it keeps Vite's
|
|
// dep scanner from walking into them during dev startup.
|
|
"chokidar",
|
|
"cross-spawn",
|
|
"which",
|
|
// `@napi-rs/keyring` is loaded only inside
|
|
// `core/auth/node/secret-store.ts` from the Hono `/api/servers`
|
|
// handlers — and lazily even there, via a cached dynamic import, so
|
|
// an unsupported platform degrades instead of crashing at startup
|
|
// (#1905). It's a native-binding package (no browser code path) so
|
|
// excluding it keeps Vite's dep scanner from chasing into the
|
|
// platform-specific binaries during dev startup.
|
|
"@napi-rs/keyring",
|
|
] as const;
|
|
|
|
export function getViteBaseConfig() {
|
|
return {
|
|
optimizeDeps: {
|
|
exclude: [...NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE],
|
|
// Pre-bundle the browser-safe client barrel up front so Vitest's
|
|
// Storybook (browser) project doesn't discover it lazily and re-optimize
|
|
// mid-run — which reloads the worker and fails in-flight dynamic story
|
|
// imports with "Failed to fetch dynamically imported module". Several
|
|
// components import it at runtime (e.g. `protocolUtils`'s
|
|
// `isInputRequiredResult` / `SUBSCRIPTION_ID_META_KEY`), so listing it
|
|
// here keeps the dep graph stable across story files.
|
|
include: ["@modelcontextprotocol/client"],
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Dev-server optimizeDeps: rebuild from scratch each launch — no stale
|
|
* pre-bundles carried across restarts (avoids 504 Outdated Optimize Dep).
|
|
* Do not use under Vitest.
|
|
*/
|
|
export function getViteDevOptimizeDeps() {
|
|
return {
|
|
exclude: [...NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE],
|
|
force: true,
|
|
ignoreOutdatedRequests: true,
|
|
include: ["ajv", "@modelcontextprotocol/client/validators/ajv"],
|
|
};
|
|
}
|
|
|
|
/** Remove Vite's pre-bundle cache under clients/web before a dev launch. */
|
|
export function clearViteDepsCache(clientWebRoot: string): void {
|
|
rmSync(join(clientWebRoot, "node_modules", ".vite"), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
}
|