mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
d056e50467
* fix(config): pass correct Next.js phase to function-form configs unwrapConfig() hardcoded "phase-development-server" regardless of whether vinext was running in dev or build mode. Apps that branch on the phase argument in next.config.ts received wrong config during production builds. Map Vite's env.command to the correct Next.js phase constant: - "serve" → "phase-development-server" - "build" → "phase-production-build" * test: add instrumentation module test coverage The instrumentation module had zero tests. Add 11 tests covering: - findInstrumentationFile: file discovery across root and src/, priority ordering, null when absent - runInstrumentation: register() lifecycle, onRequestError handler storage, graceful handling of empty modules, error isolation when ssrLoadModule fails - reportRequestError: handler invocation with correct args, no-op without handler, error isolation when handler throws * fix(test): use shared helper for App Router static export test The static-export test had its own startFixtureServer that loaded the fixture's vite.config.ts via configFile. This caused RSC plugin timing issues on Node 25 / macOS — the App Router dev server returned 404 for all pages. The Pages Router section (same approach) worked because it doesn't use the RSC plugin. Switch the App Router section to use the shared startFixtureServerHelper from helpers.ts, which uses configFile: false with explicit plugin registration. This matches how app-router.test.ts (193 passing tests) creates its dev server and avoids the RSC timing issue. * fix: address PR review feedback - Import PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants from shims/constants.ts instead of hardcoding string literals in index.ts and config/next-config.ts - Add afterEach cleanup for temp directories in instrumentation tests * refactor: use phase constants in tests, simplify pure function test setup - Replace hardcoded phase strings in next-config.test.ts with PHASE_PRODUCTION_BUILD and PHASE_DEVELOPMENT_SERVER constants - Remove unnecessary vi.resetModules() from findInstrumentationFile tests — it's a pure function with no module-level state to reset * refactor: use shared startFixtureServer helper in static export tests Remove the local startFixtureServer function — the shared helper from tests/helpers.ts does the same thing. The Pages Router fixture's vite.config.ts is just `vinext()` with no special options, so the shared helper (which uses configFile: false + vinext plugin) works identically.
177 lines
5.8 KiB
TypeScript
177 lines
5.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { findInstrumentationFile } from "../packages/vinext/src/server/instrumentation.js";
|
|
|
|
// The runInstrumentation/reportRequestError describe blocks re-import via
|
|
// vi.resetModules() to get fresh module-level state (_onRequestError).
|
|
// findInstrumentationFile is a pure function — no reset needed.
|
|
|
|
describe("findInstrumentationFile", () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-instr-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("returns the path when a file exists at root", () => {
|
|
fs.writeFileSync(path.join(tmpDir, "instrumentation.ts"), "");
|
|
|
|
const result = findInstrumentationFile(tmpDir);
|
|
|
|
expect(result).toBe(path.join(tmpDir, "instrumentation.ts"));
|
|
});
|
|
|
|
it("prefers root over src/ directory (priority order)", () => {
|
|
// Create both root and src/ variants
|
|
fs.writeFileSync(path.join(tmpDir, "instrumentation.ts"), "");
|
|
fs.mkdirSync(path.join(tmpDir, "src"));
|
|
fs.writeFileSync(path.join(tmpDir, "src", "instrumentation.ts"), "");
|
|
|
|
const result = findInstrumentationFile(tmpDir);
|
|
|
|
// Root files come first in INSTRUMENTATION_FILES, so root wins
|
|
expect(result).toBe(path.join(tmpDir, "instrumentation.ts"));
|
|
});
|
|
|
|
it("falls back to src/ directory", () => {
|
|
fs.mkdirSync(path.join(tmpDir, "src"));
|
|
fs.writeFileSync(path.join(tmpDir, "src", "instrumentation.ts"), "");
|
|
|
|
const result = findInstrumentationFile(tmpDir);
|
|
|
|
expect(result).toBe(path.join(tmpDir, "src", "instrumentation.ts"));
|
|
});
|
|
|
|
it("returns null when no instrumentation file exists", () => {
|
|
const result = findInstrumentationFile(tmpDir);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("runInstrumentation", () => {
|
|
let runInstrumentation: typeof import("../packages/vinext/src/server/instrumentation.js").runInstrumentation;
|
|
let getOnRequestErrorHandler: typeof import("../packages/vinext/src/server/instrumentation.js").getOnRequestErrorHandler;
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
const mod = await import("../packages/vinext/src/server/instrumentation.js");
|
|
runInstrumentation = mod.runInstrumentation;
|
|
getOnRequestErrorHandler = mod.getOnRequestErrorHandler;
|
|
});
|
|
|
|
it("calls register() when exported", async () => {
|
|
const register = vi.fn();
|
|
const server = {
|
|
ssrLoadModule: vi.fn().mockResolvedValue({ register }),
|
|
};
|
|
|
|
await runInstrumentation(server, "/fake/instrumentation.ts");
|
|
|
|
expect(register).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("stores onRequestError handler for later retrieval", async () => {
|
|
const onRequestError = vi.fn();
|
|
const server = {
|
|
ssrLoadModule: vi.fn().mockResolvedValue({ onRequestError }),
|
|
};
|
|
|
|
await runInstrumentation(server, "/fake/instrumentation.ts");
|
|
|
|
expect(getOnRequestErrorHandler()).toBe(onRequestError);
|
|
});
|
|
|
|
it("handles modules with no register or onRequestError gracefully", async () => {
|
|
const server = {
|
|
ssrLoadModule: vi.fn().mockResolvedValue({}),
|
|
};
|
|
|
|
// Should not throw
|
|
await runInstrumentation(server, "/fake/instrumentation.ts");
|
|
|
|
expect(getOnRequestErrorHandler()).toBeNull();
|
|
});
|
|
|
|
it("logs error and continues when ssrLoadModule fails", async () => {
|
|
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
const server = {
|
|
ssrLoadModule: vi.fn().mockRejectedValue(new Error("Module not found")),
|
|
};
|
|
|
|
// Should not throw
|
|
await runInstrumentation(server, "/fake/instrumentation.ts");
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
"[vinext] Failed to load instrumentation:",
|
|
"Module not found",
|
|
);
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("reportRequestError", () => {
|
|
let runInstrumentation: typeof import("../packages/vinext/src/server/instrumentation.js").runInstrumentation;
|
|
let reportRequestError: typeof import("../packages/vinext/src/server/instrumentation.js").reportRequestError;
|
|
|
|
const sampleRequest = { path: "/test", method: "GET", headers: {} };
|
|
const sampleContext = {
|
|
routerKind: "App Router" as const,
|
|
routePath: "/test",
|
|
routeType: "render" as const,
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
const mod = await import("../packages/vinext/src/server/instrumentation.js");
|
|
runInstrumentation = mod.runInstrumentation;
|
|
reportRequestError = mod.reportRequestError;
|
|
});
|
|
|
|
it("calls the registered handler with correct args", async () => {
|
|
const onRequestError = vi.fn();
|
|
const server = {
|
|
ssrLoadModule: vi.fn().mockResolvedValue({ onRequestError }),
|
|
};
|
|
await runInstrumentation(server, "/fake/instrumentation.ts");
|
|
|
|
const error = new Error("boom");
|
|
await reportRequestError(error, sampleRequest, sampleContext);
|
|
|
|
expect(onRequestError).toHaveBeenCalledWith(
|
|
error,
|
|
sampleRequest,
|
|
sampleContext,
|
|
);
|
|
});
|
|
|
|
it("no-ops when no handler is registered", async () => {
|
|
// No runInstrumentation called, so _onRequestError is null.
|
|
// Should not throw.
|
|
await reportRequestError(new Error("boom"), sampleRequest, sampleContext);
|
|
});
|
|
|
|
it("catches and logs errors thrown by the handler", async () => {
|
|
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
const onRequestError = vi.fn().mockRejectedValue(new Error("handler broke"));
|
|
const server = {
|
|
ssrLoadModule: vi.fn().mockResolvedValue({ onRequestError }),
|
|
};
|
|
await runInstrumentation(server, "/fake/instrumentation.ts");
|
|
|
|
await reportRequestError(new Error("boom"), sampleRequest, sampleContext);
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
"[vinext] onRequestError handler threw:",
|
|
"handler broke",
|
|
);
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|