- Introduce DevServerStore and DevServerProcessManager with a simplified store API contract - Add dev-server script detection and route legacy manager behavior through the new process manager - Add a dedicated dev-server router with start/stop/status/log endpoints and SSE event streaming - Wire dev-server routes into the main route table and hook manager shutdown into server teardown - Expand tests and architecture docs for store, process, route, detection, and SSE coverage
188 lines
6.2 KiB
TypeScript
188 lines
6.2 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { detectDevServerScripts } from "../dev-server-detect.js";
|
|
|
|
function writeJson(filePath: string, value: unknown): void {
|
|
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
}
|
|
|
|
describe("detectDevServerScripts", () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = mkdtempSync(path.join(os.tmpdir(), "fn-dev-detect-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("detects dev script in root package.json", async () => {
|
|
writeJson(path.join(tempDir, "package.json"), {
|
|
scripts: { dev: "vite" },
|
|
devDependencies: { vite: "^6.0.0" },
|
|
private: true,
|
|
});
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
expect(result.candidates).toHaveLength(1);
|
|
expect(result.candidates[0]).toMatchObject({
|
|
name: "dev",
|
|
command: "vite",
|
|
source: "root",
|
|
});
|
|
});
|
|
|
|
it("detects multiple matching scripts", async () => {
|
|
writeJson(path.join(tempDir, "package.json"), {
|
|
scripts: {
|
|
dev: "vite",
|
|
start: "next dev",
|
|
preview: "vite preview",
|
|
},
|
|
devDependencies: { vite: "^6.0.0" },
|
|
private: true,
|
|
});
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
expect(result.candidates.map((candidate) => candidate.name).sort()).toEqual(["dev", "preview", "start"]);
|
|
});
|
|
|
|
it("returns empty candidates when no matching scripts", async () => {
|
|
writeJson(path.join(tempDir, "package.json"), {
|
|
scripts: { test: "vitest", build: "vite build" },
|
|
});
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
expect(result.candidates).toEqual([]);
|
|
});
|
|
|
|
it("returns empty candidates when package.json does not exist", async () => {
|
|
const result = await detectDevServerScripts(tempDir);
|
|
expect(result.candidates).toEqual([]);
|
|
});
|
|
|
|
it("returns empty candidates when package.json has no scripts field", async () => {
|
|
writeJson(path.join(tempDir, "package.json"), { name: "demo" });
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
expect(result.candidates).toEqual([]);
|
|
});
|
|
|
|
it("monorepo detection scans packages/*/package.json", async () => {
|
|
writeJson(path.join(tempDir, "package.json"), { name: "root" });
|
|
const pkgDir = path.join(tempDir, "packages", "web");
|
|
mkdirSync(pkgDir, { recursive: true });
|
|
writeJson(path.join(pkgDir, "package.json"), {
|
|
name: "@demo/web",
|
|
scripts: { dev: "vite" },
|
|
devDependencies: { vite: "^6.0.0" },
|
|
private: true,
|
|
});
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
expect(result.candidates).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "dev",
|
|
source: "packages/web",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("monorepo detection scans apps/*/package.json", async () => {
|
|
writeJson(path.join(tempDir, "package.json"), { name: "root" });
|
|
const appDir = path.join(tempDir, "apps", "client");
|
|
mkdirSync(appDir, { recursive: true });
|
|
writeJson(path.join(appDir, "package.json"), {
|
|
name: "client",
|
|
scripts: { dev: "vite" },
|
|
devDependencies: { vite: "^6.0.0" },
|
|
private: true,
|
|
});
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
expect(result.candidates).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "dev",
|
|
source: "apps/client",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("confidence scoring gives dev higher score than serve", async () => {
|
|
writeJson(path.join(tempDir, "package.json"), {
|
|
scripts: { dev: "vite", serve: "vite preview" },
|
|
devDependencies: { vite: "^6.0.0" },
|
|
private: true,
|
|
});
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
const dev = result.candidates.find((candidate) => candidate.name === "dev");
|
|
const serve = result.candidates.find((candidate) => candidate.name === "serve");
|
|
|
|
expect(dev).toBeDefined();
|
|
expect(serve).toBeDefined();
|
|
expect((dev?.confidence ?? 0)).toBeGreaterThan(serve?.confidence ?? 0);
|
|
});
|
|
|
|
it("framework indicators boost confidence", async () => {
|
|
writeJson(path.join(tempDir, "package.json"), { name: "root" });
|
|
|
|
const withFrameworkDir = path.join(tempDir, "apps", "with-framework");
|
|
mkdirSync(withFrameworkDir, { recursive: true });
|
|
writeJson(path.join(withFrameworkDir, "package.json"), {
|
|
scripts: { serve: "vite" },
|
|
devDependencies: { vite: "^6.0.0" },
|
|
private: true,
|
|
});
|
|
|
|
const withoutFrameworkDir = path.join(tempDir, "apps", "without-framework");
|
|
mkdirSync(withoutFrameworkDir, { recursive: true });
|
|
writeJson(path.join(withoutFrameworkDir, "package.json"), {
|
|
scripts: { serve: "custom-server" },
|
|
private: true,
|
|
});
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
|
|
const withFramework = result.candidates.find((candidate) => candidate.source === "apps/with-framework");
|
|
const withoutFramework = result.candidates.find((candidate) => candidate.source === "apps/without-framework");
|
|
|
|
expect(withFramework).toBeDefined();
|
|
expect(withoutFramework).toBeDefined();
|
|
expect((withFramework?.confidence ?? 0)).toBeGreaterThan(withoutFramework?.confidence ?? 0);
|
|
});
|
|
|
|
it("results are sorted by confidence descending", async () => {
|
|
writeJson(path.join(tempDir, "package.json"), {
|
|
scripts: {
|
|
preview: "vite preview",
|
|
dev: "vite",
|
|
serve: "vite serve",
|
|
},
|
|
devDependencies: { vite: "^6.0.0" },
|
|
private: true,
|
|
});
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
const confidences = result.candidates.map((candidate) => candidate.confidence);
|
|
|
|
expect(confidences).toEqual([...confidences].sort((a, b) => b - a));
|
|
});
|
|
|
|
it("handles malformed package.json gracefully", async () => {
|
|
writeFileSync(path.join(tempDir, "package.json"), "{invalid", "utf-8");
|
|
|
|
const pkgDir = path.join(tempDir, "packages", "web");
|
|
mkdirSync(pkgDir, { recursive: true });
|
|
writeFileSync(path.join(pkgDir, "package.json"), "{broken", "utf-8");
|
|
|
|
const result = await detectDevServerScripts(tempDir);
|
|
expect(result.candidates).toEqual([]);
|
|
});
|
|
});
|