fix(config): keep packages externalized when loading TypeScript next.config#1710
Conversation
…config PR #1701 added `resolve.noExternal: true` while loading a TypeScript next.config so a baseUrl-local file could shadow an installed package of the same name. That de-externalizes every installed package and forces it through Vite's module runner, which breaks CJS config plugins that call `require` / `require.resolve` at runtime — e.g. `@next/mdx`, which failed the app-router-playground production build with "require is not defined". This was not caught pre-merge because deploy-examples is skipped for fork PRs. Drop the blanket `noExternal: true`. Installed packages stay externalized (so CJS config plugins keep working), and the native Vite 8 `resolve.tsconfigPaths` resolver still resolves baseUrl-local bare imports that have no installed package of the same name (the real Next.js tsconfig-extends fixture). The only behavior we give up is shadowing an installed package with a baseUrl-local file of the same name, which no Next.js fixture exercises. - Update the shadowing test to assert the installed package wins. - Add a regression test for an @next/mdx-style CJS plugin that calls `require.resolve` at runtime (red before this change). - Document the package-precedence behavior in the README limitations entry.
|
/bigbonk review |
commit: |
|
There was a problem hiding this comment.
Review: fix(config) keep packages externalized when loading TypeScript next.config
Verdict: Looks good. Correct, well-tested fix that restores Next.js parity.
I verified the change end-to-end:
Root cause and fix are sound
The diff drops the noExternal: true block added in #1701. That option de-externalized every installed package and forced it through Vite's module runner, which doesn't provide require/require.resolve — so CJS config factories like @next/mdx threw require is not defined at runtime. Keeping packages externalized is the right call, and resolve.tsconfigPaths still resolves the actual Next.js tsconfig-extends fixture case (a baseUrl-local file with no installed package of the same name).
Verified the regression claims locally
- On
main(withnoExternal: true), the newrequire.resolveregression test fails with exactlyReferenceError: require is not definedatfake-mdx/index.js:6— the reported deploy failure. ✅ red before - On the PR branch, the same test passes. ✅ green after
- The updated shadowing test confirms the documented matrix change:
mainreturnslocal, PR returnspackage. The behavior change (installed package wins over baseUrl-local file) is real, intentional, and accurately documented. - Full
tests/next-config.test.ts: 167 pass. vp checkon both changed files: clean (format, lint, types).
Behavior change is acceptable and documented
The only regression is losing the ability to shadow an installed package with a baseUrl-local file of the same name. As the PR notes, no Next.js fixture exercises that case — it was an extra invariant introduced in #1701. The README limitation entry and inline comments accurately describe the new precedence (package wins). This matches Next.js behavior (their SWC pipeline externalizes node_modules too).
Test quality
- The new regression test reproduces the exact failure mode (
require.resolveinside a CJS factory invoked at config-load time), not a trivialmodule.exportsstring — this directly closes the gap that let #1701 slip through. - Existing CJS/ESM package import coverage (line 664+) still passes, confirming no collateral regression.
Minor / non-blocking
- None. The comments in
next-config.ts(lines 832-835, 852-859) are updated to match the new behavior and read clearly.
Nice tight fix with a genuine red/green regression test.
|
Review posted to PR #1710. SummaryI reviewed PR #1710 ( What the PR does: Removes the Verification I performed:
No blocking issues found. The fix is correct, minimal, and backed by a meaningful regression test that closes the gap which let #1701 slip through. |
Summary
Fixes a production-build regression introduced by #1701. The
app-router-playgroundexample (and any app whosenext.config.tsimports a CJS config plugin) fails to build with:See the failing deploy: https://github.com/cloudflare/vinext/actions/runs/26746903446/job/78825508039
Root cause
#1701 added
resolve.noExternal: truewhile loading a TypeScriptnext.configso that abaseUrl-local file could shadow an installed package of the same name.noExternal: truede-externalizes every installed package and forces it through Vite's module runner. CJS config plugins like@next/mdxcallrequire/require.resolveat runtime (inside the returned config factory), and the runner doesn't providerequirefor those de-externalized modules — so they throwrequire is not defined.This wasn't caught before merge because
deploy-examplesis skipped for fork PRs, and the added "imports ESM and CommonJS packages" test only used trivialmodule.exports = "..."packages that never callrequireat runtime.Fix
Drop the blanket
noExternal: true. Installed packages stay externalized (so CJS config plugins keep working), and the native Vite 8resolve.tsconfigPathsresolver still resolvesbaseUrl-local bare imports that have no installed package of the same name — which is the actual Next.jstsconfig-extendsfixture behavior.The only behavior we give up is shadowing an installed package with a
baseUrl-local file of the same name. No Next.js fixture exercises that case; it was an extra invariant added in #1701.Behavior matrix
next.config.tsimportsbar, only localbar.tsexistsnext.config.tsimportsbar, only packagebarexistsnext.config.tsimportsbar, both localbar.tsand packagebarexistnext.config.tsimports a CJS plugin that callsrequire.resolveat runtime (@next/mdx)Tests
@next/mdx-style CJS plugin that callsrequire.resolveat runtime (verified red before this change, green after).Validation
vp test run tests/next-config.test.ts— 167 passvp buildinexamples/app-router-playground— builds successfully (was failing onmain)