Skip to content

Commit b6673b4

Browse files
committed
Fix stale dev types causing build failure after route deletion
1 parent 0daf667 commit b6673b4

File tree

8 files changed

+86
-10
lines changed

8 files changed

+86
-10
lines changed

apps/bundle-analyzer/next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import './.next/types/routes.d.ts'
3+
import "./.next/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

packages/next/src/lib/typescript/runTypeCheck.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,25 @@ export async function runTypeCheck(
2020
distDir: string,
2121
tsConfigPath: string,
2222
cacheDir?: string,
23-
isAppDirEnabled?: boolean
23+
isAppDirEnabled?: boolean,
24+
isolatedDevBuild?: boolean
2425
): Promise<TypeCheckResult> {
2526
const effectiveConfiguration = await getTypeScriptConfiguration(
2627
typescript,
2728
tsConfigPath
2829
)
2930

30-
if (effectiveConfiguration.fileNames.length < 1) {
31+
// When isolatedDevBuild is enabled, tsconfig includes both .next/types and
32+
// .next/dev/types to avoid config churn between dev/build modes. During build,
33+
// we filter out .next/dev/types files to prevent stale dev types from causing
34+
// errors when routes have been deleted since the last dev session.
35+
let fileNames = effectiveConfiguration.fileNames
36+
if (isolatedDevBuild !== false) {
37+
const devTypesPattern = /[/\\]\.next[/\\]dev[/\\]types[/\\]/
38+
fileNames = fileNames.filter((fileName) => !devTypesPattern.test(fileName))
39+
}
40+
41+
if (fileNames.length < 1) {
3142
return {
3243
hasWarnings: false,
3344
inputFilesCount: 0,
@@ -57,7 +68,7 @@ export async function runTypeCheck(
5768
}
5869
incremental = true
5970
program = typescript.createIncrementalProgram({
60-
rootNames: effectiveConfiguration.fileNames,
71+
rootNames: fileNames,
6172
options: {
6273
...options,
6374
composite: false,
@@ -66,10 +77,7 @@ export async function runTypeCheck(
6677
},
6778
})
6879
} else {
69-
program = typescript.createProgram(
70-
effectiveConfiguration.fileNames,
71-
options
72-
)
80+
program = typescript.createProgram(fileNames, options)
7381
}
7482

7583
const result = program.emit()
@@ -147,7 +155,7 @@ export async function runTypeCheck(
147155
return {
148156
hasWarnings: true,
149157
warnings,
150-
inputFilesCount: effectiveConfiguration.fileNames.length,
158+
inputFilesCount: fileNames.length,
151159
totalFilesCount: program.getSourceFiles().length,
152160
incremental,
153161
}

packages/next/src/lib/verify-typescript-setup.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ export async function verifyTypeScriptSetup({
158158
distDir,
159159
resolvedTsConfigPath,
160160
cacheDir,
161-
hasAppDir
161+
hasAppDir,
162+
isolatedDevBuild
162163
)
163164
}
164165
return { result, version: typescriptVersion }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default function RootLayout({
2+
children,
3+
}: {
4+
children: React.ReactNode
5+
}) {
6+
return (
7+
<html>
8+
<body>{children}</body>
9+
</html>
10+
)
11+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <div>Home Page</div>
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function TempPage() {
2+
return <div>Temporary Page</div>
3+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {}
5+
6+
module.exports = nextConfig
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import { nextBuild } from 'next-test-utils'
3+
4+
describe('stale-dev-types', () => {
5+
const { next, skipped } = nextTestSetup({
6+
files: __dirname,
7+
skipDeployment: true,
8+
})
9+
10+
if (skipped) {
11+
return
12+
}
13+
14+
it('should not fail build when .next/dev has stale types from deleted routes', async () => {
15+
// Step 1: Ensure dev server has generated .next/dev/types/validator.ts
16+
// The nextTestSetup in dev mode automatically starts the dev server
17+
// Wait for types to be generated
18+
await new Promise((resolve) => setTimeout(resolve, 3000))
19+
20+
// Verify .next/dev/types/validator.ts was generated with temp-route reference
21+
const validatorTs = await next.readFile('.next/dev/types/validator.ts')
22+
expect(validatorTs).toContain('/temp-route')
23+
24+
// Step 2: Stop the dev server
25+
await next.stop()
26+
27+
// Step 3: Delete the route that was included in .next/dev/types/validator.ts
28+
await next.deleteFile('app/temp-route/page.tsx')
29+
30+
// Step 4: Run build - it should NOT fail due to stale .next/dev types
31+
// The bug is that build picks up .next/dev/types/validator.ts which references
32+
// the deleted route, causing a type error
33+
const result = await nextBuild(next.testDir, [], {
34+
stdout: true,
35+
stderr: true,
36+
})
37+
38+
// Build should succeed - it should not type-check .next/dev
39+
expect(result.stdout + result.stderr).not.toContain(
40+
"Cannot find module '../../../app/temp-route/page"
41+
)
42+
expect(result.code).toBe(0)
43+
})
44+
})

0 commit comments

Comments
 (0)