diff --git a/AGENTS.md b/AGENTS.md index f835e16ef..1ba109fd0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -214,12 +214,12 @@ gh project item-edit --project-id PVT_kwDOCt2Azc4BJVxt --id "$ITEM_ID" --field-i - after making the changes, respond to each review comment with what was done (or why it was ignored) ### Mandatory pre-push gate -- ALWAYS do `npm run format` before committing — the **root** `format` auto-fixes `core/` (`format:core`) and every client's scope in one shot. `validate` runs `format:check` (the non-fixing variant, including `format:check:core`) and will fail in CI on any unformatted file, so always run the auto-fixer first rather than letting `format:check` catch it. +- ALWAYS do `npm run format` before committing — the **root** `format` auto-fixes `core/` (`format:core`), the root `scripts/**/*.{mjs,js,cjs,ts}` (`format:scripts`), and every client's scope in one shot. `validate` runs `format:check` (the non-fixing variant, including `format:check:core` and `format:check:scripts`) and will fail in CI on any unformatted file, so always run the auto-fixer first rather than letting `format:check` catch it. - **`npm run ci` is the mandatory pre-push command** — it mirrors `.github/workflows/main.yml` (minus `npm install`): `validate` → `coverage` → `verify:build-gate` (the #1769 browser-externalized-builtin build gate) → `smoke` → Storybook play-function tests (installs Playwright chromium if needed). It now runs **`npm run coverage`**, the per-file ≥90 gate (lines/statements/functions/branches) that CI enforces — so `npm run ci` is a true superset of GitHub CI, and passing it locally means CI's gates will pass. Expect several minutes. **`npm run validate`** remains the fast inner-loop check during development (unit tests only — no coverage gate, no smoke, no Storybook), but it is **NOT** an acceptable substitute for `npm run ci` before pushing: `validate` runs `test`, not `test:coverage`, so it does **zero** coverage gating. Skipping the gate is how a push passes every fast local check and still fails CI (this exact gap broke PR #1601 on a function-coverage regression). - ALWAYS do `npm run format` before committing, then **`npm run ci`** before pushing. From the repo root, `validate` runs the **`core/` gate first** (`validate:core`) and then chains the four per-client validations (`validate:web` → `validate:cli` → `validate:tui` → `validate:launcher`); each client delegates to its own `npm run validate` in its own folder (no coverage — fast). Every client is self-validating and the top level just chains them, building each client's bundle along the way (no cross-client build dependencies). - - **`validate:core` is the shared-code format + lint gate (#1689).** Each client's `prettier`/`eslint` is scoped to its own dir, so nothing reached `core/` before — `validate:core` closes that: it runs `format:check:core` (`prettier --check "core/**/*.{ts,tsx}"`) + `lint:core` (`eslint "core/**/*.{ts,tsx}"` via the **root** `eslint.config.js`). Use `npm run format:core` to auto-fix. The root carries prettier/eslint as devDependencies for this; `core/` is isomorphic (browser + Node globals, no JSX today — the `{ts,tsx}` glob future-proofs against a `core/**/*.tsx`). The root `eslint.config.js` honors an `_`-prefix as the intentionally-unused marker (`argsIgnorePattern`/`varsIgnorePattern`/`caughtErrorsIgnorePattern: '^_'`). + - **`validate:core` is the root-owned format + lint gate (#1689, widened in #1778).** Each client's `prettier`/`eslint` is scoped to its own dir, so nothing reached `core/` or the root `scripts/` before — `validate:core` closes that: it runs `format:check:core` (`prettier --check "core/**/*.{ts,tsx}"`) + `format:check:scripts` (`prettier --check "scripts/**/*.{mjs,js,cjs,ts}"`, the root build/verify tooling — #1778; a brace glob so a future non-`.mjs` script is auto-included) + `lint:core` (`eslint "core/**/*.{ts,tsx}"` via the **root** `eslint.config.js`). Use `npm run format:core` / `npm run format:scripts` to auto-fix (both folded into the root `format`). The `scripts/` gate is prettier-only — the root has no eslint config for `.mjs`. The root carries prettier/eslint as devDependencies for this; `core/` is isomorphic (browser + Node globals, no JSX today — the `{ts,tsx}` glob future-proofs against a `core/**/*.tsx`). The root `eslint.config.js` honors an `_`-prefix as the intentionally-unused marker (`argsIgnorePattern`/`varsIgnorePattern`/`caughtErrorsIgnorePattern: '^_'`). - **cli and tui now typecheck their `src` (#1689).** Their `build`/`test` run through esbuild (no type check), so each has a `typecheck` script (`tsc --noEmit -p tsconfig.json`) folded into `validate`. Their `tsconfig.json` matches `clients/web/tsconfig.app.json`'s module/lib *resolution* options — DOM lib, `moduleResolution: bundler`, and **no** `noUncheckedIndexedAccess` (web's app config does not extend `tsconfig.base`, so re-enabling it would surface `core/` issues web never gates) — so the imported `core/` sources are validated the same way web validates them. It does **not** mirror web's extra strictness flags (`noUnusedLocals`, `verbatimModuleSyntax`, ES2023 target, …), so cli/tui's own `src` is checked slightly more loosely than web's. `core/` itself still typechecks through web's `tsc -b`. - - The one CLI nuance: `clients/cli`'s out-of-process `e2e.test.ts` spawns the built binary, so its `test` **builds first** via `pretest` (`test-servers:build && build`). To avoid building it twice, `clients/cli`'s `validate` folds that in — it is `format:check && lint && typecheck && test` with **no** separate `build` step (the other clients, whose tests don't spawn their bundle, keep an explicit `build`). `validate:web`/`validate:tui`/`validate:launcher` are the uniform `format:check && lint && (typecheck &&) build && test`. + - The one CLI nuance: `clients/cli`'s out-of-process `e2e.test.ts` spawns the built binary, so its `test` **builds first** via `pretest` (`test-servers:build && build`). To avoid building it twice, `clients/cli`'s `validate` folds that in — it is `format:check && lint && typecheck && test` with **no** separate `build` step (the other clients, whose tests don't spawn their bundle, keep an explicit `build`). `validate:web`/`validate:tui`/`validate:launcher` are the uniform `format:check && lint && (typecheck &&) build && test`. (#1778) `clients/web`'s `format`/`format:check` covers `src`, `server`, and its top-level configs (`*.{ts,js}` — `vite.config.ts`, `tsup.runner.config.ts`, `eslint.config.js`, …), not just `src`, so the Node backend and Vite/build config are prettier-gated too. - **`npm run coverage`** is the per-file ≥90 gate and is now part of `npm run ci` — never treat it as optional before a push. It supersedes the old standalone `test:integration` step: web's `test:coverage` runs the `unit` **and** `integration` projects under v8 instrumentation, so `coverage` both enforces the ≥90 gate and exercises the same web integration paths CI covers. - **`smoke` is NOT part of `validate`** — it is included in `npm run ci`. It runs `smoke:launcher` (`--help` dispatch) plus the prod `smoke:cli` / `smoke:tui` / `smoke:web` / `smoke:web:browser`, and contains **no build commands** — it assumes the cli/tui/launcher bundles already exist (a full `validate` builds them; `smoke:web` builds `clients/web/dist` on demand). CI runs `validate`, then the `coverage` gate (which also covers the web integration project), then `verify:build-gate` (the #1769 build gate — see below), then `smoke` (with Playwright chromium installed just before it, since `smoke:web:browser` needs it). GitHub CI runs this same chain as separate workflow steps, with the Storybook play-function tests last (see below). - `smoke:launcher` (`scripts/smoke-launcher.mjs`) runs the built launcher with `--help`, `--cli --help`, and `--tui --help`, asserting each exits 0 and prints that mode's usage banner (which also proves the launcher resolved and loaded the right client build). It's the cheap dispatch check before the heavier prod smokes below. diff --git a/README.md b/README.md index 63fc5cf81..077406f9b 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ Each client self-validates from its own folder; the root scripts chain them. The | `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook. A true superset of GitHub CI. | | `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). | -Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …), plus root `validate:core` / `format:core` for the shared `core/` package. Run `npm run format` before committing — the root `format` fixes `core/` and every client; `validate` runs the non-fixing `format:check` and fails CI on any unformatted file. +Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …), plus root `validate:core` / `format:core` for the shared `core/` package and `format:scripts` for the root `scripts/` tooling. Run `npm run format` before committing — the root `format` fixes `core/`, the root `scripts/`, and every client; `validate` runs the non-fixing `format:check` and fails CI on any unformatted file. For the full testing rules — the ≥90% per-file gate, where test files live, the unit vs. integration vs. storybook projects, and the `v8 ignore` policy — see [`AGENTS.md`](./AGENTS.md). diff --git a/clients/web/eslint.config.js b/clients/web/eslint.config.js index 7c72fff48..86602ed31 100644 --- a/clients/web/eslint.config.js +++ b/clients/web/eslint.config.js @@ -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"], +]); diff --git a/clients/web/package.json b/clients/web/package.json index 684fa2796..3628f6d51 100644 --- a/clients/web/package.json +++ b/clients/web/package.json @@ -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" }, diff --git a/clients/web/tsup.runner.config.ts b/clients/web/tsup.runner.config.ts index d369dfbcf..863c0e296 100644 --- a/clients/web/tsup.runner.config.ts +++ b/clients/web/tsup.runner.config.ts @@ -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"), }; }, }); diff --git a/clients/web/vite.config.ts b/clients/web/vite.config.ts index cf2cb775f..21cc39af6 100644 --- a/clients/web/vite.config.ts +++ b/clients/web/vite.config.ts @@ -1,20 +1,31 @@ /// -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, '../..')], - }, - }, - 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/**'), + // 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, ], - 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, + // 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, "../..")], }, }, - 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