Skip to content

Commit

Permalink
Avoid fs write next-env.d.ts on read-only filesystems (#28206)
Browse files Browse the repository at this point in the history
Next.js currently writes the TS type declarations on startup, regardless of the existing content of the file. This is good for ensuring the file content stays consistent. However, if the file content is already correct, this will perform an unnessecary write.

When running Next in read-only filesystems (such as the Bazel sandbox) this can cause the build to fail even if the content of the type declaration file is already correct.

This fixes this by only writing the contents of the file if the current contents don't match.

## Test Plan

Added an integration test for the general behavior of writing `next-env.d.ts`.
  • Loading branch information
chrislloyd committed Aug 18, 2021
1 parent baeb98e commit 52c2f8b
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 14 deletions.
37 changes: 23 additions & 14 deletions 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,
Expand All @@ -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 =
'/// <reference types="next" />' +
os.EOL +
'/// <reference types="next/types/global" />' +
os.EOL +
(imageImportsEnabled
? '/// <reference types="next/image-types/global" />' + 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 +
'/// <reference types="next/types/global" />' +
os.EOL +
(imageImportsEnabled
? '/// <reference types="next/image-types/global" />' + 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)
}
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
@@ -0,0 +1,3 @@
export default function Index() {
return <div />
}
@@ -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)
}
})
})
21 changes: 21 additions & 0 deletions 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"]
}

0 comments on commit 52c2f8b

Please sign in to comment.