Skip to content

Derive tsconfig type globs from distDirRoot instead of NODE_ENV - #98945

Open
joshuatownsend wants to merge 1 commit into
vercel:canaryfrom
joshuatownsend:fix/tsconfig-dev-types-glob-from-distdir-root
Open

joshuatownsend wants to merge 1 commit into
vercel:canaryfrom
joshuatownsend:fix/tsconfig-dev-types-glob-from-distdir-root

Conversation

@joshuatownsend

Copy link
Copy Markdown

What

getTypeDefinitionGlobPatterns emits both {distDir}/types and {distDir}/dev/types so switching between dev and build doesn't churn tsconfig.json. Which one to derive by stripping /dev and which by appending it was decided from process.env.NODE_ENV.

But distDir gets its /dev from the build phaseserver/config.ts:

if (phase === PHASE_DEVELOPMENT_SERVER) {
  result.distDir = join(result.distDir, 'dev')
}

The two signals disagree whenever next dev runs with NODE_ENV set to anything other than development. The non-development branch then appends /dev to a path that already ends in it, and next dev rewrites the user's tsconfig.json on every start:

- include was updated to add '.next/dev/dev/types/**/*.ts'

.next/dev/dev is never created, so the glob matches nothing — but the file is modified every run, and reverting it just brings it back next time.

Why it happens silently

NODE_ENV=test is not exotic. It's on the CLI's own standard list (bin/next.ts), bin/next.ts preserves an already-set value rather than overriding it for dev, and Jest/Vitest set it while CI often exports it for the whole job. Because test is "standard", the non-standard-env warning never fires.

With a configured distDir that itself ends in dev, the same path yields a third segment — build/dev/dev/dev/types.

The fix

distDirRoot is the configured distDir before the phase appends to it, and it's already on the config object. Both patterns follow from it directly, so the function no longer needs to know its phase and the branch goes away entirely:

return [`${rootPosix}/types/**/*.ts`, `${rootPosix}/dev/types/**/*.ts`]

Callers pass nextConfig.distDirRoot. Outside the development phase it equals distDir, so build, next typegen and next test are unchanged.

Why not distDir.endsWith('/dev') — the obvious one-liner — it can't distinguish a suffix the phase added from one the user configured, and would strip distDir: 'build/dev' down to build/types during a build. That's why this threads the root through instead of guessing from the path.

Verification

I checked the new implementation against every combination of distDirRoot × phase × NODE_ENV I could construct — including custom distDir values ending in dev — and it produces {root}/types and {root}/dev/types in all of them. The current code is correct only where NODE_ENV happens to agree with the phase.

getTypeDefinitionGlobPatterns had no unit tests; this adds type-paths.test.ts covering the pattern pair, independence from NODE_ENV, and the distDir-ends-in-dev case.

The existing writeConfigurationDefaults.test.ts is unaffected: it passes distDir: '.next', which is already root-shaped, so old and new agree.

One thing I could not do: I don't have the monorepo built locally, so while I verified the pure-function logic exhaustively, the plumbing across the other five files is only verified by reading. Happy to adjust whatever CI flags.

Not included

getDevTypesPath in the same file also reads NODE_ENV, but only to decide whether to filter dev types out of a build, and its caller isn't reached on the next dev path. It has the same latent mismatch and probably wants the same treatment, but I've left it out to keep this focused.

Context

I originally reported this as #98939, which was auto-closed for not having a reproduction repo link. It reproduces on a stock create-next-app:

npx create-next-app@latest repro --ts --app
cd repro
NODE_ENV=test npx next dev

Verified on a clean scaffold with next@16.3.5, resetting tsconfig.json from git before each run:

NODE_ENV added to include
test .next/dev/dev/types/**/*.ts
development (nothing)
unset (nothing)

Unset is fine because bin/next.ts defaults it to development for dev. Present in 16.3.4, 16.3.5 and 16.4.0-canary.36.

`getTypeDefinitionGlobPatterns` emits both `{distDir}/types` and
`{distDir}/dev/types` so that switching between dev and build does not churn
tsconfig. Which of the two to derive by stripping and which by appending was
decided by `process.env.NODE_ENV === 'development'`.

But `distDir` is given its "/dev" suffix by the build *phase*, in
server/config.ts:

    if (phase === PHASE_DEVELOPMENT_SERVER) {
      result.distDir = join(result.distDir, 'dev')
    }

The two disagree whenever `next dev` runs with NODE_ENV set to something
other than "development". That is not exotic: "test" is on the CLI's standard
list, `bin/next.ts` preserves an already-set value, and test runners and CI
commonly export it. The non-development branch then appends "/dev" to a path
that already ends in it, and `next dev` rewrites the user's tsconfig.json on
every start to add an inert include:

    - include was updated to add '.next/dev/dev/types/**/*.ts'

With a configured `distDir` that itself ends in "dev" the same path produces
a third segment, e.g. `build/dev/dev/dev/types`.

`distDirRoot` is the configured value before the phase appends to it, and it
is already on the config. Both patterns follow from it directly -- `/types`
and `/dev/types` -- so the function no longer needs to know its phase and the
branch goes away. Callers pass `nextConfig.distDirRoot`; outside the
development phase it is equal to `distDir`, so nothing changes there.

Testing `endsWith('/dev')` on `distDir` instead would be wrong: it cannot
tell a suffix the phase added from one the user configured, and would strip a
`distDir: 'build/dev'` down to `build/types` during a build.

`getDevTypesPath` in the same file reads NODE_ENV too, but only to decide
whether to filter dev types out of a build, and its callers are not reached
on the `next dev` path. Left alone here.

Adds unit tests for the glob patterns, which had none.
Copilot AI lite review requested due to automatic review settings September 19, 2026 20:26

Copilot AI left a comment

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.

Copilot review overview

🟢 Approval recommended

The root path is consistently propagated and the added tests cover the affected path-generation cases.

Review effort: Lite
Findings: None

What changed in this PR

Fixes phase/environment mismatches when generating TypeScript include globs by deriving both paths from distDirRoot.

Changes:

  • Threads distDirRoot through all TypeScript setup callers.
  • Removes NODE_ENV-based path selection.
  • Adds coverage for environment independence and custom distDir values.
File Description
type-paths.ts Derives stable type globs from the configured root.
type-paths.test.ts Tests glob generation behavior.
verify-typescript-setup.ts Accepts and forwards distDirRoot.
writeConfigurationDefaults.ts Uses the root for tsconfig includes.
setup-dev-bundler.ts Passes the root during development setup.
next-typegen.ts Passes the root to type setup.
next-test.ts Passes the root to type setup.
build/​type-check.ts Propagates the root through build type checking.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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.

2 participants