Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/vinext/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
28 changes: 12 additions & 16 deletions packages/vinext/src/server/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -63,26 +64,21 @@ export async function importModule(
return (await runner.import(id)) as Record<string, any>;
}

/** 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;
Expand Down
55 changes: 19 additions & 36 deletions packages/vinext/src/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -73,53 +74,35 @@ export function resolveMiddlewareHandler(mod: Record<string, unknown>, 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;
Expand Down
7 changes: 4 additions & 3 deletions tests/features.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
});

Expand All @@ -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
Expand All @@ -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 });
Expand Down
9 changes: 5 additions & 4 deletions tests/instrumentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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"));
});
Expand All @@ -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"));
Expand All @@ -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();
});
Expand Down
44 changes: 40 additions & 4 deletions tests/shims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Loading