From 8b288b9f11b871eef994888faec3b11788215953 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 2 Dec 2025 22:03:49 +0100 Subject: [PATCH 1/8] generate dynamic types in distdir --- packages/next/src/build/index.ts | 7 +++ packages/next/src/build/type-check.ts | 3 -- packages/next/src/cli/next-test.ts | 1 - packages/next/src/cli/next-typegen.ts | 9 +++- .../typescript/writeAppTypeDeclarations.ts | 37 +++----------- .../next/src/lib/verify-typescript-setup.ts | 8 +-- .../lib/router-utils/route-types-utils.ts | 32 ++++++++++++ .../lib/router-utils/setup-dev-bundler.ts | 8 ++- .../src/server/lib/router-utils/typegen.ts | 35 +++++++++++++ .../typescript/test/index.test.ts | 19 +++---- .../typescript/test/index.test.ts | 19 +++---- test/unit/write-app-declarations.test.ts | 49 +++++-------------- 12 files changed, 120 insertions(+), 107 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index d56f45b22e7ed..0e3abb4b93310 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -220,6 +220,7 @@ import { createRouteTypesManifest, writeRouteTypesManifest, writeValidatorFile, + writeDynamicTypesFile, } from '../server/lib/router-utils/route-types-utils' import { Lockfile } from './lockfile' import { @@ -1469,6 +1470,12 @@ export default async function build( config ) await writeValidatorFile(routeTypesManifest, validatorFilePath) + await writeDynamicTypesFile({ + distDir, + imageImportsEnabled: !config.images.disableStaticImages, + hasPagesDir: !!pagesDir, + hasAppDir: !!appDir, + }) }) // Turbopack already handles conflicting app and page routes. diff --git a/packages/next/src/build/type-check.ts b/packages/next/src/build/type-check.ts index 6fcd75e339ab2..1ec75ff16f500 100644 --- a/packages/next/src/build/type-check.ts +++ b/packages/next/src/build/type-check.ts @@ -23,7 +23,6 @@ function verifyTypeScriptSetup( intentDirs: string[], typeCheckPreflight: boolean, tsconfigPath: string | undefined, - disableStaticImages: boolean, cacheDir: string | undefined, enableWorkerThreads: boolean | undefined, hasAppDir: boolean, @@ -51,7 +50,6 @@ function verifyTypeScriptSetup( intentDirs, typeCheckPreflight, tsconfigPath, - disableStaticImages, cacheDir, hasAppDir, hasPagesDir, @@ -114,7 +112,6 @@ export async function startTypeChecking({ [pagesDir, appDir].filter(Boolean) as string[], !ignoreTypeScriptErrors, config.typescript.tsconfigPath, - config.images.disableStaticImages, cacheDir, config.experimental.workerThreads, !!appDir, diff --git a/packages/next/src/cli/next-test.ts b/packages/next/src/cli/next-test.ts index c6c4813d898ae..10a6a8bff81b6 100644 --- a/packages/next/src/cli/next-test.ts +++ b/packages/next/src/cli/next-test.ts @@ -143,7 +143,6 @@ async function runPlaywright( intentDirs: [pagesDir, appDir].filter(Boolean) as string[], typeCheckPreflight: false, tsconfigPath: nextConfig.typescript.tsconfigPath, - disableStaticImages: nextConfig.images.disableStaticImages, hasAppDir: !!appDir, hasPagesDir: !!pagesDir, isolatedDevBuild: nextConfig.experimental.isolatedDevBuild, diff --git a/packages/next/src/cli/next-typegen.ts b/packages/next/src/cli/next-typegen.ts index 632c6a185ed62..a98bb8e48fb95 100644 --- a/packages/next/src/cli/next-typegen.ts +++ b/packages/next/src/cli/next-typegen.ts @@ -29,6 +29,7 @@ import { createRouteTypesManifest, writeRouteTypesManifest, writeValidatorFile, + writeDynamicTypesFile, } from '../server/lib/router-utils/route-types-utils' import { writeCacheLifeTypes } from '../server/lib/router-utils/cache-life-type-utils' import { createValidFileMatcher } from '../server/lib/find-page-file' @@ -60,7 +61,6 @@ const nextTypegen = async ( intentDirs: [pagesDir, appDir].filter(Boolean) as string[], typeCheckPreflight: false, tsconfigPath: nextConfig.typescript.tsconfigPath, - disableStaticImages: nextConfig.images.disableStaticImages, hasAppDir: !!appDir, hasPagesDir: !!pagesDir, isolatedDevBuild: nextConfig.experimental.isolatedDevBuild, @@ -170,6 +170,13 @@ const nextTypegen = async ( await writeValidatorFile(routeTypesManifest, validatorFilePath) + await writeDynamicTypesFile({ + distDir, + imageImportsEnabled: !nextConfig.images.disableStaticImages, + hasPagesDir: !!pagesDir, + hasAppDir: !!appDir, + }) + // Generate cache-life types if cacheLife config exists const cacheLifeFilePath = join(distDir, 'types', 'cache-life.d.ts') writeCacheLifeTypes(nextConfig.cacheLife, cacheLifeFilePath) diff --git a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts index 772f9cfaa0493..9980c84224a5c 100644 --- a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts +++ b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts @@ -4,15 +4,9 @@ import { promises as fs } from 'fs' export async function writeAppTypeDeclarations({ baseDir, - distDir, - imageImportsEnabled, - hasPagesDir, hasAppDir, }: { baseDir: string - distDir: string - imageImportsEnabled: boolean - hasPagesDir: boolean hasAppDir: boolean }): Promise { // Reference `next` types @@ -38,39 +32,20 @@ export async function writeAppTypeDeclarations({ /** * "Triple-slash directives" used to create typings files for Next.js projects - * using Typescript . + * using Typescript. + * + * Dynamic types (image imports, navigation compat, routes) are generated + * in .next/types/ and included via tsconfig.json. * * @see https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html */ const directives: string[] = [ // Include the core Next.js typings. '/// ', - ] - - if (imageImportsEnabled) { - directives.push('/// ') - } - - if (hasAppDir && hasPagesDir) { - directives.push( - '/// ' - ) - } - - const routeTypesPath = path.posix.join( - distDir.replaceAll(path.win32.sep, path.posix.sep), - 'types/routes.d.ts' - ) - - // Use ESM import instead of triple-slash reference for better ESLint compatibility - directives.push(`import "./${routeTypesPath}";`) - - // Push the notice in. - directives.push( '', '// NOTE: This file should not be edited', - `// see https://nextjs.org/docs/${hasAppDir ? 'app' : 'pages'}/api-reference/config/typescript for more information.` - ) + `// see https://nextjs.org/docs/${hasAppDir ? 'app' : 'pages'}/api-reference/config/typescript for more information.`, + ] const content = directives.join(eol) + eol diff --git a/packages/next/src/lib/verify-typescript-setup.ts b/packages/next/src/lib/verify-typescript-setup.ts index bcd55423cd091..9b1d3d4081a20 100644 --- a/packages/next/src/lib/verify-typescript-setup.ts +++ b/packages/next/src/lib/verify-typescript-setup.ts @@ -40,7 +40,6 @@ export async function verifyTypeScriptSetup({ intentDirs, tsconfigPath, typeCheckPreflight, - disableStaticImages, hasAppDir, hasPagesDir, isolatedDevBuild, @@ -51,7 +50,6 @@ export async function verifyTypeScriptSetup({ tsconfigPath: string | undefined intentDirs: string[] typeCheckPreflight: boolean - disableStaticImages: boolean hasAppDir: boolean hasPagesDir: boolean isolatedDevBuild: boolean | undefined @@ -132,12 +130,10 @@ export async function verifyTypeScriptSetup({ isolatedDevBuild ) // Write out the necessary `next-env.d.ts` file to correctly register - // Next.js' types: + // Next.js' types. Dynamic types (image imports, navigation compat) are + // generated in .next/types/ separately. await writeAppTypeDeclarations({ baseDir: dir, - distDir, - imageImportsEnabled: !disableStaticImages, - hasPagesDir, hasAppDir, }) diff --git a/packages/next/src/server/lib/router-utils/route-types-utils.ts b/packages/next/src/server/lib/router-utils/route-types-utils.ts index 9a1ba32da5fc9..794b7bf1aaa17 100644 --- a/packages/next/src/server/lib/router-utils/route-types-utils.ts +++ b/packages/next/src/server/lib/router-utils/route-types-utils.ts @@ -10,6 +10,7 @@ import { generateRouteTypesFile, generateLinkTypesFile, generateValidatorFile, + generateDynamicTypesFile, } from './typegen' import { tryToParsePath } from '../../../lib/try-to-parse-path' import { @@ -382,3 +383,34 @@ export async function writeValidatorFile( await fs.promises.writeFile(filePath, generateValidatorFile(manifest)) } + +/** + * Writes the dynamic types file (.next/types/next-env.d.ts) that contains + * config-dependent type references (image imports, navigation compat, etc.) + */ +export async function writeDynamicTypesFile({ + distDir, + imageImportsEnabled, + hasPagesDir, + hasAppDir, +}: { + distDir: string + imageImportsEnabled: boolean + hasPagesDir: boolean + hasAppDir: boolean +}) { + const typesDir = path.join(distDir, 'types') + const filePath = path.join(typesDir, 'next-env.d.ts') + + if (!fs.existsSync(typesDir)) { + await fs.promises.mkdir(typesDir, { recursive: true }) + } + + const content = generateDynamicTypesFile({ + imageImportsEnabled, + hasPagesDir, + hasAppDir, + }) + + await fs.promises.writeFile(filePath, content) +} diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index af3a3a9edcd63..c203aec691f05 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -85,6 +85,7 @@ import { createRouteTypesManifest, writeRouteTypesManifest, writeValidatorFile, + writeDynamicTypesFile, } from './route-types-utils' import { writeCacheLifeTypes } from './cache-life-type-utils' import { isParallelRouteSegment } from '../../../shared/lib/segment' @@ -146,7 +147,6 @@ async function verifyTypeScript(opts: SetupOpts) { intentDirs: [opts.pagesDir, opts.appDir].filter(Boolean) as string[], typeCheckPreflight: false, tsconfigPath: opts.nextConfig.typescript.tsconfigPath, - disableStaticImages: opts.nextConfig.images.disableStaticImages, hasAppDir: !!opts.appDir, hasPagesDir: !!opts.pagesDir, isolatedDevBuild: opts.nextConfig.experimental.isolatedDevBuild, @@ -260,6 +260,12 @@ async function startWatcher( path.join(distTypesDir, 'routes.d.ts'), opts.nextConfig ) + await writeDynamicTypesFile({ + distDir, + imageImportsEnabled: !opts.nextConfig.images.disableStaticImages, + hasPagesDir: !!pagesDir, + hasAppDir: !!appDir, + }) const routesManifestPath = path.join(distDir, ROUTES_MANIFEST) const routesManifest: DevRoutesManifest = { diff --git a/packages/next/src/server/lib/router-utils/typegen.ts b/packages/next/src/server/lib/router-utils/typegen.ts index b9149e84b6953..4604310f04289 100644 --- a/packages/next/src/server/lib/router-utils/typegen.ts +++ b/packages/next/src/server/lib/router-utils/typegen.ts @@ -1,6 +1,41 @@ import type { RouteTypesManifest } from './route-types-utils' import { isDynamicRoute } from '../../../shared/lib/router/utils/is-dynamic' +/** + * Generates the content for .next/types/next-env.d.ts + * This file contains dynamic type references that depend on project configuration. + */ +export function generateDynamicTypesFile({ + imageImportsEnabled, + hasPagesDir, + hasAppDir, +}: { + imageImportsEnabled: boolean + hasPagesDir: boolean + hasAppDir: boolean +}): string { + const directives: string[] = [ + '// This file is generated automatically by Next.js', + '// Do not edit this file manually', + '', + ] + + if (imageImportsEnabled) { + directives.push('/// ') + } + + if (hasAppDir && hasPagesDir) { + directives.push( + '/// ' + ) + } + + // Import routes types from the same directory + directives.push('import "./routes.d.ts"') + + return directives.join('\n') + '\n' +} + function generateRouteTypes(routesManifest: RouteTypesManifest): string { const appRoutes = Object.keys(routesManifest.appRoutes).sort() const pageRoutes = Object.keys(routesManifest.pageRoutes).sort() diff --git a/test/integration/next-image-legacy/typescript/test/index.test.ts b/test/integration/next-image-legacy/typescript/test/index.test.ts index c9dba8f87a66a..2f300fcbea744 100644 --- a/test/integration/next-image-legacy/typescript/test/index.test.ts +++ b/test/integration/next-image-legacy/typescript/test/index.test.ts @@ -12,6 +12,8 @@ import { const appDir = join(__dirname, '..') const nextConfig = join(appDir, 'next.config.js') +// Dynamic types (image types, navigation compat) are now generated in .next/types/ +const dynamicTypesFile = join(appDir, '.next/types/next-env.d.ts') let appPort let app let output @@ -29,10 +31,7 @@ describe('TypeScript Image Component', () => { expect(stderr).toMatch(/Failed to compile/) expect(stderr).toMatch(/is not assignable to type/) expect(code).toBe(1) - const envTypes = await fs.readFile( - join(appDir, 'next-env.d.ts'), - 'utf8' - ) + const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') expect(envTypes).toContain('image-types/global') }) @@ -47,10 +46,7 @@ describe('TypeScript Image Component', () => { expect(stderr).toMatch(/is not assignable to type/) expect(code).toBe(1) await fs.writeFile(nextConfig, content) - const envTypes = await fs.readFile( - join(appDir, 'next-env.d.ts'), - 'utf8' - ) + const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') expect(envTypes).not.toContain('image-types/global') }) } @@ -69,10 +65,7 @@ describe('TypeScript Image Component', () => { afterAll(() => killApp(app)) it('should have image types when enabled', async () => { - const envTypes = await fs.readFile( - join(appDir, 'next-env.d.ts'), - 'utf8' - ) + const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') expect(envTypes).toContain('image-types/global') }) @@ -99,7 +92,7 @@ describe('TypeScript Image Component', () => { const app = await launchApp(appDir, await findPort()) await killApp(app) await fs.writeFile(nextConfig, content) - const envTypes = await fs.readFile(join(appDir, 'next-env.d.ts'), 'utf8') + const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') expect(envTypes).not.toContain('image-types/global') }) }) diff --git a/test/integration/next-image-new/typescript/test/index.test.ts b/test/integration/next-image-new/typescript/test/index.test.ts index 57929d8f5fec4..5e646eef46259 100644 --- a/test/integration/next-image-new/typescript/test/index.test.ts +++ b/test/integration/next-image-new/typescript/test/index.test.ts @@ -12,6 +12,8 @@ import { const appDir = join(__dirname, '..') const nextConfig = join(appDir, 'next.config.js') +// Dynamic types (image types, navigation compat) are now generated in .next/types/ +const dynamicTypesFile = join(appDir, '.next/types/next-env.d.ts') let appPort let app let output @@ -29,10 +31,7 @@ describe('TypeScript Image Component', () => { expect(stderr).toMatch(/Failed to compile/) expect(stderr).toMatch(/is not assignable to type/) expect(code).toBe(1) - const envTypes = await fs.readFile( - join(appDir, 'next-env.d.ts'), - 'utf8' - ) + const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') expect(envTypes).toContain('image-types/global') }) @@ -47,10 +46,7 @@ describe('TypeScript Image Component', () => { expect(stderr).toMatch(/is not assignable to type/) expect(code).toBe(1) await fs.writeFile(nextConfig, content) - const envTypes = await fs.readFile( - join(appDir, 'next-env.d.ts'), - 'utf8' - ) + const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') expect(envTypes).not.toContain('image-types/global') }) } @@ -69,10 +65,7 @@ describe('TypeScript Image Component', () => { afterAll(() => killApp(app)) it('should have image types when enabled', async () => { - const envTypes = await fs.readFile( - join(appDir, 'next-env.d.ts'), - 'utf8' - ) + const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') expect(envTypes).toContain('image-types/global') }) @@ -98,7 +91,7 @@ describe('TypeScript Image Component', () => { const app = await launchApp(appDir, await findPort()) await killApp(app) await fs.writeFile(nextConfig, content) - const envTypes = await fs.readFile(join(appDir, 'next-env.d.ts'), 'utf8') + const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') expect(envTypes).not.toContain('image-types/global') }) }) diff --git a/test/unit/write-app-declarations.test.ts b/test/unit/write-app-declarations.test.ts index b0344052c25a1..b7456a5c8eeaf 100644 --- a/test/unit/write-app-declarations.test.ts +++ b/test/unit/write-app-declarations.test.ts @@ -6,9 +6,8 @@ import { writeAppTypeDeclarations } from 'next/dist/lib/typescript/writeAppTypeD const fixtureDir = join(__dirname, 'fixtures/app-declarations') const declarationFile = join(fixtureDir, 'next-env.d.ts') -const imageImportsEnabled = false -describe('find config', () => { +describe('writeAppTypeDeclarations', () => { beforeEach(async () => { await fs.ensureDir(fixtureDir) }) @@ -19,11 +18,6 @@ describe('find config', () => { const content = '/// ' + eol + - (imageImportsEnabled - ? '/// ' + eol - : '') + - `import "./.next/types/routes.d.ts";` + - eol + eol + '// NOTE: This file should not be edited' + eol + @@ -34,9 +28,6 @@ describe('find config', () => { await writeAppTypeDeclarations({ baseDir: fixtureDir, - distDir: '.next', - imageImportsEnabled, - hasPagesDir: false, hasAppDir: false, }) expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) @@ -47,11 +38,6 @@ describe('find config', () => { const content = '/// ' + eol + - (imageImportsEnabled - ? '/// ' + eol - : '') + - `import "./.next/types/routes.d.ts";` + - eol + eol + '// NOTE: This file should not be edited' + eol + @@ -62,9 +48,6 @@ describe('find config', () => { await writeAppTypeDeclarations({ baseDir: fixtureDir, - distDir: '.next', - imageImportsEnabled, - hasPagesDir: false, hasAppDir: false, }) expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) @@ -75,11 +58,6 @@ describe('find config', () => { const content = '/// ' + eol + - (imageImportsEnabled - ? '/// ' + eol - : '') + - `import "./.next/types/routes.d.ts";` + - eol + eol + '// NOTE: This file should not be edited' + eol + @@ -88,37 +66,32 @@ describe('find config', () => { await writeAppTypeDeclarations({ baseDir: fixtureDir, - distDir: '.next', - imageImportsEnabled, - hasPagesDir: false, hasAppDir: false, }) expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) }) - it('should include navigation types if app directory is enabled', async () => { + it('should use app docs URL when hasAppDir is true', async () => { await writeAppTypeDeclarations({ baseDir: fixtureDir, - distDir: '.next', - imageImportsEnabled, - hasPagesDir: false, hasAppDir: true, }) - await expect(fs.readFile(declarationFile, 'utf8')).resolves.not.toContain( - 'next/navigation-types/compat/navigation' + const content = await fs.readFile(declarationFile, 'utf8') + expect(content).toContain( + '// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.' ) + }) + it('should use pages docs URL when hasAppDir is false', async () => { await writeAppTypeDeclarations({ baseDir: fixtureDir, - distDir: '.next', - imageImportsEnabled, - hasPagesDir: true, - hasAppDir: true, + hasAppDir: false, }) - await expect(fs.readFile(declarationFile, 'utf8')).resolves.toContain( - 'next/navigation-types/compat/navigation' + const content = await fs.readFile(declarationFile, 'utf8') + expect(content).toContain( + '// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' ) }) }) From 84a689d9930cfbfd7b782145fe109371a6998aa2 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 2 Dec 2025 23:34:19 +0100 Subject: [PATCH 2/8] update route types --- .../lib/router-utils/route-types-utils.ts | 1 + .../typescript/test/index.test.ts | 21 ++++++++++++++----- .../typescript/test/index.test.ts | 21 ++++++++++++++----- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/next/src/server/lib/router-utils/route-types-utils.ts b/packages/next/src/server/lib/router-utils/route-types-utils.ts index 794b7bf1aaa17..c80f3c759e713 100644 --- a/packages/next/src/server/lib/router-utils/route-types-utils.ts +++ b/packages/next/src/server/lib/router-utils/route-types-utils.ts @@ -402,6 +402,7 @@ export async function writeDynamicTypesFile({ const typesDir = path.join(distDir, 'types') const filePath = path.join(typesDir, 'next-env.d.ts') + // Directory should already be created by writeRouteTypesManifest, but ensure it exists if (!fs.existsSync(typesDir)) { await fs.promises.mkdir(typesDir, { recursive: true }) } diff --git a/test/integration/next-image-legacy/typescript/test/index.test.ts b/test/integration/next-image-legacy/typescript/test/index.test.ts index 2f300fcbea744..3e908a18bea48 100644 --- a/test/integration/next-image-legacy/typescript/test/index.test.ts +++ b/test/integration/next-image-legacy/typescript/test/index.test.ts @@ -8,12 +8,15 @@ import { launchApp, nextBuild, killApp, + retry, } from 'next-test-utils' const appDir = join(__dirname, '..') const nextConfig = join(appDir, 'next.config.js') // Dynamic types (image types, navigation compat) are now generated in .next/types/ -const dynamicTypesFile = join(appDir, '.next/types/next-env.d.ts') +// In dev mode with isolatedDevBuild (default), types are in .next/dev/types/ +const dynamicTypesFileBuild = join(appDir, '.next/types/next-env.d.ts') +const dynamicTypesFileDev = join(appDir, '.next/dev/types/next-env.d.ts') let appPort let app let output @@ -22,6 +25,14 @@ const handleOutput = (msg) => { output += msg } +// Helper to read dynamic types file with retry (file may be written async) +async function readDynamicTypesFile(isDev: boolean) { + const filePath = isDev ? dynamicTypesFileDev : dynamicTypesFileBuild + return retry(async () => { + return await fs.readFile(filePath, 'utf8') + }) +} + describe('TypeScript Image Component', () => { ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( 'production mode', @@ -31,7 +42,7 @@ describe('TypeScript Image Component', () => { expect(stderr).toMatch(/Failed to compile/) expect(stderr).toMatch(/is not assignable to type/) expect(code).toBe(1) - const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') + const envTypes = await readDynamicTypesFile(false) expect(envTypes).toContain('image-types/global') }) @@ -46,7 +57,7 @@ describe('TypeScript Image Component', () => { expect(stderr).toMatch(/is not assignable to type/) expect(code).toBe(1) await fs.writeFile(nextConfig, content) - const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') + const envTypes = await readDynamicTypesFile(false) expect(envTypes).not.toContain('image-types/global') }) } @@ -65,7 +76,7 @@ describe('TypeScript Image Component', () => { afterAll(() => killApp(app)) it('should have image types when enabled', async () => { - const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') + const envTypes = await readDynamicTypesFile(true) expect(envTypes).toContain('image-types/global') }) @@ -92,7 +103,7 @@ describe('TypeScript Image Component', () => { const app = await launchApp(appDir, await findPort()) await killApp(app) await fs.writeFile(nextConfig, content) - const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') + const envTypes = await readDynamicTypesFile(true) expect(envTypes).not.toContain('image-types/global') }) }) diff --git a/test/integration/next-image-new/typescript/test/index.test.ts b/test/integration/next-image-new/typescript/test/index.test.ts index 5e646eef46259..bd12617a11369 100644 --- a/test/integration/next-image-new/typescript/test/index.test.ts +++ b/test/integration/next-image-new/typescript/test/index.test.ts @@ -8,12 +8,15 @@ import { launchApp, nextBuild, killApp, + retry, } from 'next-test-utils' const appDir = join(__dirname, '..') const nextConfig = join(appDir, 'next.config.js') // Dynamic types (image types, navigation compat) are now generated in .next/types/ -const dynamicTypesFile = join(appDir, '.next/types/next-env.d.ts') +// In dev mode with isolatedDevBuild (default), types are in .next/dev/types/ +const dynamicTypesFileBuild = join(appDir, '.next/types/next-env.d.ts') +const dynamicTypesFileDev = join(appDir, '.next/dev/types/next-env.d.ts') let appPort let app let output @@ -22,6 +25,14 @@ const handleOutput = (msg) => { output += msg } +// Helper to read dynamic types file with retry (file may be written async) +async function readDynamicTypesFile(isDev: boolean) { + const filePath = isDev ? dynamicTypesFileDev : dynamicTypesFileBuild + return retry(async () => { + return await fs.readFile(filePath, 'utf8') + }) +} + describe('TypeScript Image Component', () => { ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( 'production mode', @@ -31,7 +42,7 @@ describe('TypeScript Image Component', () => { expect(stderr).toMatch(/Failed to compile/) expect(stderr).toMatch(/is not assignable to type/) expect(code).toBe(1) - const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') + const envTypes = await readDynamicTypesFile(false) expect(envTypes).toContain('image-types/global') }) @@ -46,7 +57,7 @@ describe('TypeScript Image Component', () => { expect(stderr).toMatch(/is not assignable to type/) expect(code).toBe(1) await fs.writeFile(nextConfig, content) - const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') + const envTypes = await readDynamicTypesFile(false) expect(envTypes).not.toContain('image-types/global') }) } @@ -65,7 +76,7 @@ describe('TypeScript Image Component', () => { afterAll(() => killApp(app)) it('should have image types when enabled', async () => { - const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') + const envTypes = await readDynamicTypesFile(true) expect(envTypes).toContain('image-types/global') }) @@ -91,7 +102,7 @@ describe('TypeScript Image Component', () => { const app = await launchApp(appDir, await findPort()) await killApp(app) await fs.writeFile(nextConfig, content) - const envTypes = await fs.readFile(dynamicTypesFile, 'utf8') + const envTypes = await readDynamicTypesFile(true) expect(envTypes).not.toContain('image-types/global') }) }) From 0c4599f73ec227be977521f013764708860ba9a7 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 3 Dec 2025 00:00:20 +0100 Subject: [PATCH 3/8] update tests --- .../typescript/writeConfigurationDefaults.ts | 17 ++++++++++------- .../next/src/server/lib/router-utils/typegen.ts | 2 +- .../next-env.d.ts | 2 -- .../typescript-basic/app/next-env.d.ts | 1 - 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/next/src/lib/typescript/writeConfigurationDefaults.ts b/packages/next/src/lib/typescript/writeConfigurationDefaults.ts index e8f452dc6e8f3..1a96e5005747a 100644 --- a/packages/next/src/lib/typescript/writeConfigurationDefaults.ts +++ b/packages/next/src/lib/typescript/writeConfigurationDefaults.ts @@ -282,19 +282,22 @@ export async function writeConfigurationDefaults( ) if (!('include' in userTsConfig)) { - userTsConfig.include = hasAppDir - ? ['next-env.d.ts', ...nextAppTypes, '**/*.mts', '**/*.ts', '**/*.tsx'] - : ['next-env.d.ts', '**/*.mts', '**/*.ts', '**/*.tsx'] + // Always include .next/types for dynamic types (image imports, route types, etc.) + userTsConfig.include = [ + 'next-env.d.ts', + ...nextAppTypes, + '**/*.mts', + '**/*.ts', + '**/*.tsx', + ] suggestedActions.push( cyan('include') + ' was set to ' + bold( - hasAppDir - ? `['next-env.d.ts', ${nextAppTypes.map((type) => `'${type}'`).join(', ')}, '**/*.mts', '**/*.ts', '**/*.tsx']` - : `['next-env.d.ts', '**/*.mts', '**/*.ts', '**/*.tsx']` + `['next-env.d.ts', ${nextAppTypes.map((type) => `'${type}'`).join(', ')}, '**/*.mts', '**/*.ts', '**/*.tsx']` ) ) - } else if (hasAppDir) { + } else { const missingFromResolved = [] for (const type of nextAppTypes) { if (!userTsConfig.include.includes(type)) { diff --git a/packages/next/src/server/lib/router-utils/typegen.ts b/packages/next/src/server/lib/router-utils/typegen.ts index 4604310f04289..9392160117ab6 100644 --- a/packages/next/src/server/lib/router-utils/typegen.ts +++ b/packages/next/src/server/lib/router-utils/typegen.ts @@ -609,7 +609,7 @@ export function generateValidatorFile( default: (req: any, res: any) => ReturnType config?: { api?: { - bodyParser?: boolean | { sizeLimit?: string } + bodyParser?: boolean | { sizeLimit?: string | number } responseLimit?: string | number | boolean externalResolver?: boolean } diff --git a/test/integration/typescript-app-type-declarations/next-env.d.ts b/test/integration/typescript-app-type-declarations/next-env.d.ts index 7996d352f43b1..4d5020dc06ba0 100644 --- a/test/integration/typescript-app-type-declarations/next-env.d.ts +++ b/test/integration/typescript-app-type-declarations/next-env.d.ts @@ -1,6 +1,4 @@ /// -/// -import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/test/production/typescript-basic/app/next-env.d.ts b/test/production/typescript-basic/app/next-env.d.ts index 52e831b434248..4d5020dc06ba0 100644 --- a/test/production/typescript-basic/app/next-env.d.ts +++ b/test/production/typescript-basic/app/next-env.d.ts @@ -1,5 +1,4 @@ /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. From f9bf0e94e94e3de3d6b3126a5bab2e63b2cf6cef Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 3 Dec 2025 00:15:55 +0100 Subject: [PATCH 4/8] update snapshot --- .../tsconfig-module-preserve/index.test.ts | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/test/e2e/tsconfig-module-preserve/index.test.ts b/test/e2e/tsconfig-module-preserve/index.test.ts index b7c736eec04dd..4d8ed170ed590 100644 --- a/test/e2e/tsconfig-module-preserve/index.test.ts +++ b/test/e2e/tsconfig-module-preserve/index.test.ts @@ -38,34 +38,36 @@ describe('tsconfig module: preserve', () => { expect(output).not.toContain('resolveJsonModule') expect(await next.readFile('tsconfig.json')).toMatchInlineSnapshot(` - "{ - "compilerOptions": { - "module": "preserve", - "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "strict": false, - "noEmit": true, - "incremental": true, - "isolatedModules": true, - "jsx": "react-jsx" - }, - "include": [ - "next-env.d.ts", - "**/*.mts", - "**/*.ts", - "**/*.tsx" - ], - "exclude": [ - "node_modules" - ] - } - " + "{ + "compilerOptions": { + "module": "preserve", + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "isolatedModules": true, + "jsx": "react-jsx" + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] + } + " `) }) }) From 0c9ca4fb07d95e09f7cb083d0446344eb8b4720a Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 4 Dec 2025 11:36:41 +0100 Subject: [PATCH 5/8] generate types --- .../next/src/lib/typescript/writeAppTypeDeclarations.ts | 3 --- packages/next/src/server/lib/router-utils/typegen.ts | 2 ++ test/unit/write-app-declarations.test.ts | 9 --------- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts index 9980c84224a5c..20db996f2ed22 100644 --- a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts +++ b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts @@ -40,9 +40,6 @@ export async function writeAppTypeDeclarations({ * @see https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html */ const directives: string[] = [ - // Include the core Next.js typings. - '/// ', - '', '// NOTE: This file should not be edited', `// see https://nextjs.org/docs/${hasAppDir ? 'app' : 'pages'}/api-reference/config/typescript for more information.`, ] diff --git a/packages/next/src/server/lib/router-utils/typegen.ts b/packages/next/src/server/lib/router-utils/typegen.ts index 9392160117ab6..6fc3b303b2b4b 100644 --- a/packages/next/src/server/lib/router-utils/typegen.ts +++ b/packages/next/src/server/lib/router-utils/typegen.ts @@ -15,6 +15,8 @@ export function generateDynamicTypesFile({ hasAppDir: boolean }): string { const directives: string[] = [ + // Include the core Next.js typings. + '/// ', '// This file is generated automatically by Next.js', '// Do not edit this file manually', '', diff --git a/test/unit/write-app-declarations.test.ts b/test/unit/write-app-declarations.test.ts index b7456a5c8eeaf..8054c443ce046 100644 --- a/test/unit/write-app-declarations.test.ts +++ b/test/unit/write-app-declarations.test.ts @@ -16,9 +16,6 @@ describe('writeAppTypeDeclarations', () => { it('should preserve CRLF EOL', async () => { const eol = '\r\n' const content = - '/// ' + - eol + - eol + '// NOTE: This file should not be edited' + eol + '// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' + @@ -36,9 +33,6 @@ describe('writeAppTypeDeclarations', () => { it('should preserve LF EOL', async () => { const eol = '\n' const content = - '/// ' + - eol + - eol + '// NOTE: This file should not be edited' + eol + '// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' + @@ -56,9 +50,6 @@ describe('writeAppTypeDeclarations', () => { it('should use OS EOL by default', async () => { const eol = os.EOL const content = - '/// ' + - eol + - eol + '// NOTE: This file should not be edited' + eol + '// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' + From 06402e069207bd215c81797f3accd45f04510ada Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 4 Dec 2025 12:02:27 +0100 Subject: [PATCH 6/8] generate types --- .../typescript/writeAppTypeDeclarations.ts | 54 ------------ .../next/src/lib/verify-typescript-setup.ts | 9 -- test/unit/write-app-declarations.test.ts | 88 ------------------- 3 files changed, 151 deletions(-) delete mode 100644 packages/next/src/lib/typescript/writeAppTypeDeclarations.ts delete mode 100644 test/unit/write-app-declarations.test.ts diff --git a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts deleted file mode 100644 index 20db996f2ed22..0000000000000 --- a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts +++ /dev/null @@ -1,54 +0,0 @@ -import os from 'os' -import path from 'path' -import { promises as fs } from 'fs' - -export async function writeAppTypeDeclarations({ - baseDir, - hasAppDir, -}: { - baseDir: string - hasAppDir: boolean -}): Promise { - // Reference `next` types - const appTypeDeclarations = path.join(baseDir, 'next-env.d.ts') - - // Defaults EOL to system default - let eol = os.EOL - let currentContent: string | undefined - - try { - currentContent = await fs.readFile(appTypeDeclarations, 'utf8') - // If file already exists then preserve its line ending - const lf = currentContent.indexOf('\n', /* skip first so we can lf - 1 */ 1) - - if (lf !== -1) { - if (currentContent[lf - 1] === '\r') { - eol = '\r\n' - } else { - eol = '\n' - } - } - } catch {} - - /** - * "Triple-slash directives" used to create typings files for Next.js projects - * using Typescript. - * - * Dynamic types (image imports, navigation compat, routes) are generated - * in .next/types/ and included via tsconfig.json. - * - * @see https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html - */ - const directives: string[] = [ - '// NOTE: This file should not be edited', - `// see https://nextjs.org/docs/${hasAppDir ? 'app' : 'pages'}/api-reference/config/typescript for more information.`, - ] - - const content = directives.join(eol) + eol - - // Avoids an un-necessary write on read-only fs - if (currentContent === content) { - return - } - await fs.writeFile(appTypeDeclarations, content) -} diff --git a/packages/next/src/lib/verify-typescript-setup.ts b/packages/next/src/lib/verify-typescript-setup.ts index 9b1d3d4081a20..e24f865fcd213 100644 --- a/packages/next/src/lib/verify-typescript-setup.ts +++ b/packages/next/src/lib/verify-typescript-setup.ts @@ -9,7 +9,6 @@ import * as log from '../build/output/log' import { getTypeScriptIntent } from './typescript/getTypeScriptIntent' import type { TypeCheckResult } from './typescript/runTypeCheck' -import { writeAppTypeDeclarations } from './typescript/writeAppTypeDeclarations' import { writeConfigurationDefaults } from './typescript/writeConfigurationDefaults' import { installDependencies } from './install-dependencies' import { isCI } from '../server/ci-info' @@ -129,14 +128,6 @@ export async function verifyTypeScriptSetup({ hasPagesDir, isolatedDevBuild ) - // Write out the necessary `next-env.d.ts` file to correctly register - // Next.js' types. Dynamic types (image imports, navigation compat) are - // generated in .next/types/ separately. - await writeAppTypeDeclarations({ - baseDir: dir, - hasAppDir, - }) - let result if (typeCheckPreflight) { const { runTypeCheck } = diff --git a/test/unit/write-app-declarations.test.ts b/test/unit/write-app-declarations.test.ts deleted file mode 100644 index 8054c443ce046..0000000000000 --- a/test/unit/write-app-declarations.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-env jest */ -import os from 'os' -import fs from 'fs-extra' -import { join } from 'path' -import { writeAppTypeDeclarations } from 'next/dist/lib/typescript/writeAppTypeDeclarations' - -const fixtureDir = join(__dirname, 'fixtures/app-declarations') -const declarationFile = join(fixtureDir, 'next-env.d.ts') - -describe('writeAppTypeDeclarations', () => { - beforeEach(async () => { - await fs.ensureDir(fixtureDir) - }) - afterEach(() => fs.remove(declarationFile)) - - it('should preserve CRLF EOL', async () => { - const eol = '\r\n' - const content = - '// NOTE: This file should not be edited' + - eol + - '// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' + - eol - - await fs.writeFile(declarationFile, content) - - await writeAppTypeDeclarations({ - baseDir: fixtureDir, - hasAppDir: false, - }) - expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) - }) - - it('should preserve LF EOL', async () => { - const eol = '\n' - const content = - '// NOTE: This file should not be edited' + - eol + - '// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' + - eol - - await fs.writeFile(declarationFile, content) - - await writeAppTypeDeclarations({ - baseDir: fixtureDir, - hasAppDir: false, - }) - expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) - }) - - it('should use OS EOL by default', async () => { - const eol = os.EOL - const content = - '// NOTE: This file should not be edited' + - eol + - '// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' + - eol - - await writeAppTypeDeclarations({ - baseDir: fixtureDir, - hasAppDir: false, - }) - expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) - }) - - it('should use app docs URL when hasAppDir is true', async () => { - await writeAppTypeDeclarations({ - baseDir: fixtureDir, - hasAppDir: true, - }) - - const content = await fs.readFile(declarationFile, 'utf8') - expect(content).toContain( - '// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.' - ) - }) - - it('should use pages docs URL when hasAppDir is false', async () => { - await writeAppTypeDeclarations({ - baseDir: fixtureDir, - hasAppDir: false, - }) - - const content = await fs.readFile(declarationFile, 'utf8') - expect(content).toContain( - '// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' - ) - }) -}) From 14cfe09ee63031c0a4dab66e4aa0da5b6a568194 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 4 Dec 2025 13:40:17 +0100 Subject: [PATCH 7/8] update tsconfig gen --- .../writeConfigurationDefaults.test.ts | 3 +- .../typescript/writeConfigurationDefaults.ts | 10 +-- .../tsconfig-module-preserve/index.test.ts | 1 - .../next-env.d.ts | 4 -- .../test/index.test.ts | 72 +++++++++++-------- .../tsconfig.json | 2 +- .../typechecking/tsconfig.json | 2 +- test/rspack-build-tests-manifest.json | 22 +++--- test/rspack-dev-tests-manifest.json | 22 +++--- test/turbopack-build-tests-manifest.json | 22 +++--- test/turbopack-dev-tests-manifest.json | 22 +++--- 11 files changed, 90 insertions(+), 92 deletions(-) delete mode 100644 test/integration/typescript-app-type-declarations/next-env.d.ts diff --git a/packages/next/src/lib/typescript/writeConfigurationDefaults.test.ts b/packages/next/src/lib/typescript/writeConfigurationDefaults.test.ts index b4b7a73d1129a..10a9440e08995 100644 --- a/packages/next/src/lib/typescript/writeConfigurationDefaults.test.ts +++ b/packages/next/src/lib/typescript/writeConfigurationDefaults.test.ts @@ -84,7 +84,6 @@ describe('writeConfigurationDefaults()', () => { "node_modules", ], "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -107,7 +106,7 @@ describe('writeConfigurationDefaults()', () => { - strict was set to false - noEmit was set to true - incremental was set to true - - include was set to ['next-env.d.ts', '.next/types/**/*.ts', '.next/dev/types/**/*.ts', '**/*.mts', '**/*.ts', '**/*.tsx'] + - include was set to ['.next/types/**/*.ts', '.next/dev/types/**/*.ts', '**/*.mts', '**/*.ts', '**/*.tsx'] - plugins was updated to add { name: 'next' } - exclude was set to ['node_modules'] diff --git a/packages/next/src/lib/typescript/writeConfigurationDefaults.ts b/packages/next/src/lib/typescript/writeConfigurationDefaults.ts index 1a96e5005747a..8d65a9039c254 100644 --- a/packages/next/src/lib/typescript/writeConfigurationDefaults.ts +++ b/packages/next/src/lib/typescript/writeConfigurationDefaults.ts @@ -283,18 +283,12 @@ export async function writeConfigurationDefaults( if (!('include' in userTsConfig)) { // Always include .next/types for dynamic types (image imports, route types, etc.) - userTsConfig.include = [ - 'next-env.d.ts', - ...nextAppTypes, - '**/*.mts', - '**/*.ts', - '**/*.tsx', - ] + userTsConfig.include = [...nextAppTypes, '**/*.mts', '**/*.ts', '**/*.tsx'] suggestedActions.push( cyan('include') + ' was set to ' + bold( - `['next-env.d.ts', ${nextAppTypes.map((type) => `'${type}'`).join(', ')}, '**/*.mts', '**/*.ts', '**/*.tsx']` + `[${nextAppTypes.map((type) => `'${type}'`).join(', ')}, '**/*.mts', '**/*.ts', '**/*.tsx']` ) ) } else { diff --git a/test/e2e/tsconfig-module-preserve/index.test.ts b/test/e2e/tsconfig-module-preserve/index.test.ts index 4d8ed170ed590..ffe5fc6b9e7c3 100644 --- a/test/e2e/tsconfig-module-preserve/index.test.ts +++ b/test/e2e/tsconfig-module-preserve/index.test.ts @@ -56,7 +56,6 @@ describe('tsconfig module: preserve', () => { "jsx": "react-jsx" }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", diff --git a/test/integration/typescript-app-type-declarations/next-env.d.ts b/test/integration/typescript-app-type-declarations/next-env.d.ts deleted file mode 100644 index 4d5020dc06ba0..0000000000000 --- a/test/integration/typescript-app-type-declarations/next-env.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/test/integration/typescript-app-type-declarations/test/index.test.ts b/test/integration/typescript-app-type-declarations/test/index.test.ts index fe655be667149..36a77cbcd8d6e 100644 --- a/test/integration/typescript-app-type-declarations/test/index.test.ts +++ b/test/integration/typescript-app-type-declarations/test/index.test.ts @@ -5,53 +5,63 @@ import { findPort, launchApp, killApp } from 'next-test-utils' import { promises as fs } from 'fs' const appDir = join(__dirname, '..') -const appTypeDeclarations = join(appDir, 'next-env.d.ts') +const rootTypeDeclarations = join(appDir, 'next-env.d.ts') +const distTypeDeclarations = join(appDir, '.next/types/next-env.d.ts') describe('TypeScript App Type Declarations', () => { - it('should write a new next-env.d.ts if none exist', async () => { - const prevContent = await fs.readFile(appTypeDeclarations, 'utf8') + it('should not create next-env.d.ts in project root', async () => { + // Ensure no next-env.d.ts exists before starting try { - await fs.unlink(appTypeDeclarations) - const appPort = await findPort() - let app - try { - app = await launchApp(appDir, appPort, {}) - const content = await fs.readFile(appTypeDeclarations, 'utf8') - expect(content).toEqual(prevContent) - } finally { - await killApp(app) - } + await fs.unlink(rootTypeDeclarations) + } catch { + // File doesn't exist, which is fine + } + + const appPort = await findPort() + let app + try { + app = await launchApp(appDir, appPort, {}) + // Verify next-env.d.ts was NOT created in project root + await expect(fs.access(rootTypeDeclarations)).rejects.toThrow() } finally { - await fs.writeFile(appTypeDeclarations, prevContent) + await killApp(app) } }) - it('should overwrite next-env.d.ts if an incorrect one exists', async () => { - const prevContent = await fs.readFile(appTypeDeclarations, 'utf8') + it('should not modify existing next-env.d.ts in project root', async () => { + const existingContent = '// custom next-env.d.ts content\n' + await fs.writeFile(rootTypeDeclarations, existingContent) + const prevStat = await fs.stat(rootTypeDeclarations) + + const appPort = await findPort() + let app try { - await fs.writeFile(appTypeDeclarations, prevContent + 'modification') - const appPort = await findPort() - let app - try { - app = await launchApp(appDir, appPort, {}) - const content = await fs.readFile(appTypeDeclarations, 'utf8') - expect(content).toEqual(prevContent) - } finally { - await killApp(app) - } + app = await launchApp(appDir, appPort, {}) + // Verify next-env.d.ts was NOT modified + const stat = await fs.stat(rootTypeDeclarations) + expect(stat.mtime).toEqual(prevStat.mtime) + const content = await fs.readFile(rootTypeDeclarations, 'utf8') + expect(content).toEqual(existingContent) } finally { - await fs.writeFile(appTypeDeclarations, prevContent) + await killApp(app) + // Clean up + await fs.unlink(rootTypeDeclarations).catch(() => {}) } }) - it('should not touch an existing correct next-env.d.ts', async () => { - const prevStat = await fs.stat(appTypeDeclarations) + it('should overwrite next-env.d.ts in .next/types if incorrect', async () => { + // Create .next/types directory and write incorrect content + await fs.mkdir(join(appDir, '.next/types'), { recursive: true }) + await fs.writeFile(distTypeDeclarations, '// incorrect content\n') + const appPort = await findPort() let app try { app = await launchApp(appDir, appPort, {}) - const stat = await fs.stat(appTypeDeclarations) - expect(stat.mtime).toEqual(prevStat.mtime) + // Verify next-env.d.ts was overwritten with correct content + const content = await fs.readFile(distTypeDeclarations, 'utf8') + expect(content).not.toEqual('// incorrect content\n') + expect(content).toContain('/// ') } finally { await killApp(app) } diff --git a/test/integration/typescript-app-type-declarations/tsconfig.json b/test/integration/typescript-app-type-declarations/tsconfig.json index bad53066af0cd..baf06653ad5a7 100644 --- a/test/integration/typescript-app-type-declarations/tsconfig.json +++ b/test/integration/typescript-app-type-declarations/tsconfig.json @@ -17,5 +17,5 @@ "isolatedModules": true }, "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"], - "include": ["next-env.d.ts", "components", "pages"] + "include": ["components", "pages", ".next/types/**/*.ts"] } diff --git a/test/production/typescript-basic/typechecking/tsconfig.json b/test/production/typescript-basic/typechecking/tsconfig.json index b427f3a9fb8f8..50c26602bfee7 100644 --- a/test/production/typescript-basic/typechecking/tsconfig.json +++ b/test/production/typescript-basic/typechecking/tsconfig.json @@ -22,6 +22,6 @@ "@/*": ["./*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/test/rspack-build-tests-manifest.json b/test/rspack-build-tests-manifest.json index 4727c495306ca..8161ccfb370cd 100644 --- a/test/rspack-build-tests-manifest.json +++ b/test/rspack-build-tests-manifest.json @@ -19987,17 +19987,6 @@ "flakey": [], "runtimeError": false }, - "test/integration/typescript-app-type-declarations/test/index.test.ts": { - "passed": [ - "TypeScript App Type Declarations should not touch an existing correct next-env.d.ts", - "TypeScript App Type Declarations should overwrite next-env.d.ts if an incorrect one exists", - "TypeScript App Type Declarations should write a new next-env.d.ts if none exist" - ], - "failed": [], - "pending": [], - "flakey": [], - "runtimeError": false - }, "test/integration/typescript-baseurl/test/index.test.ts": { "passed": ["TypeScript Features default behavior should render the page"], "failed": [], @@ -22069,5 +22058,16 @@ "pending": [], "flakey": [], "runtimeError": false + }, + "test/integration/typescript-app-type-declarations/test/index.test.ts": { + "passed": [ + "TypeScript App Type Declarations should not create next-env.d.ts in project root", + "TypeScript App Type Declarations should not modify existing next-env.d.ts in project root", + "TypeScript App Type Declarations should overwrite next-env.d.ts in .next/types if incorrect" + ], + "failed": [], + "pending": [], + "flakey": [], + "runtimeError": false } } diff --git a/test/rspack-dev-tests-manifest.json b/test/rspack-dev-tests-manifest.json index 99072c58efd4a..19db8e9189806 100644 --- a/test/rspack-dev-tests-manifest.json +++ b/test/rspack-dev-tests-manifest.json @@ -21974,17 +21974,6 @@ "flakey": [], "runtimeError": false }, - "test/integration/typescript-app-type-declarations/test/index.test.ts": { - "passed": [ - "TypeScript App Type Declarations should not touch an existing correct next-env.d.ts", - "TypeScript App Type Declarations should overwrite next-env.d.ts if an incorrect one exists", - "TypeScript App Type Declarations should write a new next-env.d.ts if none exist" - ], - "failed": [], - "pending": [], - "flakey": [], - "runtimeError": false - }, "test/integration/typescript-baseurl/test/index.test.ts": { "passed": ["TypeScript Features default behavior should render the page"], "failed": [], @@ -22178,5 +22167,16 @@ ], "flakey": [], "runtimeError": false + }, + "test/integration/typescript-app-type-declarations/test/index.test.ts": { + "passed": [ + "TypeScript App Type Declarations should not create next-env.d.ts in project root", + "TypeScript App Type Declarations should not modify existing next-env.d.ts in project root", + "TypeScript App Type Declarations should overwrite next-env.d.ts in .next/types if incorrect" + ], + "failed": [], + "pending": [], + "flakey": [], + "runtimeError": false } } diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json index a4b9bc1fbe834..4114926a434a7 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -17865,17 +17865,6 @@ "flakey": [], "runtimeError": false }, - "test/integration/typescript-app-type-declarations/test/index.test.ts": { - "passed": [ - "TypeScript App Type Declarations should not touch an existing correct next-env.d.ts", - "TypeScript App Type Declarations should overwrite next-env.d.ts if an incorrect one exists", - "TypeScript App Type Declarations should write a new next-env.d.ts if none exist" - ], - "failed": [], - "pending": [], - "flakey": [], - "runtimeError": false - }, "test/integration/typescript-baseurl/test/index.test.ts": { "passed": ["TypeScript Features default behavior should render the page"], "failed": [], @@ -19631,5 +19620,16 @@ "pending": [], "flakey": [], "runtimeError": false + }, + "test/integration/typescript-app-type-declarations/test/index.test.ts": { + "passed": [ + "TypeScript App Type Declarations should not create next-env.d.ts in project root", + "TypeScript App Type Declarations should not modify existing next-env.d.ts in project root", + "TypeScript App Type Declarations should overwrite next-env.d.ts in .next/types if incorrect" + ], + "failed": [], + "pending": [], + "flakey": [], + "runtimeError": false } } diff --git a/test/turbopack-dev-tests-manifest.json b/test/turbopack-dev-tests-manifest.json index e86ad4277f699..718b86a834c45 100644 --- a/test/turbopack-dev-tests-manifest.json +++ b/test/turbopack-dev-tests-manifest.json @@ -21395,17 +21395,6 @@ "flakey": [], "runtimeError": false }, - "test/integration/typescript-app-type-declarations/test/index.test.ts": { - "passed": [ - "TypeScript App Type Declarations should not touch an existing correct next-env.d.ts", - "TypeScript App Type Declarations should overwrite next-env.d.ts if an incorrect one exists", - "TypeScript App Type Declarations should write a new next-env.d.ts if none exist" - ], - "failed": [], - "pending": [], - "flakey": [], - "runtimeError": false - }, "test/integration/typescript-baseurl/test/index.test.ts": { "passed": ["TypeScript Features default behavior should render the page"], "failed": [], @@ -21636,5 +21625,16 @@ ], "flakey": [], "runtimeError": false + }, + "test/integration/typescript-app-type-declarations/test/index.test.ts": { + "passed": [ + "TypeScript App Type Declarations should not create next-env.d.ts in project root", + "TypeScript App Type Declarations should not modify existing next-env.d.ts in project root", + "TypeScript App Type Declarations should overwrite next-env.d.ts in .next/types if incorrect" + ], + "failed": [], + "pending": [], + "flakey": [], + "runtimeError": false } } From d1dc85d6bf214867934e17cf026bcab1a6651675 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 4 Dec 2025 14:45:39 +0100 Subject: [PATCH 8/8] update tests --- .../tsconfig-verifier/test/index.test.ts | 14 -------------- .../test/index.test.ts | 12 +++++------- .../typescript-app-type-declarations/tsconfig.json | 7 ++++++- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/test/integration/tsconfig-verifier/test/index.test.ts b/test/integration/tsconfig-verifier/test/index.test.ts index 7c7cee21b0db1..15aa8ba2338ed 100644 --- a/test/integration/tsconfig-verifier/test/index.test.ts +++ b/test/integration/tsconfig-verifier/test/index.test.ts @@ -52,7 +52,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -109,7 +108,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -185,7 +183,6 @@ import path from 'path' // in-object comment 2 , "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -240,7 +237,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -294,7 +290,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -352,7 +347,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -410,7 +404,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -465,7 +458,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -523,7 +515,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -581,7 +572,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts", @@ -629,7 +619,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", "**/*.mts", "**/*.ts", @@ -688,7 +677,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", "**/*.mts", "**/*.ts", @@ -750,7 +738,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", "**/*.mts", "**/*.ts", @@ -821,7 +808,6 @@ import path from 'path' "strictNullChecks": true }, "include": [ - "next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx" diff --git a/test/integration/typescript-app-type-declarations/test/index.test.ts b/test/integration/typescript-app-type-declarations/test/index.test.ts index 36a77cbcd8d6e..b3f5985d4fecb 100644 --- a/test/integration/typescript-app-type-declarations/test/index.test.ts +++ b/test/integration/typescript-app-type-declarations/test/index.test.ts @@ -6,7 +6,7 @@ import { promises as fs } from 'fs' const appDir = join(__dirname, '..') const rootTypeDeclarations = join(appDir, 'next-env.d.ts') -const distTypeDeclarations = join(appDir, '.next/types/next-env.d.ts') +const distTypeDeclarations = join(appDir, '.next/dev/types/next-env.d.ts') describe('TypeScript App Type Declarations', () => { it('should not create next-env.d.ts in project root', async () => { @@ -49,18 +49,16 @@ describe('TypeScript App Type Declarations', () => { } }) - it('should overwrite next-env.d.ts in .next/types if incorrect', async () => { - // Create .next/types directory and write incorrect content - await fs.mkdir(join(appDir, '.next/types'), { recursive: true }) - await fs.writeFile(distTypeDeclarations, '// incorrect content\n') + it('should generate next-env.d.ts in .next/types', async () => { + // Clean up any existing .next directory + await fs.rm(join(appDir, '.next'), { recursive: true, force: true }) const appPort = await findPort() let app try { app = await launchApp(appDir, appPort, {}) - // Verify next-env.d.ts was overwritten with correct content + // Verify next-env.d.ts was generated in .next/types const content = await fs.readFile(distTypeDeclarations, 'utf8') - expect(content).not.toEqual('// incorrect content\n') expect(content).toContain('/// ') } finally { await killApp(app) diff --git a/test/integration/typescript-app-type-declarations/tsconfig.json b/test/integration/typescript-app-type-declarations/tsconfig.json index baf06653ad5a7..abb79a52de501 100644 --- a/test/integration/typescript-app-type-declarations/tsconfig.json +++ b/test/integration/typescript-app-type-declarations/tsconfig.json @@ -17,5 +17,10 @@ "isolatedModules": true }, "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"], - "include": ["components", "pages", ".next/types/**/*.ts"] + "include": [ + "components", + "pages", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ] }