* feat(web): build dist on demand for prod --web + end-to-end CI smoke (#1486) Prod `mcp-inspector --web` serves static assets from clients/web/dist, which only exists after a build. In a fresh checkout it was absent and the prod path served a broken page (ENOENT on index.html); the launcher smoke test only covered --help, so this path was unverified in CI. - ensure-web-build.ts: build dist on demand (vite build via build:client) when missing, or exit with an actionable error if the build can't run. - run-web.ts: invoke ensureWebBuild before startHonoServer in the prod path (--dev runs Vite directly and skips it). - scripts/smoke-web.mjs + smoke:web: start --web against built dist and assert GET / returns 200 with the injected __INSPECTOR_API_TOKEN__. Wired into validate:launcher and a new CI step. - Docs: launcher README + AGENTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): surface spawn error in build-on-demand; clarify smoke comment Address PR #1497 review: - runViteBuild now logs result.error.message (e.g. ENOENT when npm isn't on PATH) before returning a null status, so the spawn-failure cause surfaces instead of only the generic actionable error. The build seam gains a `log` param; added a test covering the ENOENT path. - Reword scripts/smoke-web.mjs header so it no longer implies the readiness wait covers a cold build-on-demand vite build (it's sized for a pre-built dist, which validate/CI guarantee). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
/**
|
|
* Ensure the production web assets exist before the Hono server tries to serve
|
|
* them (#1486).
|
|
*
|
|
* `mcp-inspector --web` (prod, no `--dev`) serves static files from
|
|
* `clients/web/dist/`, which only exists after a build. In the published package
|
|
* `dist/` always ships, so this is a no-op there. In a fresh dev checkout the
|
|
* directory is absent and the prod path would otherwise serve a broken page
|
|
* (`ENOENT` on `index.html`). When the build is missing we build it once, on
|
|
* demand; if that build can't run (e.g. dev dependencies are absent, as in a
|
|
* stripped published install) we throw an actionable error instead of letting
|
|
* the server come up and serve a broken page.
|
|
*/
|
|
|
|
import { existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
/** Seams for testing — defaults hit the real filesystem / child process. */
|
|
export interface EnsureWebBuildDeps {
|
|
exists?: (path: string) => boolean;
|
|
/** Build the web assets in `webRoot`; returns the child exit code (null if it never ran). */
|
|
build?: (webRoot: string, log: (message: string) => void) => number | null;
|
|
log?: (message: string) => void;
|
|
}
|
|
|
|
function runViteBuild(
|
|
webRoot: string,
|
|
log: (message: string) => void,
|
|
): number | null {
|
|
const result = spawnSync("npm", ["run", "build:client"], {
|
|
cwd: webRoot,
|
|
stdio: "inherit",
|
|
// npm resolves to npm.cmd on Windows, which needs a shell to be spawnable.
|
|
shell: process.platform === "win32",
|
|
});
|
|
// A spawn failure (e.g. `npm` not on PATH → ENOENT) leaves `status` null and
|
|
// the child produces no output of its own, so surface the cause before the
|
|
// caller falls back to the generic actionable error.
|
|
if (result.error) {
|
|
log(`Web build failed to start: ${result.error.message}`);
|
|
}
|
|
return result.status;
|
|
}
|
|
|
|
/**
|
|
* Build `clients/web/dist` on demand when it is missing so `--web` always has
|
|
* something to serve. `webRoot` is the web client root (where `package.json` and
|
|
* the `build:client` script live); `distRoot` is the static-asset directory the
|
|
* Hono server serves from. Throws an actionable error if the build can't be
|
|
* produced — the caller surfaces the message and exits non-zero.
|
|
*/
|
|
export function ensureWebBuild(
|
|
webRoot: string,
|
|
distRoot: string,
|
|
deps: EnsureWebBuildDeps = {},
|
|
): void {
|
|
const exists = deps.exists ?? existsSync;
|
|
const build = deps.build ?? runViteBuild;
|
|
const log = deps.log ?? ((message: string) => console.log(message));
|
|
|
|
const indexHtml = join(distRoot, "index.html");
|
|
if (exists(indexHtml)) return;
|
|
|
|
log(
|
|
"No production web build found at clients/web/dist. Building the web UI now (first run only; this can take a minute)...",
|
|
);
|
|
|
|
const status = build(webRoot, log);
|
|
|
|
if (status !== 0 || !exists(indexHtml)) {
|
|
throw new Error(
|
|
"Could not build the web UI automatically. Build it manually with `npm run build` (run from clients/web), or launch with `--dev` to use the Vite dev server.",
|
|
);
|
|
}
|
|
}
|