diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index de697e95455d..da2e9274a13c 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1251,22 +1251,25 @@ export default async function build( let middlewareFilePath: string | undefined for (const rootPath of rootPaths) { - const { name: fileBaseName, dir: fileDir } = path.parse(rootPath) + const { base: fileBase, dir: fileDir } = path.parse(rootPath) const normalizedFileDir = normalizePathSep(fileDir) const isAtConventionLevel = normalizedFileDir === '/' || normalizedFileDir === '/src' - if (isAtConventionLevel && fileBaseName === MIDDLEWARE_FILENAME) { + if (!isAtConventionLevel) continue + + // Use the same regexes that filtered `rootPaths` above to assign each + // matched file to its convention bucket. Comparing against the bare + // `path.parse(...).name` would miss files using a multi-segment + // custom `pageExtensions` entry such as `'universal.ts'`, because + // `path.parse('/instrumentation.universal.ts').name` is + // `'instrumentation.universal'` — not `INSTRUMENTATION_HOOK_FILENAME`. + if (middlewareDetectionRegExp.test(fileBase)) { middlewareFilePath = rootPath - } - if (isAtConventionLevel && fileBaseName === PROXY_FILENAME) { + } else if (proxyDetectionRegExp.test(fileBase)) { proxyFilePath = rootPath - } - if ( - isAtConventionLevel && - fileBaseName === INSTRUMENTATION_HOOK_FILENAME - ) { + } else if (instrumentationHookDetectionRegExp.test(fileBase)) { instrumentationHookFilePath = rootPath } } diff --git a/test/integration/instrumentation-page-extensions/app/next.config.js b/test/integration/instrumentation-page-extensions/app/next.config.js new file mode 100644 index 000000000000..d9b311567596 --- /dev/null +++ b/test/integration/instrumentation-page-extensions/app/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'universal.ts', 'universal.tsx'], +} diff --git a/test/integration/instrumentation-page-extensions/app/src/instrumentation.universal.ts b/test/integration/instrumentation-page-extensions/app/src/instrumentation.universal.ts new file mode 100644 index 000000000000..cd2d2a2f3e48 --- /dev/null +++ b/test/integration/instrumentation-page-extensions/app/src/instrumentation.universal.ts @@ -0,0 +1,5 @@ +export function register() { + // Marker so the test can confirm the build picked the hook up; in + // practice, just compiling `instrumentation.js` into `.next/server/` + // is the signal we assert on. +} diff --git a/test/integration/instrumentation-page-extensions/app/src/pages/index.tsx b/test/integration/instrumentation-page-extensions/app/src/pages/index.tsx new file mode 100644 index 000000000000..051969f4dd15 --- /dev/null +++ b/test/integration/instrumentation-page-extensions/app/src/pages/index.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
hello
+} diff --git a/test/integration/instrumentation-page-extensions/app/tsconfig.json b/test/integration/instrumentation-page-extensions/app/tsconfig.json new file mode 100644 index 000000000000..4ef6da7de289 --- /dev/null +++ b/test/integration/instrumentation-page-extensions/app/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx" + }, + "include": ["next-env.d.ts", "**/*.mts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/test/integration/instrumentation-page-extensions/test/index.test.ts b/test/integration/instrumentation-page-extensions/test/index.test.ts new file mode 100644 index 000000000000..1e01eca45ef9 --- /dev/null +++ b/test/integration/instrumentation-page-extensions/test/index.test.ts @@ -0,0 +1,36 @@ +/* eslint-env jest */ + +import { existsSync } from 'fs' +import { join } from 'path' +import { nextBuild } from 'next-test-utils' + +const appDir = join(__dirname, '../app') + +describe('instrumentation hook detection with custom multi-segment pageExtensions', () => { + ;(process.env.IS_TURBOPACK_TEST ? describe.skip : describe)( + 'production mode (webpack)', + () => { + it('compiles src/instrumentation.universal.ts when it matches pageExtensions', async () => { + const result = await nextBuild(appDir, undefined, { + cwd: appDir, + stderr: true, + stdout: true, + }) + expect(result.code).toBe(0) + + // If the hook was detected, the build emits the compiled + // instrumentation entry to `.next/server/instrumentation.js`. + // Before the fix for #92342, custom multi-segment pageExtensions + // (e.g. `universal.ts`) caused the file to be matched by the + // detection regex but then dropped because + // `path.parse('instrumentation.universal.ts').name` is + // `'instrumentation.universal'` — not `'instrumentation'` — so + // the equality check against `INSTRUMENTATION_HOOK_FILENAME` + // failed and `instrumentation.js` was never produced. + expect( + existsSync(join(appDir, '.next/server/instrumentation.js')) + ).toBe(true) + }) + } + ) +})