diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index db15cdb3..98ac869e 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -1507,14 +1507,14 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { appDir = path.join(baseDir, "app"); hasPagesDir = fs.existsSync(pagesDir); hasAppDir = !options.disableAppRouter && fs.existsSync(appDir); - middlewarePath = findMiddlewareFile(root); - instrumentationPath = findInstrumentationFile(root); // Load next.config.js if present (always from project root, not src/) const phase = env?.command === "build" ? PHASE_PRODUCTION_BUILD : PHASE_DEVELOPMENT_SERVER; const rawConfig = await loadNextConfig(root, phase); nextConfig = await resolveNextConfig(rawConfig, root); fileMatcher = createValidFileMatcher(nextConfig.pageExtensions); + instrumentationPath = findInstrumentationFile(root, fileMatcher); + middlewarePath = findMiddlewareFile(root, fileMatcher); // Merge env from next.config.js with NEXT_PUBLIC_* env vars const defines = getNextPublicEnvDefines(); diff --git a/packages/vinext/src/server/instrumentation.ts b/packages/vinext/src/server/instrumentation.ts index 0e084f03..20bd353e 100644 --- a/packages/vinext/src/server/instrumentation.ts +++ b/packages/vinext/src/server/instrumentation.ts @@ -39,6 +39,7 @@ import fs from "node:fs"; import path from "node:path"; import { getRequestExecutionContext } from "../shims/request-context.js"; +import { ValidFileMatcher } from "../routing/file-matcher.js"; /** * Minimal duck-typed interface for the module runner passed to * `runInstrumentation`. Only `.import()` is used — this avoids requiring @@ -63,26 +64,21 @@ export async function importModule( return (await runner.import(id)) as Record; } -/** Possible instrumentation file names. */ -const INSTRUMENTATION_FILES = [ - "instrumentation.ts", - "instrumentation.tsx", - "instrumentation.js", - "instrumentation.mjs", - "src/instrumentation.ts", - "src/instrumentation.tsx", - "src/instrumentation.js", - "src/instrumentation.mjs", -]; +const INSTRUMENTATION_LOCATIONS = ["", "src/"]; /** * Find the instrumentation file in the project root. */ -export function findInstrumentationFile(root: string): string | null { - for (const file of INSTRUMENTATION_FILES) { - const fullPath = path.join(root, file); - if (fs.existsSync(fullPath)) { - return fullPath; +export function findInstrumentationFile( + root: string, + fileMatcher: ValidFileMatcher, +): string | null { + for (const dir of INSTRUMENTATION_LOCATIONS) { + for (const ext of fileMatcher.dottedExtensions) { + const fullPath = path.join(root, dir, `instrumentation${ext}`); + if (fs.existsSync(fullPath)) { + return fullPath; + } } } return null; diff --git a/packages/vinext/src/server/middleware.ts b/packages/vinext/src/server/middleware.ts index efdce4ad..0244f3dd 100644 --- a/packages/vinext/src/server/middleware.ts +++ b/packages/vinext/src/server/middleware.ts @@ -32,6 +32,7 @@ import { NextRequest, NextFetchEvent } from "../shims/server.js"; import { normalizePath } from "./normalize-path.js"; import { shouldKeepMiddlewareHeader } from "./middleware-request-headers.js"; import { normalizePathnameForRouteMatchStrict } from "../routing/utils.js"; +import { ValidFileMatcher } from "../routing/file-matcher.js"; /** * Determine whether a middleware/proxy file path refers to a proxy file. @@ -73,53 +74,35 @@ export function resolveMiddlewareHandler(mod: Record, filePath: return handler as Function; } -/** - * Possible proxy/middleware file names. - * proxy.ts (Next.js 16) is checked first, then middleware.ts (deprecated). - */ -const PROXY_FILES = [ - "proxy.ts", - "proxy.js", - "proxy.mjs", - "src/proxy.ts", - "src/proxy.js", - "src/proxy.mjs", -]; - -const MIDDLEWARE_FILES = [ - "middleware.ts", - "middleware.tsx", - "middleware.js", - "middleware.mjs", - "src/middleware.ts", - "src/middleware.tsx", - "src/middleware.js", - "src/middleware.mjs", -]; +const MIDDLEWARE_LOCATIONS = ["", "src/"]; /** * Find the proxy or middleware file in the project root. * Checks for proxy.ts (Next.js 16) first, then falls back to middleware.ts. * If middleware.ts is found, logs a deprecation warning. */ -export function findMiddlewareFile(root: string): string | null { +export function findMiddlewareFile(root: string, fileMatcher: ValidFileMatcher): string | null { // Check proxy.ts first (Next.js 16 replacement for middleware.ts) - for (const file of PROXY_FILES) { - const fullPath = path.join(root, file); - if (fs.existsSync(fullPath)) { - return fullPath; + for (const dir of MIDDLEWARE_LOCATIONS) { + for (const ext of fileMatcher.dottedExtensions) { + const fullPath = path.join(root, dir, `proxy${ext}`); + if (fs.existsSync(fullPath)) { + return fullPath; + } } } // Fall back to middleware.ts (deprecated in Next.js 16) - for (const file of MIDDLEWARE_FILES) { - const fullPath = path.join(root, file); - if (fs.existsSync(fullPath)) { - console.warn( - "[vinext] middleware.ts is deprecated in Next.js 16. " + - "Rename to proxy.ts and export a default or named proxy function.", - ); - return fullPath; + for (const dir of MIDDLEWARE_LOCATIONS) { + for (const ext of fileMatcher.dottedExtensions) { + const fullPath = path.join(root, dir, `middleware${ext}`); + if (fs.existsSync(fullPath)) { + console.warn( + "[vinext] middleware.ts is deprecated in Next.js 16. " + + "Rename to proxy.ts and export a default or named proxy function.", + ); + return fullPath; + } } } return null; diff --git a/tests/features.test.ts b/tests/features.test.ts index e00e14c2..d1f1096d 100644 --- a/tests/features.test.ts +++ b/tests/features.test.ts @@ -20,6 +20,7 @@ import { requestNodeServerWithHost, startFixtureServer, } from "./helpers.js"; +import { createValidFileMatcher } from "../packages/vinext/src/routing/file-matcher.js"; const FIXTURE_DIR = PAGES_FIXTURE_DIR; @@ -2817,7 +2818,7 @@ describe("instrumentation.ts support", () => { it("findInstrumentationFile returns null when no file exists", async () => { const { findInstrumentationFile } = await import("../packages/vinext/src/server/instrumentation.js"); - const result = findInstrumentationFile("/nonexistent/path"); + const result = findInstrumentationFile("/nonexistent/path", createValidFileMatcher()); expect(result).toBeNull(); }); @@ -2835,7 +2836,7 @@ describe("instrumentation.ts support", () => { 'export function register() { console.log("registered"); }', ); - const result = findInstrumentationFile(tmpDir); + const result = findInstrumentationFile(tmpDir, createValidFileMatcher()); expect(result).toBe(path.join(tmpDir, "instrumentation.ts")); // Cleanup @@ -2856,7 +2857,7 @@ describe("instrumentation.ts support", () => { "export function register() {}", ); - const result = findInstrumentationFile(tmpDir); + const result = findInstrumentationFile(tmpDir, createValidFileMatcher()); expect(result).toBe(path.join(tmpDir, "src", "instrumentation.ts")); fs.rmSync(tmpDir, { recursive: true }); diff --git a/tests/instrumentation.test.ts b/tests/instrumentation.test.ts index 18598f00..e2481d9a 100644 --- a/tests/instrumentation.test.ts +++ b/tests/instrumentation.test.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { findInstrumentationFile } from "../packages/vinext/src/server/instrumentation.js"; +import { createValidFileMatcher } from "../packages/vinext/src/routing/file-matcher.js"; // The runInstrumentation/reportRequestError describe blocks re-import via // vi.resetModules() to get fresh module-level state (_onRequestError). @@ -22,7 +23,7 @@ describe("findInstrumentationFile", () => { it("returns the path when a file exists at root", () => { fs.writeFileSync(path.join(tmpDir, "instrumentation.ts"), ""); - const result = findInstrumentationFile(tmpDir); + const result = findInstrumentationFile(tmpDir, createValidFileMatcher()); expect(result).toBe(path.join(tmpDir, "instrumentation.ts")); }); @@ -33,7 +34,7 @@ describe("findInstrumentationFile", () => { fs.mkdirSync(path.join(tmpDir, "src")); fs.writeFileSync(path.join(tmpDir, "src", "instrumentation.ts"), ""); - const result = findInstrumentationFile(tmpDir); + const result = findInstrumentationFile(tmpDir, createValidFileMatcher()); // Root files come first in INSTRUMENTATION_FILES, so root wins expect(result).toBe(path.join(tmpDir, "instrumentation.ts")); @@ -43,13 +44,13 @@ describe("findInstrumentationFile", () => { fs.mkdirSync(path.join(tmpDir, "src")); fs.writeFileSync(path.join(tmpDir, "src", "instrumentation.ts"), ""); - const result = findInstrumentationFile(tmpDir); + const result = findInstrumentationFile(tmpDir, createValidFileMatcher()); expect(result).toBe(path.join(tmpDir, "src", "instrumentation.ts")); }); it("returns null when no instrumentation file exists", () => { - const result = findInstrumentationFile(tmpDir); + const result = findInstrumentationFile(tmpDir, createValidFileMatcher()); expect(result).toBeNull(); }); diff --git a/tests/shims.test.ts b/tests/shims.test.ts index 5000242d..3b714fc2 100644 --- a/tests/shims.test.ts +++ b/tests/shims.test.ts @@ -2107,30 +2107,64 @@ describe("replyToCacheKey deterministic hashing", () => { describe("middleware runner", () => { it("findMiddlewareFile finds middleware.ts at project root", async () => { const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js"); + const { createValidFileMatcher } = + await import("../packages/vinext/src/routing/file-matcher.js"); // pages-basic fixture has middleware.ts - const result = findMiddlewareFile(FIXTURE_DIR); + const result = findMiddlewareFile(FIXTURE_DIR, createValidFileMatcher()); expect(result).not.toBeNull(); expect(result).toContain("middleware.ts"); }); it("findMiddlewareFile returns null when no middleware exists", async () => { const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js"); - const result = findMiddlewareFile("/tmp/nonexistent-dir-" + Date.now()); + const { createValidFileMatcher } = + await import("../packages/vinext/src/routing/file-matcher.js"); + const result = findMiddlewareFile( + "/tmp/nonexistent-dir-" + Date.now(), + createValidFileMatcher(), + ); + expect(result).toBeNull(); + }); + + it("findMiddlewareFile does not find middleware.ts when ts is not a configured pageExtension", async () => { + const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js"); + const { createValidFileMatcher } = + await import("../packages/vinext/src/routing/file-matcher.js"); + // FIXTURE_DIR has middleware.ts — restricting to mdx only means it should not match + const result = findMiddlewareFile(FIXTURE_DIR, createValidFileMatcher(["mdx"])); expect(result).toBeNull(); }); + it("findMiddlewareFile emits a deprecation warning when middleware.ts is found", async () => { + const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js"); + const { createValidFileMatcher } = + await import("../packages/vinext/src/routing/file-matcher.js"); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + try { + findMiddlewareFile(FIXTURE_DIR, createValidFileMatcher()); + expect(warnSpy).toHaveBeenCalledOnce(); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("middleware.ts is deprecated in Next.js 16"), + ); + } finally { + warnSpy.mockRestore(); + } + }); + it("findMiddlewareFile prefers proxy.ts over middleware.ts (Next.js 16)", async () => { const fs = await import("node:fs"); const path = await import("node:path"); const os = await import("node:os"); const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js"); + const { createValidFileMatcher } = + await import("../packages/vinext/src/routing/file-matcher.js"); // Create a temp directory with both proxy.ts and middleware.ts const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-proxy-test-")); try { fs.writeFileSync(path.join(tmpDir, "proxy.ts"), "export default function proxy() {}"); fs.writeFileSync(path.join(tmpDir, "middleware.ts"), "export function middleware() {}"); - const result = findMiddlewareFile(tmpDir); + const result = findMiddlewareFile(tmpDir, createValidFileMatcher()); expect(result).not.toBeNull(); expect(result).toContain("proxy.ts"); } finally { @@ -2143,11 +2177,13 @@ describe("middleware runner", () => { const path = await import("node:path"); const os = await import("node:os"); const { findMiddlewareFile } = await import("../packages/vinext/src/server/middleware.js"); + const { createValidFileMatcher } = + await import("../packages/vinext/src/routing/file-matcher.js"); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-proxy-test-")); try { fs.writeFileSync(path.join(tmpDir, "proxy.js"), "module.exports = function proxy() {}"); - const result = findMiddlewareFile(tmpDir); + const result = findMiddlewareFile(tmpDir, createValidFileMatcher()); expect(result).not.toBeNull(); expect(result).toContain("proxy.js"); } finally {