Merge pull request #2342 from modelcontextprotocol/v2/fix/2340-storybook-optimizer-race

fix: force the Storybook dep pre-bundle on every run (#2340)
This commit is contained in:
Cliff Hall 2026-09-12 19:34:30 -04:00 • committed by GitHub
commit 856729fb4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 66 additions and 0 deletions

View file

@ -55,11 +55,47 @@ export function getViteBaseConfig() {
// 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. `getStorybookOptimizeDeps` below adds `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"],
},
};
}
/**
* Storybook (browser Vitest project) optimizeDeps: the base set, re-bundled
* on every run instead of read from the cache (#2340).
*
* Vite keys that cache on the lockfile and on the config, never on what the
* story graph imports, so a cached bundle stays "valid" after a story or
* component gains an import of a package that is already installed. With a
* valid cache Vite skips the scan, and the first request for the new package
* re-runs the optimizer mid-run: the shared chunks are rewritten, the `?v=`
* browser hash is bumped, and Vite sends a `full-reload` that the Vitest
* tester iframes do not act on. The `@storybook/react` renderer already loaded
* in a live iframe still holds the old hash 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" — and
* the next run is green, because the cache has caught up. `force` discards the
* cache up front so the scanner crawls every story entry before the browser
* opens: the same cold path CI takes on every PR. Cost: ~0.8s per run.
*
* Spread from `getViteBaseConfig()` rather than left to Vitest's project merge
* so the include/exclude the base set carries are the helper's own claim, and
* tested as such.
*/
export function getStorybookOptimizeDeps() {
return {
...getViteBaseConfig().optimizeDeps,
force: true,
};
}
/**
* Dev-server optimizeDeps: rebuild from scratch each launch — no stale
* pre-bundles carried across restarts (avoids 504 Outdated Optimize Dep).

View file

@ -10,6 +10,7 @@ import { join } from "node:path";
import { tmpdir } from "node:os";
import {
clearViteDepsCache,
getStorybookOptimizeDeps,
getViteBaseConfig,
getViteDevOptimizeDeps,
} from "../../../../server/vite-base-config.js";
@ -54,6 +55,30 @@ describe("getViteDevOptimizeDeps", () => {
});
});
describe("getStorybookOptimizeDeps", () => {
it("forces a re-bundle on every run so a stale cache cannot re-optimize mid-run (#2340)", () => {
const config = getStorybookOptimizeDeps();
expect(config.force).toBe(true);
});
it("keeps the base include and exclude sets", () => {
const config = getStorybookOptimizeDeps();
const base = getViteBaseConfig().optimizeDeps;
expect(config.include).toEqual(base.include);
expect(config.exclude).toEqual(base.exclude);
expect(config.include).toContain("@modelcontextprotocol/client");
});
it("does not carry the dev server's stale-request tolerance", () => {
// `ignoreOutdatedRequests` belongs to `vite dev`, where a full page reload
// follows a re-optimization; under Vitest a request for an outdated dep
// must fail loudly, since nothing reloads the tester iframe.
expect(getStorybookOptimizeDeps()).not.toHaveProperty(
"ignoreOutdatedRequests",
);
});
});
describe("clearViteDepsCache", () => {
let tempRoot: string;

View file

@ -9,6 +9,7 @@ import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
import { playwright } from "@vitest/browser-playwright";
import { honoMiddlewarePlugin } from "./server/vite-hono-plugin";
import {
getStorybookOptimizeDeps,
getViteBaseConfig,
getViteDevOptimizeDeps,
} from "./server/vite-base-config";
@ -391,6 +392,10 @@ export default defineConfig(({ command }) => {
configDir: path.join(dirname, ".storybook"),
}),
],
// Re-bundled on every run rather than read from the cache — the
// stale-cache failure it prevents, and why `force` is the lever, are
// on the helper (#2340).
optimizeDeps: getStorybookOptimizeDeps(),
test: {
name: "storybook",
// Vitest's default is 5000ms, which is the whole budget a play