Skip to content

build: detect instrumentation/middleware/proxy with multi-segment pageExtensions#92934

Open
yogeshwaran-c wants to merge 1 commit intovercel:canaryfrom
yogeshwaran-c:fix/instrumentation-multi-segment-extension
Open

build: detect instrumentation/middleware/proxy with multi-segment pageExtensions#92934
yogeshwaran-c wants to merge 1 commit intovercel:canaryfrom
yogeshwaran-c:fix/instrumentation-multi-segment-extension

Conversation

@yogeshwaran-c
Copy link
Copy Markdown

What?

Make next build detect instrumentation, middleware, and proxy files when the user configures a multi-segment pageExtensions entry (e.g. 'universal.ts') and names their hook accordingly:

// next.config.js
module.exports = {
  pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'universal.ts', 'universal.tsx'],
}
// src/instrumentation.universal.ts
export function register() { /* ... */ }

Today this hook is silently dropped — the file is never compiled to .next/server/instrumentation.js and register() never runs. Same for middleware.universal.ts and proxy.universal.ts.

Why?

packages/next/src/build/index.ts builds three regexes from config.pageExtensions and uses them to filter root-level files into rootPaths. That part works — instrumentation.universal.ts correctly survives the filter (the unescaped . in the extension list is treated as \.|., which still matches the literal . in the filename).

The bug is in the bucketing loop right after, which iterates rootPaths and assigns each match to one of the three target paths via:

const { name: fileBaseName } = path.parse(rootPath)
if (fileBaseName === INSTRUMENTATION_HOOK_FILENAME) instrumentationHookFilePath = rootPath

path.parse('/src/instrumentation.universal.ts').name is 'instrumentation.universal' — Node only strips the last extension. So the equality check fails and the file is dropped on the floor. The filter said yes, the bucket said no.

The same pattern applies to MIDDLEWARE_FILENAME and PROXY_FILENAME, so any user with proxy.<custom>.ts / middleware.<custom>.ts hits the identical bug.

How?

Reuse the three detection regexes that already filter rootPaths to also assign each matched file to its bucket, comparing against path.parse(rootPath).base (the full filename including extension). The filter and the bucketing now agree on the same matching rule:

if (middlewareDetectionRegExp.test(fileBase))            middlewareFilePath = rootPath
else if (proxyDetectionRegExp.test(fileBase))            proxyFilePath = rootPath
else if (instrumentationHookDetectionRegExp.test(fileBase)) instrumentationHookFilePath = rootPath

Behavior with the default pageExtensions (['ts', 'tsx', 'js', 'jsx', 'mts', 'mjs', ...]) is unchanged — those are all single-segment, and a file named instrumentation.ts matches the regex and lands in the bucket exactly as before.

Scope

This fixes the detection gap reported as the primary issue (instrumentation never registered with custom multi-segment pageExtensions). The reporter also notes a secondary observation in their case 3 (adding proxy.universal.ts makes instrumentation register but with NEXT_RUNTIME=edge). That smells like a separate downstream runtime-routing issue and is out of scope here — once detection works correctly with this PR, that scenario is worth re-evaluating against canary.

Test plan

Added test/integration/instrumentation-page-extensions/:

  • Fixture: project with pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'universal.ts', 'universal.tsx'], src/instrumentation.universal.ts exporting register(), and a minimal pages/index.tsx.
  • Test: runs nextBuild, asserts exit code 0 and that .next/server/instrumentation.js exists — the latter only happens if detection succeeded.
  • Webpack-only (gated on IS_TURBOPACK_TEST), mirroring the existing build-trace-extra-entries integration suite.

Verified locally on Windows + webpack: test fails on canary without the fix and passes after it.

Fixes #92342

…eExtensions

When `next.config.js` declares a multi-segment custom `pageExtensions`
entry such as `'universal.ts'` and the user names their hook
`src/instrumentation.universal.ts`, the regex that filters root files
correctly matches the file (because `.` in the extension list is
treated as `\.|.` — an "any char" wildcard — which still happens to
hit `universal.ts`), but the per-file bucketing loop immediately
dropped it. The bucketing used `path.parse(rootPath).name`, which
strips only the *last* extension, so `instrumentation.universal.ts`
collapsed to `'instrumentation.universal'` — not equal to
`INSTRUMENTATION_HOOK_FILENAME` (`'instrumentation'`) — and
`instrumentationHookFilePath` stayed `undefined`. The hook was never
registered, never compiled to `.next/server/instrumentation.js`, and
`register()` silently never ran.

The same shape of bug applied to `middleware.universal.ts` and
`proxy.universal.ts`, since all three buckets used the same
`name === FILENAME_CONSTANT` check.

Reuse the three detection regexes that already filter `rootPaths` to
also assign the matched files to their convention buckets, comparing
against `path.parse(rootPath).base` (the full filename including
extension). The filter and the bucketing now agree on the same
matching rule, and a multi-segment extension in `pageExtensions` no
longer silently drops the file.

Added an integration test under
`test/integration/instrumentation-page-extensions/` that configures
`pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'universal.ts',
'universal.tsx']`, places the hook at
`src/instrumentation.universal.ts`, and asserts that
`.next/server/instrumentation.js` is produced — which only happens if
detection succeeded. Webpack-only (gated on `IS_TURBOPACK_TEST`)
mirroring the existing `build-trace-extra-entries` integration suite.

Fixes vercel#92342
@yogeshwaran-c yogeshwaran-c marked this pull request as ready for review April 17, 2026 10:44
@nextjs-bot
Copy link
Copy Markdown
Contributor

Allow CI Workflow Run

  • approve CI run for commit: e62e919

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom nextConfig.pageExtensions breaks instrumentation.ts

2 participants