fix(adapter-nextjs): support next.config.mts and stop stranding the user config - #682
Open
Om-singhaI wants to merge 1 commit into
Open
fix(adapter-nextjs): support next.config.mts and stop stranding the user config#682Om-singhaI wants to merge 1 commit into
Om-singhaI wants to merge 1 commit into
Conversation
…ser config next.config.mts was rejected by overrideNextConfig. The extension switch only handled ".js", ".mjs" and ".ts", so ".mts" fell through to the default branch and threw "Unsupported file extension for Next Config". Adding the extension to the switch alone is not enough. getCustomNextConfig chose the module tail by testing for ".mjs" only, so a ".mts" file would have been given "module.exports = config;". A .mts file is always an ES module regardless of the package type field, so "module" is not defined there and the generated config would export nothing. The import specifier also differs from the ".ts" case: Next.js loads next.config.mts through Node's type stripping, which resolves ES module specifiers as written, so the ".mts" extension must be kept rather than dropped the way the ".ts" case drops it. The rename was also destructive. overrideNextConfig renamed the user's config to next.config.original with the same extension before deciding whether it could generate a replacement, and the catch only logged and rethrew. bin/build.ts then called overrideNextConfig and validateNextConfigOverride outside the try block whose finally runs restoreNextConfig. Any throw from the override step therefore escaped with no restore, leaving the project with a next.config.original file and no next config at all. That was independent of the extension: a write failure stranded the tree the same way. The config content is now generated before anything is renamed, so an unsupported extension fails while the project is still untouched, and a failure after the rename moves the original file back. The override and validation calls in bin/build.ts moved inside the try so the existing restoreNextConfig in the finally covers them.
Om-singhaI
requested review from
Yuangwang,
abhis3,
annajowang,
sjjj986 and
taeold
as code owners
August 24, 2026 02:18
Contributor
There was a problem hiding this comment.
Code Review
This pull request adds support for the .mts file extension in Next.js configuration overrides and improves error handling during the override process. Specifically, validation of the file extension is now performed before renaming the original configuration file, and a fallback mechanism has been added to restore the original configuration if the override process fails. Unit tests have also been added to verify these changes. I have no feedback to provide.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(adapter-nextjs): support next.config.mts and stop stranding the user config
Addresses issue #680.
What breaks
Building an app whose Next config is
next.config.mtsfails, and the failure takes theuser's config file with it.
overrideNextConfiginpackages/@apphosting/adapter-nextjs/src/overrides.tsswitches onthe config file extension. It handles
.js,.mjsand.ts;.mtsfalls through to thedefault branch and throws:
The throw happens after the function has already renamed the user's
next.config.mtstonext.config.original.mts, and the surrounding catch only logs and rethrows. Insrc/bin/build.tstheoverrideNextConfigandvalidateNextConfigOverridecalls satoutside the
tryblock whosefinallyrunsrestoreNextConfig, so the error escaped withno restore. The build left the project holding
next.config.original.mtsand nonext.config.mtsat all.That second half is not specific to
.mts. Any failure after the rename, including a writefailure, stranded the tree the same way.
Why the extension switch was not the whole fix
Two further things had to change for a generated
.mtsconfig to actually load.Module tail.
getCustomNextConfigselected the export form by testing for.mjsonly,so a
.mtsfile would have receivedmodule.exports = config;. A.mtsfile is always anES module regardless of the package
typefield, somoduleis not defined in it. Iconfirmed this on Node v25.6.1:
The generated config would have had no default export, and
validateNextConfigOverridewould have thrown on it anyway.
Import specifier. The
.tscase strips the extension entirely, producingimport originalConfig from './next.config.original';. Copying that for.mtsdoes notwork. Next.js discovers
next.config.mtsonly when Node's type stripping is available, andloads it through Node, which resolves ES module specifiers as written. I checked all three
candidate specifiers against a real
.mtsfile on Node v25.6.1:'./next.config.original'(mirrors the.tscase)'./next.config.original.mjs''./next.config.original.mts'So
.mtskeeps its extension. It shares the import statement with.mjsbut, unlike.ts, must not have the extension dropped.What the fix does
packages/@apphosting/adapter-nextjs/src/overrides.ts.mtsto the extension switch, sharing the.mjsimport statement so the specifierstays fully specified. A comment records why it must not follow the
.tscase.export default config;for.mtsas well as.mjs.extension now fails while the user's project is still untouched.
restoreNextConfigif the rename succeededbut the write did not, guarded so it cannot clobber a live config with a stale backup from
an earlier crashed run.
packages/@apphosting/adapter-nextjs/src/bin/build.tsoverrideNextConfigandvalidateNextConfigOverridecalls inside the existingtry, so therestoreNextConfigalready in thefinallycovers a validation failure too.Doc comments listing the supported extensions were updated to include
mts.Testing
Two regression tests were added to
src/overrides.spec.ts, in the same string and fsassertion style as the existing cases:
should set images.unoptimized to true - TypeScript ES Modulescovers the.mtsoutput,asserting both the fully specified import and the
export default config;tail.should leave the original config in place when the override failsdrives a failingoverride and asserts the user's config still exists with its original contents and that no
backup file was left behind. It uses
.cjs, which is unsupported before and after thischange, so it stays a real test of the failure path.
Both tests were written first and observed failing against unmodified source:
The ENOENT in the second test is the bug itself: the config the test wrote had been renamed
away and never restored.
With the fix,
npm run test:unitinpackages/@apphosting/adapter-nextjsreports30 passing, up from the 28 on
main, with no regressions.npm run lintreports 0errors, and the changed files pass
prettier --check.Beyond the unit tests I generated a real override with the built adapter and loaded it the
way Next.js does. The result loads cleanly, applies the App Hosting override and preserves
the user's own settings:
Note on the runtime gate
Next.js only lists
next.config.mtsamong its candidate config files whenprocess.features.typescriptis set, that is when Node has type stripping available, whichis the default on Node 22.18 and later and on Node 24 and later. The bug is real but only
reachable on such a runtime. The added unit tests call
overrideNextConfigdirectly, sothey exercise the adapter's own logic and do not depend on that gate.
Why there is no end to end scenario
e2e/config-override-test-cases.yamlalready drives one scenario per config flavour,including
next.config.tsandnext.config.mjs, so anext.config.mtsentry would be thenatural home for end to end coverage. It cannot go in yet.
starters/nextjs/basicpinsnext@15.0.5, andnext.config.mtsfirst appears in Next'sCONFIG_FILESinv16.0.0, soon the pinned starter the file is never discovered and the scenario would fail for a reason
unrelated to this change. CI also runs the suite on Node 18, where
process.features.typescriptis unset and the gate is closed. Adding that scenario meansbumping the starter to Next 16 first, which is worth doing separately.
The
.mtsoutput was instead checked by hand against Node itself, which is what Next 16delegates to for this file: it loads
next.config.mtswith a plainawait import(pathToFileURL(path).href)rather than throughtranspileConfig, the way ithandles
next.config.ts.