diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index b934157b42e65..f3c1d5d29378f 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -93,6 +93,10 @@ import { TurborepoAccessTraceResult, writeTurborepoAccessTraceResult, } from './turborepo-access-trace' +import { + validateMiddlewareInSrcDir, + validateInstrumentationInSrcDir, +} from './utils' import { eventBuildOptimize, @@ -1157,6 +1161,19 @@ export default async function build( NextBuildContext.hasInstrumentationHook = hasInstrumentationHook + if (isSrcDir) { + for (const rootPath of rootPaths) { + if (rootPath.includes(MIDDLEWARE_FILENAME)) { + const pathWithoutExtension = rootPath.replace(/\.[^/.]+$/, '') + validateMiddlewareInSrcDir(pathWithoutExtension, isSrcDir) + } + if (rootPath.includes(INSTRUMENTATION_HOOK_FILENAME)) { + const pathWithoutExtension = rootPath.replace(/\.[^/.]+$/, '') + validateInstrumentationInSrcDir(pathWithoutExtension, isSrcDir) + } + } + } + const previewProps: __ApiPreviewProps = await generatePreviewKeys({ isBuild: true, distDir, diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index f3a74a59d170a..ac1484ea5bf6c 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -81,6 +81,8 @@ import { buildAppStaticPaths } from './static-paths/app' import { buildPagesStaticPaths } from './static-paths/pages' import type { PrerenderedRoute } from './static-paths/types' import type { CacheControl } from '../server/lib/cache-control' +import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' + import { formatExpire, formatRevalidate } from './output/format' export type ROUTER_TYPE = 'pages' | 'app' @@ -1821,6 +1823,46 @@ export class NestedMiddlewareError extends Error { } } +export class SrcDirectoryError extends Error { + constructor(fileName: string, fileType: 'middleware' | 'instrumentation') { + super( + `${fileType === 'middleware' ? 'Middleware' : 'Instrumentation'} file found outside src directory.\n` + + `Found: ${fileName}\n` + + `When using the src directory, ${fileType} files must be placed inside the src directory.\n` + + `Please move your ${fileType} file to src/${fileType}.\n` + + `Read More - https://nextjs.org/docs/app/api-reference/file-conventions/src-folder` + ) + } +} + +export function validateMiddlewareInSrcDir( + fileName: string, + isSrcDir: boolean +) { + const normalizedPath = normalizePathSep(fileName) + if ( + isSrcDir && + isMiddlewareFile(fileName) && + !normalizedPath.startsWith('/src/') + ) { + throw new SrcDirectoryError(fileName, 'middleware') + } +} + +export function validateInstrumentationInSrcDir( + fileName: string, + isSrcDir: boolean +) { + const normalizedPath = normalizePathSep(fileName) + if ( + isSrcDir && + isInstrumentationHookFile(fileName) && + !normalizedPath.startsWith('/src/') + ) { + throw new SrcDirectoryError(fileName, 'instrumentation') + } +} + export function getSupportedBrowsers( dir: string, isDevelopment: boolean diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 20a9c98890155..c55a10af47900 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -64,6 +64,8 @@ import { isInstrumentationHookFile, getPossibleMiddlewareFilenames, getPossibleInstrumentationHookFilenames, + validateMiddlewareInSrcDir, + validateInstrumentationInSrcDir, } from '../../../build/utils' import { devPageFiles } from '../../../build/webpack/plugins/next-types-plugin/shared' import type { LazyRenderServerInstance } from '../router-server' @@ -426,6 +428,8 @@ async function startWatcher( }) if (isMiddlewareFile(rootFile)) { + validateMiddlewareInSrcDir(rootFile, opts.isSrcDir) + const staticInfo = await getStaticInfoIncludingLayouts({ pageFilePath: fileName, config: nextConfig, @@ -453,6 +457,8 @@ async function startWatcher( continue } if (isInstrumentationHookFile(rootFile)) { + validateInstrumentationInSrcDir(rootFile, opts.isSrcDir) + serverFields.actualInstrumentationHookFile = rootFile await propagateServerField( opts, diff --git a/test/integration/src-dir-validation/next.config.js b/test/integration/src-dir-validation/next.config.js new file mode 100644 index 0000000000000..f3bbebcc4dad4 --- /dev/null +++ b/test/integration/src-dir-validation/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + appDir: false, + }, +} diff --git a/test/integration/src-dir-validation/src/pages/index.js b/test/integration/src-dir-validation/src/pages/index.js new file mode 100644 index 0000000000000..8f5129010b9b0 --- /dev/null +++ b/test/integration/src-dir-validation/src/pages/index.js @@ -0,0 +1,3 @@ +export default function Home() { + return
Hello World
+} diff --git a/test/integration/src-dir-validation/test/index.test.js b/test/integration/src-dir-validation/test/index.test.js new file mode 100644 index 0000000000000..2b027fd764cd1 --- /dev/null +++ b/test/integration/src-dir-validation/test/index.test.js @@ -0,0 +1,59 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import { nextBuild, nextStart, findPort, killApp } from 'next-test-utils' + +const appDir = join(__dirname, '../') + +describe('Src Directory Validation', () => { + beforeEach(async () => { + await fs.remove(join(appDir, 'middleware.js')).catch(() => {}) + await fs.remove(join(appDir, 'instrumentation.js')).catch(() => {}) + }) + + afterEach(async () => { + await fs.remove(join(appDir, 'middleware.js')).catch(() => {}) + await fs.remove(join(appDir, 'instrumentation.js')).catch(() => {}) + }) + + it('should throw error when middleware is outside src directory', async () => { + await fs.writeFile( + join(appDir, 'middleware.js'), + `export function middleware() { return new Response('middleware') }` + ) + + const result = await nextBuild(appDir, [], { stderr: true, stdout: true }) + expect(result.stderr + result.stdout).toContain('Middleware file found outside src directory') + }) + + it('should throw error when instrumentation is outside src directory', async () => { + await fs.writeFile( + join(appDir, 'instrumentation.js'), + `export function register() { console.log('instrumentation') }` + ) + + const result = await nextBuild(appDir, [], { stderr: true, stdout: true }) + expect(result.stderr + result.stdout).toContain('Instrumentation file found outside src directory') + }) + + it('should work correctly when middleware is inside src directory', async () => { + await fs.writeFile( + join(appDir, 'src/middleware.js'), + `export function middleware() { return new Response('middleware') }` + ) + + const result = await nextBuild(appDir, [], { stderr: true, stdout: true }) + expect(result.code).toBe(0) + }) + + it('should work correctly when instrumentation is inside src directory', async () => { + await fs.writeFile( + join(appDir, 'src/instrumentation.js'), + `export function register() { console.log('instrumentation') }` + ) + + const result = await nextBuild(appDir, [], { stderr: true, stdout: true }) + expect(result.code).toBe(0) + }) +})