[CLI] Exit non-zero with a clear message when next typegen fails (#94241)

The `typegen` command chained `nextTypegen(...).then(() =>
process.exit(0))` with no rejection handler. When `next.config` throws
during `loadConfig`, the rejection is orphaned by the synchronous
`program.parse` and then absorbed by Next's `unhandledRejection` filter,
so the process exits `0` having written no route types. A later `tsc`
then reports `Cannot find name 'PageProps'`/`'LayoutProps'` (and missing
`*.png` modules) with nothing tying it back to typegen. Add a `.catch`
that prints an actionable message about the missing route types and
`process.exit(1)`, so `next typegen && tsc` halts on failure. Includes
an e2e test whose fixture config throws and asserts a non-zero exit with
the diagnostic.

<!-- NEXT_JS_LLM_PR -->
This commit is contained in:
Jude Gao
2026-05-30 21:47:57 +00:00
committed by GitHub
parent 2bbb67b99c
commit acb74eb551
5 changed files with 89 additions and 1 deletions
+14 -1
View File
@@ -518,7 +518,20 @@ program
// ensure process exits after typegen completes so open handles/connections
// don't cause process to hang
import('../cli/next-typegen.js').then((mod) =>
mod.nextTypegen(options, directory).then(() => process.exit(0))
mod
.nextTypegen(options, directory)
.then(() => process.exit(0))
.catch((err: unknown) => {
// Without this, a failed typegen (e.g. `next.config` throwing) is
// swallowed as an unhandled rejection that exits 0; surface it with a
// non-zero exit so `next typegen && tsc` halts instead of running
// against missing route types.
console.error(
'\n> Unexpected error while generating route types. Original error:\n'
)
console.error(err)
process.exit(1)
})
)
)
.usage('[directory] [options]')
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
@@ -0,0 +1,25 @@
// Faithful repro of the real-world trigger: `next typegen` runs with
// NODE_ENV=production (see packages/next/src/bin/next.ts), which flips a
// `warnOrError` helper to throw when a required env var is missing — exactly the
// pattern apps like vercel-site use (e.g. CONTENTFUL_DELIVERY_TOKEN).
//
// In dev (e.g. during test harness setup) it only warns, so setup is unaffected;
// during `next typegen` it throws, taking config loading — and therefore route
// type generation — down with it.
const isDev = process.env.NODE_ENV !== 'production'
const warnOrError = isDev
? console.warn
: (msg) => {
throw new Error(msg)
}
if (!process.env.SOME_REQUIRED_TOKEN) {
warnOrError('SOME_REQUIRED_TOKEN is missing from env')
}
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}
module.exports = nextConfig
@@ -0,0 +1,39 @@
import { nextTestSetup } from 'e2e-utils'
import { runNextCommand } from 'next-test-utils'
describe('typegen error diagnostic', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
// We drive `next typegen` manually; no server needed.
skipStart: true,
})
if (skipped) {
return
}
it('fails loudly with an actionable message when route types cannot be generated', async () => {
// `next typegen` runs with NODE_ENV=production, which makes the fixture's
// next.config.js throw on the missing SOME_REQUIRED_TOKEN (the same shape as
// a real app's env assertion). typegen therefore cannot generate route types.
const { code, stdout, stderr } = await runNextCommand(
['typegen', next.testDir],
{ stderr: true, stdout: true }
)
const output = stdout + stderr
// Regression: `next typegen` used to swallow the failure (exit code 0) and
// write no route types, leaving the user with cryptic
// `Cannot find name 'PageProps'/'LayoutProps'` errors from a later `tsc` run.
// It must instead fail so `next typegen && tsc` chains halt.
expect(code).not.toBe(0)
// The message must connect the failure to route-type generation, so the user
// understands why PageProps/LayoutProps go missing.
expect(output).toMatch(/route types/i)
// ...and still surface the underlying cause.
expect(output).toContain('SOME_REQUIRED_TOKEN is missing from env')
})
})