diff --git a/packages/next/lib/typescript/writeAppTypeDeclarations.ts b/packages/next/lib/typescript/writeAppTypeDeclarations.ts
index 3a8942a3f790f8c..c6b64dc2b200775 100644
--- a/packages/next/lib/typescript/writeAppTypeDeclarations.ts
+++ b/packages/next/lib/typescript/writeAppTypeDeclarations.ts
@@ -1,6 +1,7 @@
import { promises as fs } from 'fs'
import os from 'os'
import path from 'path'
+import { fileExists } from '../file-exists'
export async function writeAppTypeDeclarations(
baseDir: string,
@@ -9,19 +10,27 @@ export async function writeAppTypeDeclarations(
// Reference `next` types
const appTypeDeclarations = path.join(baseDir, 'next-env.d.ts')
- await fs.writeFile(
- appTypeDeclarations,
+ const content =
'/// ' +
- os.EOL +
- '/// ' +
- os.EOL +
- (imageImportsEnabled
- ? '/// ' + os.EOL
- : '') +
- os.EOL +
- '// NOTE: This file should not be edited' +
- os.EOL +
- '// see https://nextjs.org/docs/basic-features/typescript for more information.' +
- os.EOL
- )
+ os.EOL +
+ '/// ' +
+ os.EOL +
+ (imageImportsEnabled
+ ? '/// ' + os.EOL
+ : '') +
+ os.EOL +
+ '// NOTE: This file should not be edited' +
+ os.EOL +
+ '// see https://nextjs.org/docs/basic-features/typescript for more information.' +
+ os.EOL
+
+ // Avoids a write for read-only filesystems
+ if (
+ (await fileExists(appTypeDeclarations)) &&
+ (await fs.readFile(appTypeDeclarations, 'utf8')) === content
+ ) {
+ return
+ }
+
+ await fs.writeFile(appTypeDeclarations, content)
}
diff --git a/test/integration/typescript-app-type-declarations/next-env.d.ts b/test/integration/typescript-app-type-declarations/next-env.d.ts
new file mode 100644
index 000000000000000..9bc3dd46b9d9b21
--- /dev/null
+++ b/test/integration/typescript-app-type-declarations/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/test/integration/typescript-app-type-declarations/pages/index.tsx b/test/integration/typescript-app-type-declarations/pages/index.tsx
new file mode 100644
index 000000000000000..3618c0765503375
--- /dev/null
+++ b/test/integration/typescript-app-type-declarations/pages/index.tsx
@@ -0,0 +1,3 @@
+export default function Index() {
+ return
+}
diff --git a/test/integration/typescript-app-type-declarations/test/index.test.js b/test/integration/typescript-app-type-declarations/test/index.test.js
new file mode 100644
index 000000000000000..ad2ea6a74c03143
--- /dev/null
+++ b/test/integration/typescript-app-type-declarations/test/index.test.js
@@ -0,0 +1,61 @@
+/* eslint-env jest */
+
+import { join } from 'path'
+import { findPort, launchApp, killApp } from 'next-test-utils'
+import { promises as fs } from 'fs'
+
+jest.setTimeout(1000 * 60 * 2)
+
+const appDir = join(__dirname, '..')
+const appTypeDeclarations = join(appDir, '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')
+ 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)
+ }
+ } finally {
+ await fs.writeFile(appTypeDeclarations, prevContent)
+ }
+ })
+
+ it('should overwrite next-env.d.ts if an incorrect one exists', async () => {
+ const prevContent = await fs.readFile(appTypeDeclarations, 'utf8')
+ 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)
+ }
+ } finally {
+ await fs.writeFile(appTypeDeclarations, prevContent)
+ }
+ })
+
+ it('should not touch an existing correct next-env.d.ts', async () => {
+ const prevStat = await fs.stat(appTypeDeclarations)
+ const appPort = await findPort()
+ let app
+ try {
+ app = await launchApp(appDir, appPort, {})
+ const stat = await fs.stat(appTypeDeclarations)
+ expect(stat.mtime).toEqual(prevStat.mtime)
+ } finally {
+ await killApp(app)
+ }
+ })
+})
diff --git a/test/integration/typescript-app-type-declarations/tsconfig.json b/test/integration/typescript-app-type-declarations/tsconfig.json
new file mode 100644
index 000000000000000..11bbfe8efba7aaa
--- /dev/null
+++ b/test/integration/typescript-app-type-declarations/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "esModuleInterop": true,
+ "module": "esnext",
+ "jsx": "preserve",
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "incremental": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true
+ },
+ "exclude": ["node_modules"],
+ "include": ["next-env.d.ts", "components", "pages"]
+}