Skip to content

fix(nextjs): Fix middleware detection logic #9637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 23, 2023
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
14 changes: 12 additions & 2 deletions packages/nextjs/src/config/loaders/wrappingLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const routeHandlerWrapperTemplatePath = path.resolve(__dirname, '..', 'templates
const routeHandlerWrapperTemplateCode = fs.readFileSync(routeHandlerWrapperTemplatePath, { encoding: 'utf8' });

export type WrappingLoaderOptions = {
pagesDir: string;
appDir: string;
pagesDir: string | undefined;
appDir: string | undefined;
pageExtensionRegex: string;
excludeServerRoutes: Array<RegExp | string>;
wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'server-component' | 'sentry-init' | 'route-handler';
Expand Down Expand Up @@ -101,6 +101,11 @@ export default function wrappingLoader(
return;
}
} else if (wrappingTargetKind === 'page' || wrappingTargetKind === 'api-route') {
if (pagesDir === undefined) {
this.callback(null, userCode, userModuleSourceMap);
return;
}

// Get the parameterized route name from this page's filepath
const parameterizedPagesRoute = path
// Get the path of the file insde of the pages directory
Expand Down Expand Up @@ -137,6 +142,11 @@ export default function wrappingLoader(
// Inject the route and the path to the file we're wrapping into the template
templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, '\\\\'));
} else if (wrappingTargetKind === 'server-component' || wrappingTargetKind === 'route-handler') {
if (appDir === undefined) {
this.callback(null, userCode, userModuleSourceMap);
return;
}

// Get the parameterized route name from this page's filepath
const parameterizedPagesRoute = path
// Get the path of the file insde of the app directory
Expand Down
35 changes: 23 additions & 12 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,26 +98,31 @@ export function constructWebpackConfigFunction(
],
});

let pagesDirPath: string;
let pagesDirPath: string | undefined;
const maybePagesDirPath = path.join(projectDir, 'pages');
const maybeSrcPagesDirPath = path.join(projectDir, 'src', 'pages');
if (fs.existsSync(maybePagesDirPath) && fs.lstatSync(maybePagesDirPath).isDirectory()) {
pagesDirPath = path.join(projectDir, 'pages');
} else {
pagesDirPath = path.join(projectDir, 'src', 'pages');
pagesDirPath = maybePagesDirPath;
} else if (fs.existsSync(maybeSrcPagesDirPath) && fs.lstatSync(maybeSrcPagesDirPath).isDirectory()) {
pagesDirPath = maybeSrcPagesDirPath;
}

let appDirPath: string;
let appDirPath: string | undefined;
const maybeAppDirPath = path.join(projectDir, 'app');
const maybeSrcAppDirPath = path.join(projectDir, 'src', 'app');
if (fs.existsSync(maybeAppDirPath) && fs.lstatSync(maybeAppDirPath).isDirectory()) {
appDirPath = path.join(projectDir, 'app');
} else {
appDirPath = path.join(projectDir, 'src', 'app');
appDirPath = maybeAppDirPath;
} else if (fs.existsSync(maybeSrcAppDirPath) && fs.lstatSync(maybeSrcAppDirPath).isDirectory()) {
appDirPath = maybeSrcAppDirPath;
}

const apiRoutesPath = path.join(pagesDirPath, 'api');
const apiRoutesPath = pagesDirPath ? path.join(pagesDirPath, 'api') : undefined;

const middlewareJsPath = path.join(pagesDirPath, '..', 'middleware.js');
const middlewareTsPath = path.join(pagesDirPath, '..', 'middleware.ts');
const middlewareLocationFolder = pagesDirPath
? path.join(pagesDirPath, '..')
: appDirPath
? path.join(appDirPath, '..')
: projectDir;

