Skip to content

fix(middleware): shim __filename and __dirname for user proxy/middleware (#1546)#1650

Open
james-elicx wants to merge 1 commit into
mainfrom
fix/issue-1546-proxy-nfc-traced
Open

fix(middleware): shim __filename and __dirname for user proxy/middleware (#1546)#1650
james-elicx wants to merge 1 commit into
mainfrom
fix/issue-1546-proxy-nfc-traced

Conversation

@james-elicx
Copy link
Copy Markdown
Member

Summary

  • User proxy.ts / middleware.ts files that reference __filename or __dirname threw ReferenceError at runtime because vinext bundles them as ESM. The middleware runtime caught the error and returned a 500, which the upstream proxy-nfc-traced deploy test sees as an empty body when following the /home → / redirect into the crashing branch.
  • Add a Vite plugin (createMiddlewareCjsGlobalsPlugin) that injects const __filename / const __dirname at the top of the resolved middleware/proxy module's source, matching Next.js's webpack template behaviour. This is the same pattern already used by cjsGlobalsInjectorPlugin for next.config.ts.
  • Skip the corresponding vinext check warning for the shimmed middleware/proxy file.

Fixes #1546.

Test plan

  • New tests/proxy-nfc-traced.test.ts builds a minimal app + proxy.ts fixture, then asserts both that the bundled output contains the injected __filename / __dirname const declarations (the structural guard — vitest itself provides __filename, masking the production failure) and that the HTTP responses for /home (with redirect follow) and / render hello world.
  • pnpm test tests/proxy-nfc-traced.test.ts — 6/6 pass
  • pnpm test tests/app-router.test.ts — 331/331 pass
  • pnpm test tests/prerender.test.ts — 77/77 pass
  • pnpm test tests/pages-router.test.ts — 252/252 pass
  • pnpm test tests/middleware-runtime.test.ts tests/middleware-runtime-trailing-slash.test.ts tests/middleware-server-only.test.ts tests/shims.test.ts — 1016/1016 pass
  • pnpm test tests/check.test.ts — 99/99 pass
  • pnpm run check — clean (format + lint + types)
  • Manual: vinext build --prerender-all && vinext start against the upstream fixture now serves <p>hello world</p> for GET /home (previously returned Internal Server Error)

…are (#1546)

User middleware/proxy files referenced `__filename` and `__dirname` throw
`ReferenceError` because vinext bundles them as ESM. The middleware runtime
catches the error and returns a 500, which surfaces as an empty body to any
client request that touches the offending branch (in `proxy-nfc-traced` the
client follows the `/home → /` redirect into the crashing fall-through).

Mirror Next.js's webpack template by injecting `const __filename` /
`__dirname` shims at build time for the resolved middleware/proxy file,
using the same Vite-plugin pattern already used for `next.config.ts`. Skip
the corresponding `vinext check` warning for that file too.
@james-elicx
Copy link
Copy Markdown
Member Author

/bigbonk review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1650

commit: 72e4422

@github-actions
Copy link
Copy Markdown

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
web preview production

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bigbonk review

Clean, well-scoped fix. The plugin implementation is correct, the check.ts exclusion is properly placed, and the test strategy (structural guards on bundled output + e2e) is thoughtful. A few observations and one minor concern:

What's good

  • The continue placement in check.ts is correct. It sits after the ViewTransition check (line 644) but before the CJS globals scan (line 657), so middleware/proxy files still get checked for ViewTransition imports — only the __dirname/__filename warning is suppressed. This is the right scoping.

  • Dev/prod parity is not a concern here. The plugin is a Vite transform hook that runs during bundling, so it applies uniformly to dev, prod, and Cloudflare Workers builds. The middleware path comes from the same findMiddlewareFile call in all cases.

  • The lazy getter pattern is correct. middlewarePath is set during the config hook (index.ts:1097), and refreshTarget() runs in buildStart, which fires after config — so the closure always reads a populated value.

  • Test quality is strong. The explicit acknowledgment that vitest masks the production failure mode, combined with the structural bundle assertions (extractProxyRegion + regex match on the emitted const __filename), is the right way to guard this regression. The sanity check that console.log(__filename) is still present prevents the structural guards from becoming vacuously true if a future Rolldown change elides the reference.

Observations

  1. isShimmedMiddlewareFile regex is broader than findMiddlewareFile. The regex accepts .cjs, .mjs, .cts, .mts extensions via [cm]?[jt]sx?, but findMiddlewareFile only searches for extensions from DEFAULT_PAGE_EXTENSIONS (tsx, ts, jsx, js) unless the user has custom pageExtensions. This is a harmless false-negative suppression (worst case: we skip a CJS warning for a .mjs middleware file that doesn't actually exist), but worth noting for future readers. If pageExtensions is ever expanded to include .mts/.cts variants upstream, this regex already handles it.

  2. map: null is acceptable. Both this plugin and the existing cjsGlobalsInjectorPlugin for next.config.ts return map: null. Since the preamble is only 2 lines, error stack offsets shift by +2 lines in the middleware file. In practice, middleware files are small and the offset is negligible. This is consistent with the existing pattern.

  3. Consolidation with cjsGlobalsInjectorPlugin. The PR description notes the similarity. The two plugins have different scopes — the next-config version also handles require, module, exports, and module.exports reassignment, which middleware doesn't need. Keeping them separate is the right call for now; the middleware version is a strict subset. A future refactor could extract the shared __filename/__dirname injection logic into a helper, but that's not blocking.

  4. safeRealpath is duplicated. This small utility now exists in both config/next-config.ts and plugins/middleware-cjs-globals.ts. Not a blocking concern for a 3-line function, but if a third copy appears it should be extracted.

* Issue: https://github.com/cloudflare/vinext/issues/1546
*/

const CJS_GLOBAL_SCAN_REGEX = /\b(?:__filename|__dirname)\b/;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The CJS_GLOBAL_SCAN_REGEX here uses a non-global regex (no g flag), which is correct for the .test() usage on line 77 — you only need to know whether a match exists, not iterate. Good. The check.ts version uses the g flag because it iterates with .exec(). Worth noting for anyone comparing the two.


return {
code: preamble + code,
map: null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both this plugin and cjsGlobalsInjectorPlugin in next-config.ts return map: null. For a 2-line preamble this is fine in practice, but if the preamble ever grows (or if someone reports confusing stack traces in middleware), generating a proper source map with MagicString would be straightforward. Not blocking.

*/
function isShimmedMiddlewareFile(relPath: string): boolean {
const normalized = relPath.split(path.sep).join("/");
return /^(?:src\/)?(?:middleware|proxy)\.[cm]?[jt]sx?$/.test(normalized);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex [cm]?[jt]sx? accepts extensions like .mts, .cts, .mjs, .cjs that findMiddlewareFile does not currently search for (it uses DEFAULT_PAGE_EXTENSIONS = tsx, ts, jsx, js). This is a safe over-match — suppressing a warning for a file that the shim plugin would also target if found — but if precision matters, you could tighten this to \.[jt]sx?$ to match the default extensions exactly. Fine either way.

throw new Error("Could not find matching endregion for proxy.ts");
}
return remainder.slice(0, endRelative);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractProxyRegion relies on Rolldown emitting //#region markers. This is a Rolldown implementation detail that could change across versions. If a future Rolldown update removes or changes the region comment format, these structural tests will throw rather than silently pass — which is the right failure mode (explicit breakage, not false green). Good defensive design.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Both the summary review and inline comments have been posted. Here's a summary of the review:


Review posted on PR #1650 with 4 inline comments.

Verdict: Clean, well-scoped fix. No blocking issues found.

Key findings:

  1. continue in check.ts is correctly placed — it sits after the ViewTransition check but before the CJS globals scan, so only the __dirname/__filename warning is suppressed for middleware/proxy files.

  2. Dev/prod parity is not a concern — the plugin is a Vite transform hook that runs during bundling, applying uniformly to all build targets.

  3. Lazy getter pattern is correctmiddlewarePath is set during config hook, and refreshTarget() runs in buildStart which fires after.

  4. Minor observations (all non-blocking):

    • isShimmedMiddlewareFile regex is slightly broader than findMiddlewareFile's actual extensions (accepts .mts/.cjs etc. that aren't searched for) — harmless over-match
    • map: null is consistent with the existing cjsGlobalsInjectorPlugin pattern and acceptable for a 2-line preamble
    • safeRealpath is duplicated across two files — fine for now, extract if a third copy appears
    • extractProxyRegion relies on Rolldown region markers (implementation detail) but fails explicitly if format changes — good defensive design

github run

@james-elicx james-elicx marked this pull request as ready for review May 28, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App Router: proxy-nfc-traced route redirect produces empty content

1 participant