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:
parent
a82a70f4f5
commit
166ed38e7f
10 changed files with 406 additions and 324 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
2
clients/web/vitest.shims.d.ts
vendored
2
clients/web/vitest.shims.d.ts
vendored
|
|
@ -1 +1 @@
|
|||
/// <reference types="@vitest/browser-playwright" />
|
||||
/// <reference types="@vitest/browser-playwright" />
|
||||
|
|
|
|||
|
|
@ -41,11 +41,13 @@
|
|||
"ci:storybook": "cd clients/web && npx playwright install chromium && npm run test:storybook",
|
||||
"verify:build-gate": "node scripts/verify-build-gate.mjs",
|
||||
"validate": "npm run validate:core && npm run validate:web && npm run validate:cli && npm run validate:tui && npm run validate:launcher",
|
||||
"validate:core": "npm run format:check:core && npm run lint:core",
|
||||
"validate:core": "npm run format:check:core && npm run format:check:scripts && npm run lint:core",
|
||||
"lint:core": "eslint \"core/**/*.{ts,tsx}\"",
|
||||
"format:core": "prettier --write \"core/**/*.{ts,tsx}\"",
|
||||
"format:check:core": "prettier --check \"core/**/*.{ts,tsx}\"",
|
||||
"format": "npm run format:core && cd clients/web && npm run format && cd ../cli && npm run format && cd ../tui && npm run format && cd ../launcher && npm run format",
|
||||
"format:scripts": "prettier --write \"scripts/**/*.{mjs,js,cjs,ts}\"",
|
||||
"format:check:scripts": "prettier --check \"scripts/**/*.{mjs,js,cjs,ts}\"",
|
||||
"format": "npm run format:core && npm run format:scripts && cd clients/web && npm run format && cd ../cli && npm run format && cd ../tui && npm run format && cd ../launcher && npm run format",
|
||||
"validate:cli": "cd clients/cli && npm run validate",
|
||||
"validate:tui": "cd clients/tui && npm run validate",
|
||||
"validate:web": "cd clients/web && npm run validate",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,12 @@ import { join, resolve } from "node:path";
|
|||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
const repoRoot = resolve(import.meta.dirname, "..");
|
||||
const testServer = join(repoRoot, "test-servers", "build", "test-server-stdio.js");
|
||||
const testServer = join(
|
||||
repoRoot,
|
||||
"test-servers",
|
||||
"build",
|
||||
"test-server-stdio.js",
|
||||
);
|
||||
|
||||
// Mirrors INSPECTOR_API_TOKEN_GLOBAL in core/mcp/remote/constants.ts; kept as a
|
||||
// literal because this plain .mjs script can't import the TS source.
|
||||
|
|
@ -72,7 +77,9 @@ function fail(message) {
|
|||
if (workDir) {
|
||||
rmSync(workDir, { recursive: true, force: true });
|
||||
// `tarball` is initialized before `workDir` is ever set, so this is safe.
|
||||
console.error(`pack:verify — tarball retained for inspection at ${tarball}`);
|
||||
console.error(
|
||||
`pack:verify — tarball retained for inspection at ${tarball}`,
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -97,7 +104,9 @@ function ensureTestServer() {
|
|||
step("building test-servers (missing build output)...");
|
||||
const status = runInherit("npx", ["tsc", "-p", "test-servers", "--noCheck"]);
|
||||
if (status !== 0 || !existsSync(testServer)) {
|
||||
fail("could not build the stdio test server (test-servers/build/test-server-stdio.js)");
|
||||
fail(
|
||||
"could not build the stdio test server (test-servers/build/test-server-stdio.js)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -175,10 +184,16 @@ const work = mkdtempSync(join(tmpdir(), "pack-verify-"));
|
|||
workDir = work; // from now on, fail() cleans this up
|
||||
try {
|
||||
ensureTestServer();
|
||||
step(`installing the tarball into a clean consumer at ${work} (pulls runtime deps)...`);
|
||||
step(
|
||||
`installing the tarball into a clean consumer at ${work} (pulls runtime deps)...`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(work, "package.json"),
|
||||
JSON.stringify({ name: "pack-verify-consumer", private: true, version: "0.0.0" }),
|
||||
JSON.stringify({
|
||||
name: "pack-verify-consumer",
|
||||
private: true,
|
||||
version: "0.0.0",
|
||||
}),
|
||||
);
|
||||
const install = runInherit(
|
||||
"npm",
|
||||
|
|
@ -189,8 +204,18 @@ try {
|
|||
fail("`npm install <tarball>` into the throwaway consumer failed");
|
||||
}
|
||||
|
||||
const installedPkg = join(work, "node_modules", "@modelcontextprotocol", "inspector");
|
||||
const bin = join(work, "node_modules", ".bin", process.platform === "win32" ? "mcp-inspector.cmd" : "mcp-inspector");
|
||||
const installedPkg = join(
|
||||
work,
|
||||
"node_modules",
|
||||
"@modelcontextprotocol",
|
||||
"inspector",
|
||||
);
|
||||
const bin = join(
|
||||
work,
|
||||
"node_modules",
|
||||
".bin",
|
||||
process.platform === "win32" ? "mcp-inspector.cmd" : "mcp-inspector",
|
||||
);
|
||||
if (!existsSync(bin)) {
|
||||
fail(`installed \`mcp-inspector\` bin not found at ${bin}`);
|
||||
}
|
||||
|
|
@ -213,7 +238,11 @@ try {
|
|||
env: { ...process.env, ...extraEnv },
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
return { status: r.status, output: `${r.stdout ?? ""}${r.stderr ?? ""}`, stdout: r.stdout ?? "" };
|
||||
return {
|
||||
status: r.status,
|
||||
output: `${r.stdout ?? ""}${r.stderr ?? ""}`,
|
||||
stdout: r.stdout ?? "",
|
||||
};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -223,17 +252,33 @@ try {
|
|||
// build from its installed location.
|
||||
step("verifying help dispatch (--help, --cli --help, --tui --help)...");
|
||||
const helpChecks = [
|
||||
{ args: ["--help"], marker: "Mode flags (--web, --cli, --tui)", label: "launcher --help" },
|
||||
{ args: ["--cli", "--help"], marker: "Usage: inspector-cli", label: "--cli dispatch" },
|
||||
{ args: ["--tui", "--help"], marker: "Usage: mcp-inspector-tui", label: "--tui dispatch" },
|
||||
{
|
||||
args: ["--help"],
|
||||
marker: "Mode flags (--web, --cli, --tui)",
|
||||
label: "launcher --help",
|
||||
},
|
||||
{
|
||||
args: ["--cli", "--help"],
|
||||
marker: "Usage: inspector-cli",
|
||||
label: "--cli dispatch",
|
||||
},
|
||||
{
|
||||
args: ["--tui", "--help"],
|
||||
marker: "Usage: mcp-inspector-tui",
|
||||
label: "--tui dispatch",
|
||||
},
|
||||
];
|
||||
for (const { args, marker, label } of helpChecks) {
|
||||
const r = runBin(args);
|
||||
if (r.status !== 0) {
|
||||
fail(`\`${args.join(" ")}\` exited ${r.status}\n${r.output.slice(0, 800)}`);
|
||||
fail(
|
||||
`\`${args.join(" ")}\` exited ${r.status}\n${r.output.slice(0, 800)}`,
|
||||
);
|
||||
}
|
||||
if (!r.output.includes(marker)) {
|
||||
fail(`\`${args.join(" ")}\` (${label}) did not print "${marker}"\n${r.output.slice(0, 800)}`);
|
||||
fail(
|
||||
`\`${args.join(" ")}\` (${label}) did not print "${marker}"\n${r.output.slice(0, 800)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -250,7 +295,15 @@ try {
|
|||
},
|
||||
}),
|
||||
);
|
||||
const list = runBin(["--cli", "--catalog", catalogPath, "--server", "test", "--method", "tools/list"]);
|
||||
const list = runBin([
|
||||
"--cli",
|
||||
"--catalog",
|
||||
catalogPath,
|
||||
"--server",
|
||||
"test",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
if (list.status !== 0) {
|
||||
fail(`\`--cli … tools/list\` exited ${list.status}\n${list.output}`);
|
||||
}
|
||||
|
|
@ -258,7 +311,9 @@ try {
|
|||
try {
|
||||
parsed = JSON.parse(list.stdout);
|
||||
} catch {
|
||||
fail(`\`--cli … tools/list\` did not return JSON on stdout:\n${list.stdout}`);
|
||||
fail(
|
||||
`\`--cli … tools/list\` did not return JSON on stdout:\n${list.stdout}`,
|
||||
);
|
||||
}
|
||||
if (!(parsed.tools ?? []).some((t) => t.name === "echo")) {
|
||||
fail(`\`--cli … tools/list\` missing expected "echo" tool`);
|
||||
|
|
@ -291,7 +346,9 @@ try {
|
|||
* shipped `dist`. Kills the server before returning.
|
||||
*/
|
||||
async function verifyWeb(bin, cwd) {
|
||||
step("verifying prod `--web` serves the shipped dist from the installed package...");
|
||||
step(
|
||||
"verifying prod `--web` serves the shipped dist from the installed package...",
|
||||
);
|
||||
const host = "127.0.0.1";
|
||||
const port = process.env.PACK_VERIFY_WEB_PORT ?? "6399";
|
||||
const token = "pack-verify-token";
|
||||
|
|
@ -326,7 +383,9 @@ async function verifyWeb(bin, cwd) {
|
|||
let res = null;
|
||||
for (let attempt = 0; attempt < 120 && !res; attempt++) {
|
||||
if (exited) {
|
||||
fail(`\`--web\` exited (code ${exitCode}) before serving — see output above`);
|
||||
fail(
|
||||
`\`--web\` exited (code ${exitCode}) before serving — see output above`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
res = await fetch(`http://${host}:${port}/`);
|
||||
|
|
@ -342,7 +401,9 @@ async function verifyWeb(bin, cwd) {
|
|||
}
|
||||
const body = await res.text();
|
||||
if (!body.includes(TOKEN_GLOBAL)) {
|
||||
fail(`\`--web\` served HTML is missing the ${TOKEN_GLOBAL} global (token not injected)`);
|
||||
fail(
|
||||
`\`--web\` served HTML is missing the ${TOKEN_GLOBAL} global (token not injected)`,
|
||||
);
|
||||
}
|
||||
if (!body.includes(token)) {
|
||||
fail("`--web` served HTML is missing the injected auth-token value");
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ import { tmpdir } from "node:os";
|
|||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repoRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"..",
|
||||
);
|
||||
const webDir = path.join(repoRoot, "clients/web");
|
||||
// Hardcoded browser entry. If it's ever renamed, the guarded read below fails
|
||||
// with an actionable message rather than a raw ENOENT stack.
|
||||
|
|
|
|||
Loading…
Reference in a new issue