// Default page extensions per https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161
const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js'];
Expand Down Expand Up @@ -151,6 +156,7 @@ export function constructWebpackConfigFunction(
const isPageResource = (resourcePath: string): boolean => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
return (
pagesDirPath !== undefined &&
normalizedAbsoluteResourcePath.startsWith(pagesDirPath + path.sep) &&
!normalizedAbsoluteResourcePath.startsWith(apiRoutesPath + path.sep) &&
dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
Expand All @@ -167,7 +173,10 @@ export function constructWebpackConfigFunction(

const isMiddlewareResource = (resourcePath: string): boolean => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
return normalizedAbsoluteResourcePath === middlewareJsPath || normalizedAbsoluteResourcePath === middlewareTsPath;
return (
normalizedAbsoluteResourcePath.startsWith(middlewareLocationFolder + path.sep) &&
!!normalizedAbsoluteResourcePath.match(/[\\/]middleware\.(js|jsx|ts|tsx)$/)
);
};

const isServerComponentResource = (resourcePath: string): boolean => {
Expand All @@ -176,6 +185,7 @@ export function constructWebpackConfigFunction(
// ".js, .jsx, or .tsx file extensions can be used for Pages"
// https://beta.nextjs.org/docs/routing/pages-and-layouts#pages:~:text=.js%2C%20.jsx%2C%20or%20.tsx%20file%20extensions%20can%20be%20used%20for%20Pages.
return (
appDirPath !== undefined &&
normalizedAbsoluteResourcePath.startsWith(appDirPath + path.sep) &&
!!normalizedAbsoluteResourcePath.match(/[\\/](page|layout|loading|head|not-found)\.(js|jsx|tsx)$/)
);
Expand All @@ -184,6 +194,7 @@ export function constructWebpackConfigFunction(
const isRouteHandlerResource = (resourcePath: string): boolean => {
const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath);
return (
appDirPath !== undefined &&
normalizedAbsoluteResourcePath.startsWith(appDirPath + path.sep) &&
!!normalizedAbsoluteResourcePath.match(/[\\/]route\.(js|jsx|ts|tsx)$/)
);
Expand Down
25 changes: 24 additions & 1 deletion packages/nextjs/test/config/loaders.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// mock helper functions not tested directly in this file
import './mocks';

import * as fs from 'fs';

import type { ModuleRuleUseProperty, WebpackModuleRule } from '../../src/config/types';
import {
clientBuildContext,
Expand All @@ -11,6 +13,9 @@ import {
} from './fixtures';
import { materializeFinalWebpackConfig } from './testUtils';

const existsSyncSpy = jest.spyOn(fs, 'existsSync');
const lstatSyncSpy = jest.spyOn(fs, 'lstatSync');

type MatcherResult = { pass: boolean; message: () => string };

expect.extend({
Expand Down Expand Up @@ -85,6 +90,7 @@ describe('webpack loaders', () => {
});
});

// For these tests we assume that we have an app and pages folder in {rootdir}/src
it.each([
{
resourcePath: '/Users/Maisey/projects/squirrelChasingSimulator/src/pages/testPage.tsx',
Expand Down Expand Up @@ -139,8 +145,9 @@ describe('webpack loaders', () => {
resourcePath: '/Users/Maisey/projects/squirrelChasingSimulator/src/middleware.ts',
expectedWrappingTargetKind: 'middleware',
},
// Since we assume we have a pages file in src middleware will only be included in the build if it is also in src
{
resourcePath: '/Users/Maisey/projects/squirrelChasingSimulator/src/middleware.tsx',
resourcePath: '/Users/Maisey/projects/squirrelChasingSimulator/middleware.tsx',
expectedWrappingTargetKind: undefined,
},
{
Expand Down Expand Up @@ -182,6 +189,22 @@ describe('webpack loaders', () => {
])(
'should apply the right wrappingTargetKind with wrapping loader ($resourcePath)',
async ({ resourcePath, expectedWrappingTargetKind }) => {
// We assume that we have an app and pages folder in {rootdir}/src
existsSyncSpy.mockImplementation(path => {
if (
path.toString().startsWith('/Users/Maisey/projects/squirrelChasingSimulator/app') ||
path.toString().startsWith('/Users/Maisey/projects/squirrelChasingSimulator/pages')
) {
return false;
}
return true;
});

// @ts-expect-error Too lazy to mock the entire thing
lstatSyncSpy.mockImplementation(() => ({
isDirectory: () => true,
}));

const finalWebpackConfig = await materializeFinalWebpackConfig({
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
Expand Down