Improved CLI init Next.js middleware detection

This commit is contained in:
Matt Aitken
2023-09-29 11:26:25 +01:00
parent 3ca4456c88
commit 7760e09462
4 changed files with 217 additions and 42 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Improved CLI init Next.js middleware detection
+22 -1
View File
@@ -10,6 +10,7 @@ import { logger } from "../../utils/logger";
import { getPathAlias } from "../../utils/pathAlias";
import { readPackageJson } from "../../utils/readPackageJson";
import { standardWatchFilePaths } from "../watchConfig";
import { telemetryClient } from "../../telemetry/telemetry";
import { detectMiddlewareUsage } from "./middleware";
export class NextJs implements Framework {
@@ -65,7 +66,27 @@ export class NextJs implements Framework {
path: string,
options: { typescript: boolean; packageManager: PackageManager; endpointSlug: string }
): Promise<void> {
await detectMiddlewareUsage(path);
const result = await detectMiddlewareUsage(path, options.typescript);
if (result.hasMiddleware) {
switch (result.conflict) {
case "possible": {
logger.warn(
`⚠️ ⚠️ ⚠️ It looks like there might be conflicting Next.js middleware in ${result.middlewarePath} which can cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
break;
}
case "likely": {
logger.warn(
`🚨 It looks like there might be conflicting Next.js middleware in ${result.middlewarePath} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
break;
}
default:
break;
}
}
}
defaultHostnames = ["localhost"];
@@ -4,66 +4,87 @@ import pathModule from "path";
import { logger } from "../../utils/logger";
import { telemetryClient } from "../../telemetry/telemetry";
import { pathToRegexp } from "path-to-regexp";
import { detectUseOfSrcDir } from ".";
export async function detectMiddlewareUsage(path: string, usesSrcDir = false) {
const middlewarePath = pathModule.join(path, usesSrcDir ? "src" : "", "middleware.ts");
type Result =
| {
hasMiddleware: false;
}
| {
hasMiddleware: true;
conflict: "unlikely" | "possible" | "likely";
middlewarePath: string;
};
const middlewareExists = await pathExists(middlewarePath);
export async function detectMiddlewareUsage(path: string, typescript: boolean): Promise<Result> {
const usesSrcDir = await detectUseOfSrcDir(path);
const middlewarePath = pathModule.join(
path,
usesSrcDir ? "src" : "",
`middleware.${typescript ? "ts" : "js"}`
);
if (!middlewareExists) {
return;
try {
return await detectMiddleware(path, typescript, middlewarePath);
} catch (e) {
return {
hasMiddleware: true,
conflict: "possible",
middlewarePath: pathModule.relative(process.cwd(), middlewarePath),
};
}
}
async function detectMiddleware(
path: string,
typescript: boolean,
middlewarePath: string
): Promise<Result> {
const middlewareExists = await pathExists(middlewarePath);
if (!middlewareExists) {
return { hasMiddleware: false };
}
const middlewareRelativeFilePath = pathModule.relative(process.cwd(), middlewarePath);
const matcher = await getMiddlewareConfigMatcher(middlewarePath);
if (!matcher || matcher.length === 0) {
logger.warn(
`⚠️ ⚠️ ⚠️ It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
process.cwd(),
middlewarePath
)} which can cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
return;
return {
hasMiddleware: true,
conflict: "possible",
middlewarePath: middlewareRelativeFilePath,
};
}
if (matcher.length === 0) {
return;
return {
hasMiddleware: true,
conflict: "unlikely",
middlewarePath: middlewareRelativeFilePath,
};
}
if (typeof matcher === "string") {
const matcherRegex = pathToRegexp(matcher);
// Check to see if /api/trigger matches the regex, if it does, then we need to output a warning with a link to the docs to fix it
if (matcherRegex.test("/api/trigger")) {
logger.warn(
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
process.cwd(),
middlewarePath
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict_api_trigger", { projectPath: path });
}
} else if (Array.isArray(matcher) && matcher.every((m) => typeof m === "string")) {
const matcherRegexes = matcher.map((m) => pathToRegexp(m));
if (matcherRegexes.some((r) => r.test("/api/trigger"))) {
logger.warn(
`🚨 It looks like there might be conflicting Next.js middleware in ${pathModule.relative(
process.cwd(),
middlewarePath
)} which will cause issues with Trigger.dev. Please see https://trigger.dev/docs/documentation/guides/platforms/nextjs#middleware`
);
telemetryClient.init.warning("middleware_conflict", { projectPath: path });
}
const matcherRegexes = matcher.map((m) => pathToRegexp(m));
if (matcherRegexes.some((r) => r.test("/api/trigger"))) {
return {
hasMiddleware: true,
conflict: "likely",
middlewarePath: middlewareRelativeFilePath,
};
}
return {
hasMiddleware: true,
conflict: "possible",
middlewarePath: middlewareRelativeFilePath,
};
}
async function getMiddlewareConfigMatcher(path: string): Promise<Array<string>> {
const fileContent = await fs.readFile(path, "utf-8");
const regex = /matcher:\s*(\[.*\]|".*")/s;
const regex = /matcher:\s*(\[.*\]|["'].*["'])/g;
let match = regex.exec(fileContent);
if (!match) {
@@ -2,6 +2,7 @@ import mock from "mock-fs";
import { NextJs, detectPagesOrAppDir, detectUseOfSrcDir } from ".";
import { getFramework } from "..";
import { pathExists } from "../../utils/fileSystem";
import { detectMiddlewareUsage } from "./middleware";
afterEach(() => {
mock.restore();
@@ -257,3 +258,130 @@ describe("app install", () => {
expect(await pathExists("jobs/examples.ts")).toEqual(true);
});
});
describe("Next middleware detection", () => {
test("no middleware", async () => {
mock({});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(false);
});
test("Basic middleware", async () => {
mock({
"middleware.js": `import { NextResponse } from 'next/server'
export function middleware(request) {
return NextResponse.redirect(new URL('/home', request.url))
}
// See "Matching Paths" below to learn more
export const config = {
matcher: '/about/:path*',
}`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("possible");
});
test("Wildcard that throws middleware", async () => {
mock({
"middleware.js": `export const config = {
matcher: "*",
}`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("possible");
});
test("Array middleware", async () => {
mock({
"middleware.js": `export const config = {
matcher: ['/about/:path*', "/dashboard/:path*"],
}`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("possible");
});
test("With dashes middleware", async () => {
mock({
"middleware.js": `export const config = {
matcher: ["/configurations-test/:path*", "/projects/:path*"],
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("possible");
});
test("Likely double quoted string", async () => {
mock({
"middleware.js": `export const config = {
matcher: "/(.*)",
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("likely");
});
test("Likely single quoted string", async () => {
mock({
"middleware.js": `export const config = {
matcher: '/(.*)',
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("likely");
});
test("Likely double quoted array", async () => {
mock({
"middleware.js": `export const config = {
matcher: ["/pages/", "/(.*)"],
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("likely");
});
test("Likely single quoted array", async () => {
mock({
"middleware.js": `export const config = {
matcher: ['/pages/', '/(.*)'],
};`,
});
const result = await detectMiddlewareUsage("", false);
expect(result.hasMiddleware).toEqual(true);
if (!result.hasMiddleware) throw "Should have middleware";
expect(result.middlewarePath).toEqual("middleware.js");
expect(result.conflict).toEqual("likely");
});
});