chore(web): format-gate the ungated first-party TS/JS (server/, vite.config.ts, scripts/) (#1788)

* chore(web): format-gate the ungated first-party TS/JS (#1778)

Prettier only covered core/ and each client's src/, leaving the web Node
backend (clients/web/server/**), web top-level configs (vite.config.ts,
tsup.runner.config.ts, eslint.config.js, ...), and the root build/verify
tooling (scripts/**/*.mjs) ungated — so their formatting was never checked
by validate/CI. eslint already reached these via `eslint .`, so this is a
prettier-only gap.

- clients/web format/format:check now cover src, server, and top-level
  *.{ts,js}, not just src.
- New root format:scripts / format:check:scripts for scripts/**/*.mjs,
  folded into the root format and into validate:core.
- Normalized the newly-covered files with prettier --write (pure
  formatting; no runtime change).

Closes #1778

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* chore: future-proof the scripts format glob (#1778 review)

Widen format:scripts / format:check:scripts from "scripts/**/*.mjs" to
"scripts/**/*.{mjs,js,cjs,ts}" so a future non-.mjs file under scripts/
is auto-gated, matching the future-proof glob style clients/web now uses.
A brace glob is a single pattern, so prettier does not error on the
unmatched branches. Addresses observation #2 from the PR #1788 review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* docs: sync AGENTS.md scripts glob after the widening (#1788 review r2)

d86d8f7 widened the scripts glob to {mjs,js,cjs,ts} and updated the
validate:core bullet, but the format bullet at AGENTS.md:217 still said
scripts/**/*.mjs. Bring it in line so the two mentions agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-07-25 23:15:45 -04:00 • committed by GitHub
commit 166ed38e7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 406 additions and 324 deletions

View file

@ -1,30 +1,35 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from "eslint-plugin-storybook";
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([globalIgnores(['dist', 'storybook-static', 'coverage']), {
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
export default defineConfig([
globalIgnores(["dist", "storybook-static", "coverage"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
}, {
// Test setup files re-export utilities and mix components with helpers — the
// react-refresh rule does not apply.
files: ['src/test/**/*.{ts,tsx}', 'src/**/*.test.{ts,tsx}'],
rules: {
'react-refresh/only-export-components': 'off',
{
// Test setup files re-export utilities and mix components with helpers — the
// react-refresh rule does not apply.
files: ["src/test/**/*.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
rules: {
"react-refresh/only-export-components": "off",
},
},
}, ...storybook.configs["flat/recommended"]])
...storybook.configs["flat/recommended"],
]);

View file

@ -26,8 +26,8 @@
"test:integration:watch": "npm run test-servers:build && vitest --project=integration",
"test-servers:build": "tsc -p ../../test-servers --noCheck",
"lint": "eslint .",
"format": "prettier --write src",
"format:check": "prettier --check src",
"format": "prettier --write src server \"*.{ts,js}\"",
"format:check": "prettier --check src server \"*.{ts,js}\"",
"preview": "vite preview",
"storybook": "storybook dev -p 6006"
},

View file

@ -1,38 +1,38 @@
import { defineConfig } from 'tsup';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig } from "tsup";
import path from "node:path";
import { fileURLToPath } from "node:url";
const dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(dirname, '../..');
const repoRoot = path.resolve(dirname, "../..");
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
outDir: 'build',
entry: ["src/index.ts"],
format: ["esm"],
outDir: "build",
clean: true,
// No source maps in the published bundle — they roughly double the on-disk
// size and aren't needed at runtime (debug via `npm run dev` on the source).
sourcemap: false,
target: 'node22',
platform: 'node',
target: "node22",
platform: "node",
noExternal: [/^@inspector\/core/],
external: [
'commander',
'open',
'pino',
'hono',
'@hono/node-server',
'vite',
'@vitejs/plugin-react',
'atomically',
'chokidar',
'@napi-rs/keyring',
'@modelcontextprotocol/client',
'@modelcontextprotocol/core',
"commander",
"open",
"pino",
"hono",
"@hono/node-server",
"vite",
"@vitejs/plugin-react",
"atomically",
"chokidar",
"@napi-rs/keyring",
"@modelcontextprotocol/client",
"@modelcontextprotocol/core",
],
esbuildOptions(options) {
options.alias = {
'@inspector/core': path.join(repoRoot, 'core'),
"@inspector/core": path.join(repoRoot, "core"),
};
},
});

View file

@ -1,20 +1,31 @@
/// <reference types="vitest/config" />
import { defineConfig, type Plugin } from 'vite';
import react from '@vitejs/plugin-react';
import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
// https://vite.dev/config/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { playwright } from '@vitest/browser-playwright';
import { honoMiddlewarePlugin } from './server/vite-hono-plugin';
import { getViteBaseConfig, getViteDevOptimizeDeps } from './server/vite-base-config';
import { buildWebServerConfigFromEnv } from './server/web-server-config';
import { createBrowserExternalizedBuiltinGate } from './server/browser-externalized-builtin-gate';
import { vitestSharedPaths } from '../../vitest.shared.mts';
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
const { repoRoot, sharedDedupe, nodeModulesAliases, projectResolve, sharedAliases } =
vitestSharedPaths(dirname);
import path from "node:path";
import { fileURLToPath } from "node:url";
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
import { playwright } from "@vitest/browser-playwright";
import { honoMiddlewarePlugin } from "./server/vite-hono-plugin";
import {
getViteBaseConfig,
getViteDevOptimizeDeps,
} from "./server/vite-base-config";
import { buildWebServerConfigFromEnv } from "./server/web-server-config";
import { createBrowserExternalizedBuiltinGate } from "./server/browser-externalized-builtin-gate";
import { vitestSharedPaths } from "../../vitest.shared.mts";
const dirname =
typeof __dirname !== "undefined"
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
const {
repoRoot,
sharedDedupe,
nodeModulesAliases,
projectResolve,
sharedAliases,
} = vitestSharedPaths(dirname);
// Integration tests live under clients/web/src/test/integration/ and run in
// the node-env vitest project below. The folder is the manifest: anything
@ -26,7 +37,7 @@ const { repoRoot, sharedDedupe, nodeModulesAliases, projectResolve, sharedAliase
// Match `{ts,tsx}` to mirror the unit project's include below — otherwise a
// stray `.test.tsx` placed inside this folder would slip past the integration
// include AND fail to be excluded from unit, silently landing under happy-dom.
const integrationGlob = 'clients/web/src/test/integration/**/*.test.{ts,tsx}';
const integrationGlob = "clients/web/src/test/integration/**/*.test.{ts,tsx}";
// Fail `vite build` when a Node built-in lands in the browser bundle (#1769).
// Vite 8 otherwise only *warns* and ships a `module.exports = {}` stub, so a
@ -55,9 +66,9 @@ const integrationGlob = 'clients/web/src/test/integration/**/*.test.{ts,tsx}';
function browserExternalizedBuiltinGate(): Plugin {
const gate = createBrowserExternalizedBuiltinGate();
return {
name: 'inspector:fail-on-browser-externalized-builtin',
apply: 'build',
applyToEnvironment: (environment) => environment.name === 'client',
name: "inspector:fail-on-browser-externalized-builtin",
apply: "build",
applyToEnvironment: (environment) => environment.name === "client",
// `enforce: 'pre'` runs this ahead of the normal-plugin group (and Vite's
// core plugins) in the `onLog` chain: a plugin whose `onLog` returns `false`
// filters that log for every later plugin, so trailing the array would let a
@ -66,7 +77,7 @@ function browserExternalizedBuiltinGate(): Plugin {
// position. Harmless to the emitting `rolldown:vite-resolve` plugin — the gate
// defines no resolve/load/transform, and `onLog` delivery doesn't depend on
// the emitter's position.
enforce: 'pre',
enforce: "pre",
buildStart() {
gate.reset();
},
@ -83,248 +94,248 @@ function browserExternalizedBuiltinGate(): Plugin {
export default defineConfig(({ command }) => {
const isDevServer = command === "serve" && !process.env.VITEST;
return {
// `honoMiddlewarePlugin` is gated by `apply: 'serve'` so it only attaches
// during `vite dev` / `vite preview` — vitest projects share this config
// but never invoke `configureServer`, so the plugin stays inert there.
//
// The plugin statically imports the node-only dev backend
// (`core/mcp/remote/node/server.ts`), so Vite's config bundler (Rolldown)
// walks that chain when it loads this file and reaches node-only deps
// (`chokidar`, `atomically`, `@napi-rs/keyring`). Those resolve cleanly at
// config-bundle time because they're declared in the repo-root
// `package.json` and installed into the repo-root `node_modules`, which sits
// on `core/`'s module-resolution chain (core/ has no node_modules of its
// own). Keep them in the root manifest: drop one and Rolldown can no longer
// resolve it from core/, reviving the benign `UNRESOLVED_IMPORT` warnings
// that #1491 eliminated at the source (by removing the old stream filter and
// build-time onwarn suppressions rather than re-hiding the symptom).
// `browserExternalizedBuiltinGate` fails `vite build` if a Node built-in
// reaches the browser bundle (#1769) — see its definition above.
plugins: [
react(),
honoMiddlewarePlugin(buildWebServerConfigFromEnv()),
browserExternalizedBuiltinGate(),
],
// Shared optimizeDeps exclusions so node-only packages
// (`@modelcontextprotocol/client/stdio`, `cross-spawn`, `which`)
// consumed by the dev backend aren't scanned for browser pre-bundling.
// Browser code reaches the node-side stack via the Hono plugin only.
// Dev server: force a full dep pre-bundle each launch (no stale cache).
optimizeDeps: isDevServer
? getViteDevOptimizeDeps()
: getViteBaseConfig().optimizeDeps,
resolve: {
// NOTE: the unit vitest project (below) overrides this — see comment there.
// `honoMiddlewarePlugin` is gated by `apply: 'serve'` so it only attaches
// during `vite dev` / `vite preview` — vitest projects share this config
// but never invoke `configureServer`, so the plugin stays inert there.
//
// Once App.tsx started consuming the full hook + state-manager surface
// (#1244), the browser dep graph reached bare-module subpaths in core/
// that Rolldown couldn't resolve against `core/`'s parent (it has no
// node_modules of its own). Promote the same bare-module aliases the
// vitest projects use so `vite dev` / `vite build` can resolve them
// from `clients/web/node_modules`.
alias: [
...Object.entries(sharedAliases).map(([find, replacement]) => ({
find,
replacement,
})),
...nodeModulesAliases,
// The plugin statically imports the node-only dev backend
// (`core/mcp/remote/node/server.ts`), so Vite's config bundler (Rolldown)
// walks that chain when it loads this file and reaches node-only deps
// (`chokidar`, `atomically`, `@napi-rs/keyring`). Those resolve cleanly at
// config-bundle time because they're declared in the repo-root
// `package.json` and installed into the repo-root `node_modules`, which sits
// on `core/`'s module-resolution chain (core/ has no node_modules of its
// own). Keep them in the root manifest: drop one and Rolldown can no longer
// resolve it from core/, reviving the benign `UNRESOLVED_IMPORT` warnings
// that #1491 eliminated at the source (by removing the old stream filter and
// build-time onwarn suppressions rather than re-hiding the symptom).
// `browserExternalizedBuiltinGate` fails `vite build` if a Node built-in
// reaches the browser bundle (#1769) — see its definition above.
plugins: [
react(),
honoMiddlewarePlugin(buildWebServerConfigFromEnv()),
browserExternalizedBuiltinGate(),
],
// Source files in core/ import bare modules (react, @testing-library/react,
// etc.) that only exist in clients/web/node_modules. Dedupe ensures Vite
// resolves them from this package rather than walking up from core/'s
// location (which has no node_modules of its own yet).
dedupe: sharedDedupe,
},
// Pin the Vite dev server to the same port (and host) the Hono plugin
// configures from env, so `allowedOrigins` actually matches the browser
// origin. Without this, `vite dev` falls back to Vite's default 5173
// while the dev backend's `buildWebServerConfigFromEnv()` defaults to
// CLIENT_PORT=6274 — origin check rejects every `/api/*` request from
// the browser. CLIENT_PORT / HOST overrides flow through here too.
// `strictPort: true` so a port collision fails loudly instead of
// silently picking a different port (which would leave `allowedOrigins`
// pointing at the wrong host and break browser fetches).
server: {
port: parseInt(process.env.CLIENT_PORT ?? '6274', 10),
host: process.env.HOST ?? 'localhost',
strictPort: true,
fs: {
allow: [path.resolve(dirname, '../..')],
// Shared optimizeDeps exclusions so node-only packages
// (`@modelcontextprotocol/client/stdio`, `cross-spawn`, `which`)
// consumed by the dev backend aren't scanned for browser pre-bundling.
// Browser code reaches the node-side stack via the Hono plugin only.
// Dev server: force a full dep pre-bundle each launch (no stale cache).
optimizeDeps: isDevServer
? getViteDevOptimizeDeps()
: getViteBaseConfig().optimizeDeps,
resolve: {
// NOTE: the unit vitest project (below) overrides this — see comment there.
//
// Once App.tsx started consuming the full hook + state-manager surface
// (#1244), the browser dep graph reached bare-module subpaths in core/
// that Rolldown couldn't resolve against `core/`'s parent (it has no
// node_modules of its own). Promote the same bare-module aliases the
// vitest projects use so `vite dev` / `vite build` can resolve them
// from `clients/web/node_modules`.
alias: [
...Object.entries(sharedAliases).map(([find, replacement]) => ({
find,
replacement,
})),
...nodeModulesAliases,
],
// Source files in core/ import bare modules (react, @testing-library/react,
// etc.) that only exist in clients/web/node_modules. Dedupe ensures Vite
// resolves them from this package rather than walking up from core/'s
// location (which has no node_modules of its own yet).
dedupe: sharedDedupe,
},
},
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json-summary'],
include: [
'src/components/**/*.{ts,tsx}',
'src/lib/**/*.{ts,tsx}',
'src/utils/**/*.{ts,tsx}',
'clients/web/server/**/*.{ts,tsx}',
path.join(repoRoot, 'core/mcp/**/*.{ts,tsx}'),
path.join(repoRoot, 'core/json/**/*.{ts,tsx}'),
path.join(repoRoot, 'core/client/**/*.{ts,tsx}'),
path.join(repoRoot, 'core/react/**/*.{ts,tsx}'),
path.join(repoRoot, 'core/auth/**/*.{ts,tsx}'),
path.join(repoRoot, 'core/storage/**/*.{ts,tsx}'),
path.join(repoRoot, 'core/logging/**/*.{ts,tsx}'),
path.join(repoRoot, 'core/node/**/*.{ts,tsx}'),
],
exclude: [
'**/*.stories.{ts,tsx}',
'**/*.test.{ts,tsx}',
'**/*.fixtures.{ts,tsx}',
'**/index.{ts,tsx}',
'src/components/**/types.ts',
// Dev-backend runtime glue: each file is exercised end-to-end via
// `npm run dev` (Hono plugin attaches, banner prints, /api/* serves).
// `vite-hono-plugin.ts` requires standing up a real Vite server with
// an HTTP listener to drive `configureServer`; `server.ts` is the
// production Hono entry that the v2 launcher (not yet ported, #1246)
// will invoke; `start-vite-dev-server.ts` is its dev counterpart.
// The non-glue parts are extracted to `web-server-config.ts` (fully
// tested) and `sandbox-controller.ts` (HTTP behavior tested).
'clients/web/server/vite-hono-plugin.ts',
'clients/web/server/server.ts',
'clients/web/server/start-vite-dev-server.ts',
// Pure-type modules: `interface`/`type` declarations only, no runtime
// statements. Excluding them keeps the report clean (would otherwise
// surface as misleading 0/0 rows).
path.join(repoRoot, 'core/mcp/types.ts'),
path.join(repoRoot, 'core/mcp/elicitationCreateMessage.ts'),
path.join(repoRoot, 'core/mcp/samplingCreateMessage.ts'),
path.join(repoRoot, 'core/mcp/sessionStorage.ts'),
path.join(repoRoot, 'core/mcp/inspectorClientProtocol.ts'),
path.join(repoRoot, 'core/mcp/remote/types.ts'),
path.join(repoRoot, 'core/mcp/import/types.ts'),
'clients/web/server/types.ts',
// .d.ts files are declaration-only.
path.join(repoRoot, '**/*.d.ts'),
// inspectorClientEventTarget.ts is types + a single empty-body class
// (extends TypedEventTarget). v8/istanbul records 0 statements for it
// today. TODO(#1243): drop this exclusion once the class gains real
// behavior as the v1.5 InspectorClient port progresses.
path.join(repoRoot, 'core/mcp/inspectorClientEventTarget.ts'),
path.join(repoRoot, 'core/mcp/__tests__/**'),
// test-servers/ is test infrastructure (composable MCP servers and
// fixtures), not application code — its build output also lives at
// test-servers/build/, which we don't want to measure either.
path.join(repoRoot, 'test-servers/**'),
],
thresholds: {
perFile: true,
// Uniform 90 per-file gate across every dimension. The branch floor was
// lifted 50 → 70 (#1271) and then the whole gate raised to 90 after a
// codebase-wide coverage audit added real tests for every outlier.
// Genuinely-unreachable branches (Mantine portal / `useMediaQuery` /
// `typeof window` SSR guards, React StrictMode effect replay, and
// provably-dead defensive guards) are annotated with justified
// `/* v8 ignore … */` comments at the source rather than relaxing the
// gate. New code must clear 90 on all four dimensions.
lines: 90,
statements: 90,
functions: 90,
branches: 90,
// Pin the Vite dev server to the same port (and host) the Hono plugin
// configures from env, so `allowedOrigins` actually matches the browser
// origin. Without this, `vite dev` falls back to Vite's default 5173
// while the dev backend's `buildWebServerConfigFromEnv()` defaults to
// CLIENT_PORT=6274 — origin check rejects every `/api/*` request from
// the browser. CLIENT_PORT / HOST overrides flow through here too.
// `strictPort: true` so a port collision fails loudly instead of
// silently picking a different port (which would leave `allowedOrigins`
// pointing at the wrong host and break browser fetches).
server: {
port: parseInt(process.env.CLIENT_PORT ?? "6274", 10),
host: process.env.HOST ?? "localhost",
strictPort: true,
fs: {
allow: [path.resolve(dirname, "../..")],
},
},
projects: [
{
extends: true,
// Vitest projects don't inherit `resolve` from the parent. The unit
// project runs from repoRoot (so vitest's coverage transformer can
// reach core/), but repoRoot has no node_modules of its own — the
// shared regex aliases redirect bare `react`/`pino`/etc. imports
// from core/ back into clients/web/node_modules.
resolve: projectResolve,
test: {
name: 'unit',
environment: 'happy-dom',
// Don't let happy-dom actually navigate child frames. Components like
// the MCP Apps sandbox render an <iframe src="/sandbox.html">; with
// navigation enabled happy-dom fetches that URL (and unloads it on
// teardown), which fails under the test server and floods the run with
// alarming-but-expected `DOMException [NetworkError/AbortError]` and
// `AsyncTaskManager destroyed` output. The component tests only assert
// on the iframe element/attributes, not its loaded document, so
// disabling frame navigation removes the noise without losing coverage.
environmentOptions: {
happyDOM: {
settings: { navigation: { disableChildFrameNavigation: true } },
},
},
// Root the unit project at the repo root so vitest's coverage
// transformer (which only processes files inside a project root)
// can reach core/ modules. Without this, untested core/ files fall
// back to raw-TS parsing in rolldown, which can't handle TS-only
// syntax (e.g. `import type`) — silently dropping them and bypassing
// the per-file gate.
root: repoRoot,
// No `globals: true` — every test file imports `describe`, `it`,
// `expect`, `vi` explicitly from "vitest". This keeps the pattern
// consistent and avoids relying on auto-cleanup tied to Vitest's
// global lifecycle hooks; cleanup is invoked manually in setup.ts.
include: ['clients/web/src/**/*.test.{ts,tsx}'],
// Integration tests run in the integration project below (node env).
exclude: [integrationGlob],
setupFiles: [path.join(dirname, 'src/test/setup.ts')],
},
},
{
extends: true,
// See note on the unit project: integration tests also run from
// repoRoot and import core/ modules, so they need the same alias
// setup. The shared bare-module aliases keep `pino`, `hono`, etc.
// resolving against clients/web/node_modules.
resolve: projectResolve,
test: {
name: 'integration',
environment: 'node',
// Same reason as the unit project: rooted at repoRoot so vitest
// can transform core/ modules and run tests against the source.
root: repoRoot,
include: [integrationGlob],
// Integration tests spawn real HTTP/stdio servers via test-servers/,
// bind sockets, run e2e OAuth flows, and exercise filesystem-backed
// storage. 30s matches the v1.5 core/vitest.config.ts.
testTimeout: 30000,
hookTimeout: 30000,
// Inline the MCP SDK so vi.mock("@modelcontextprotocol/client")
// hooks the same transformed copy that source files import.
// Externalized node_modules are loaded via Node's loader and bypass
// Vitest's mock system. Inlining ensures the transformer pipeline
// owns the module rather than the Node loader.
server: {
deps: {
inline: [/@modelcontextprotocol\/(client|core)/],
},
},
},
},
{
extends: true,
plugins: [
// The plugin will run tests for the stories defined in your Storybook config
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
storybookTest({
configDir: path.join(dirname, '.storybook'),
}),
test: {
coverage: {
provider: "v8",
reporter: ["text", "html", "json-summary"],
include: [
"src/components/**/*.{ts,tsx}",
"src/lib/**/*.{ts,tsx}",
"src/utils/**/*.{ts,tsx}",
"clients/web/server/**/*.{ts,tsx}",
path.join(repoRoot, "core/mcp/**/*.{ts,tsx}"),
path.join(repoRoot, "core/json/**/*.{ts,tsx}"),
path.join(repoRoot, "core/client/**/*.{ts,tsx}"),
path.join(repoRoot, "core/react/**/*.{ts,tsx}"),
path.join(repoRoot, "core/auth/**/*.{ts,tsx}"),
path.join(repoRoot, "core/storage/**/*.{ts,tsx}"),
path.join(repoRoot, "core/logging/**/*.{ts,tsx}"),
path.join(repoRoot, "core/node/**/*.{ts,tsx}"),
],
test: {
name: 'storybook',
browser: {
enabled: true,
headless: true,
provider: playwright({}),
instances: [
{
browser: 'chromium',
},
],
},
setupFiles: ['.storybook/vitest.setup.ts'],
exclude: [
"**/*.stories.{ts,tsx}",
"**/*.test.{ts,tsx}",
"**/*.fixtures.{ts,tsx}",
"**/index.{ts,tsx}",
"src/components/**/types.ts",
// Dev-backend runtime glue: each file is exercised end-to-end via
// `npm run dev` (Hono plugin attaches, banner prints, /api/* serves).
// `vite-hono-plugin.ts` requires standing up a real Vite server with
// an HTTP listener to drive `configureServer`; `server.ts` is the
// production Hono entry that the v2 launcher (not yet ported, #1246)
// will invoke; `start-vite-dev-server.ts` is its dev counterpart.
// The non-glue parts are extracted to `web-server-config.ts` (fully
// tested) and `sandbox-controller.ts` (HTTP behavior tested).
"clients/web/server/vite-hono-plugin.ts",
"clients/web/server/server.ts",
"clients/web/server/start-vite-dev-server.ts",
// Pure-type modules: `interface`/`type` declarations only, no runtime
// statements. Excluding them keeps the report clean (would otherwise
// surface as misleading 0/0 rows).
path.join(repoRoot, "core/mcp/types.ts"),
path.join(repoRoot, "core/mcp/elicitationCreateMessage.ts"),
path.join(repoRoot, "core/mcp/samplingCreateMessage.ts"),
path.join(repoRoot, "core/mcp/sessionStorage.ts"),
path.join(repoRoot, "core/mcp/inspectorClientProtocol.ts"),
path.join(repoRoot, "core/mcp/remote/types.ts"),
path.join(repoRoot, "core/mcp/import/types.ts"),
"clients/web/server/types.ts",
// .d.ts files are declaration-only.
path.join(repoRoot, "**/*.d.ts"),
// inspectorClientEventTarget.ts is types + a single empty-body class
// (extends TypedEventTarget). v8/istanbul records 0 statements for it
// today. TODO(#1243): drop this exclusion once the class gains real
// behavior as the v1.5 InspectorClient port progresses.
path.join(repoRoot, "core/mcp/inspectorClientEventTarget.ts"),
path.join(repoRoot, "core/mcp/__tests__/**"),
// test-servers/ is test infrastructure (composable MCP servers and
// fixtures), not application code — its build output also lives at
// test-servers/build/, which we don't want to measure either.
path.join(repoRoot, "test-servers/**"),
],
thresholds: {
perFile: true,
// Uniform 90 per-file gate across every dimension. The branch floor was
// lifted 50 → 70 (#1271) and then the whole gate raised to 90 after a
// codebase-wide coverage audit added real tests for every outlier.
// Genuinely-unreachable branches (Mantine portal / `useMediaQuery` /
// `typeof window` SSR guards, React StrictMode effect replay, and
// provably-dead defensive guards) are annotated with justified
// `/* v8 ignore … */` comments at the source rather than relaxing the
// gate. New code must clear 90 on all four dimensions.
lines: 90,
statements: 90,
functions: 90,
branches: 90,
},
},
],
},
};
projects: [
{
extends: true,
// Vitest projects don't inherit `resolve` from the parent. The unit
// project runs from repoRoot (so vitest's coverage transformer can
// reach core/), but repoRoot has no node_modules of its own — the
// shared regex aliases redirect bare `react`/`pino`/etc. imports
// from core/ back into clients/web/node_modules.
resolve: projectResolve,
test: {
name: "unit",
environment: "happy-dom",
// Don't let happy-dom actually navigate child frames. Components like
// the MCP Apps sandbox render an <iframe src="/sandbox.html">; with
// navigation enabled happy-dom fetches that URL (and unloads it on
// teardown), which fails under the test server and floods the run with
// alarming-but-expected `DOMException [NetworkError/AbortError]` and
// `AsyncTaskManager destroyed` output. The component tests only assert
// on the iframe element/attributes, not its loaded document, so
// disabling frame navigation removes the noise without losing coverage.
environmentOptions: {
happyDOM: {
settings: { navigation: { disableChildFrameNavigation: true } },
},
},
// Root the unit project at the repo root so vitest's coverage
// transformer (which only processes files inside a project root)
// can reach core/ modules. Without this, untested core/ files fall
// back to raw-TS parsing in rolldown, which can't handle TS-only
// syntax (e.g. `import type`) — silently dropping them and bypassing
// the per-file gate.
root: repoRoot,
// No `globals: true` — every test file imports `describe`, `it`,
// `expect`, `vi` explicitly from "vitest". This keeps the pattern
// consistent and avoids relying on auto-cleanup tied to Vitest's
// global lifecycle hooks; cleanup is invoked manually in setup.ts.
include: ["clients/web/src/**/*.test.{ts,tsx}"],
// Integration tests run in the integration project below (node env).
exclude: [integrationGlob],
setupFiles: [path.join(dirname, "src/test/setup.ts")],
},
},
{
extends: true,
// See note on the unit project: integration tests also run from
// repoRoot and import core/ modules, so they need the same alias
// setup. The shared bare-module aliases keep `pino`, `hono`, etc.
// resolving against clients/web/node_modules.
resolve: projectResolve,
test: {
name: "integration",
environment: "node",
// Same reason as the unit project: rooted at repoRoot so vitest
// can transform core/ modules and run tests against the source.
root: repoRoot,
include: [integrationGlob],
// Integration tests spawn real HTTP/stdio servers via test-servers/,
// bind sockets, run e2e OAuth flows, and exercise filesystem-backed
// storage. 30s matches the v1.5 core/vitest.config.ts.
testTimeout: 30000,
hookTimeout: 30000,
// Inline the MCP SDK so vi.mock("@modelcontextprotocol/client")
// hooks the same transformed copy that source files import.
// Externalized node_modules are loaded via Node's loader and bypass
// Vitest's mock system. Inlining ensures the transformer pipeline
// owns the module rather than the Node loader.
server: {
deps: {
inline: [/@modelcontextprotocol\/(client|core)/],
},
},
},
},
{
extends: true,
plugins: [
// The plugin will run tests for the stories defined in your Storybook config
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
storybookTest({
configDir: path.join(dirname, ".storybook"),
}),
],
test: {
name: "storybook",
browser: {
enabled: true,
headless: true,
provider: playwright({}),
instances: [
{
browser: "chromium",
},
],
},
setupFiles: [".storybook/vitest.setup.ts"],
},
},
],
},
};
});

View file

@ -1 +1 @@
/// <reference types="@vitest/browser-playwright" />
/// <reference types="@vitest/browser-playwright" />