diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index ed2680467a47..c43be3565ab0 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -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; wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'server-component' | 'sentry-init' | 'route-handler'; @@ -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 @@ -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 diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 5816c0bf058c..1e85ae125a92 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -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']; @@ -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)) @@ -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 => { @@ -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)$/) ); @@ -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)$/) ); diff --git a/packages/nextjs/test/config/loaders.test.ts b/packages/nextjs/test/config/loaders.test.ts index 62616c58c3a1..ed3589363539 100644 --- a/packages/nextjs/test/config/loaders.test.ts +++ b/packages/nextjs/test/config/loaders.test.ts @@ -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, @@ -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({ @@ -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', @@ -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, }, { @@ -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,