inspector/clients/web/server/vite-base-config.ts
cliffhall 5b9405fe8c fix: force the Storybook dep pre-bundle on every run instead of trusting the cache
Vite keys the Storybook project's dep cache on the lockfile and the config,
never on what the story graph imports. After a story or component gains an
import of an already-installed package, the cached bundle is still "valid",
Vite skips the scan, and the first request for the new package re-runs the
optimizer mid-run: the browser hash is bumped, a full-reload is sent that the
Vitest tester iframes ignore, and the `@storybook/react` renderer already
loaded in a live iframe keeps the old `?v=` for its lazy
`import("@storybook/react-dom-shim")`, so every story that iframe renders from
then on fails with "Failed to fetch dynamically imported module". The next run
is green because the cache has caught up.

`optimizeDeps.force: true` on the storybook project discards the cache up
front so the scanner crawls every story entry before the browser opens — the
cold path CI already takes on every PR. Measured: stale-cache 6/6 red on the
baseline, 6/6 green with the fix, 3/3 red reverted; ~0.8s per run.

Closes #2340

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-12 19:11:08 -04:00

90 lines
4 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",
// `proper-lockfile` is reached only through `core/auth/node/file-lock.ts`
// — the secrets file's cross-process lock (#2082) — which the Hono
// `/api/servers` handlers pull in via `core/auth/node/file-secret-store.ts`.
// Same node-only import chain as `atomically` above, and the same reason:
// it is CJS with a `graceful-fs`/`signal-exit`/`retry` graph that Vite's
// dev scanner has no business walking. Note the tsup `external` lists do
// **not** cover this — they configure the production bundles, not `vite dev`.
"proper-lockfile",
] 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.
//
// That was one instance of a class, not the class: any package a story
// or component starts importing after the cache was written is
// discovered the same way, and pinning each one here would chase the
// symptom. The `storybook` project in `vite.config.ts` now sets
// `optimizeDeps.force` so every run re-scans the story entries up front
// (#2340); this entry stays as the explicit include it always was.
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,
});
}