Skip to content

Improve initial setup with new App Router TypeScript project #64826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/build_reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ jobs:
# clean up any previous artifacts to avoid hitting disk space limits
- run: git clean -xdf && rm -rf /tmp/next-repo-*; rm -rf /tmp/next-install-* /tmp/yarn-* /tmp/ncc-cache target

# Configure a git user so that Create Next App can initialize git repos during integration tests.
- name: Set CI git user
run: |
git config --global user.name "vercel-ci-bot"
git config --global user.email "infra+ci@vercel.com"

- run: cargo clean
if: ${{ inputs.skipNativeBuild != 'yes' || inputs.needsNextest == 'yes' || inputs.needsRust == 'yes' }}

Expand Down
1 change: 1 addition & 0 deletions packages/create-next-app/templates/app-tw/ts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
Expand Down
1 change: 1 addition & 0 deletions packages/create-next-app/templates/app/ts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
Expand Down
257 changes: 257 additions & 0 deletions packages/next/src/lib/typescript/writeConfigurationDefaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { mkdtemp, writeFile, readFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
// eslint-disable-next-line import/no-extraneous-dependencies
import ts from 'typescript'
import { writeConfigurationDefaults } from './writeConfigurationDefaults'

describe('writeConfigurationDefaults()', () => {
let consoleLogSpy: jest.SpyInstance
let distDir: string
let hasAppDir: boolean
let tmpDir: string
let tsConfigPath: string
let isFirstTimeSetup: boolean
let hasPagesDir: boolean

beforeEach(async () => {
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
distDir = '.next'
tmpDir = await mkdtemp(join(tmpdir(), 'nextjs-test-'))
tsConfigPath = join(tmpDir, 'tsconfig.json')
isFirstTimeSetup = false
})

afterEach(() => {
consoleLogSpy.mockRestore()
})

describe('appDir', () => {
beforeEach(() => {
hasAppDir = true
hasPagesDir = false
})

it('applies suggested and mandatory defaults to existing tsconfig.json and logs them', async () => {
await writeFile(tsConfigPath, JSON.stringify({ compilerOptions: {} }), {
encoding: 'utf8',
})

await writeConfigurationDefaults(
ts,
tsConfigPath,
isFirstTimeSetup,
hasAppDir,
distDir,
hasPagesDir
)

const tsConfig = await readFile(tsConfigPath, { encoding: 'utf8' })

expect(JSON.parse(tsConfig)).toMatchInlineSnapshot(`
{
"compilerOptions": {
"allowJs": true,
"esModuleInterop": true,
"incremental": true,
"isolatedModules": true,
"jsx": "preserve",
"lib": [
"dom",
"dom.iterable",
"esnext",
],
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"plugins": [
{
"name": "next",
},
],
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": false,
"target": "ES2017",
},
"exclude": [
"node_modules",
],
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx",
],
}
`)

expect(
consoleLogSpy.mock.calls
.flat()
.join('\n')
// eslint-disable-next-line no-control-regex
.replace(/\x1B\[\d+m/g, '') // remove color control characters
).toMatchInlineSnapshot(`
"

We detected TypeScript in your project and reconfigured your tsconfig.json file for you. Strict-mode is set to false by default.

The following suggested values were added to your tsconfig.json. These values can be changed to fit your project's needs:


- target was set to ES2017 (For top-level \`await\`. Note: Next.js only polyfills for the esmodules target.)

- lib was set to dom,dom.iterable,esnext

- allowJs was set to true

- skipLibCheck was set to true

- 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', '**/*.ts', '**/*.tsx']

- plugins was updated to add { name: 'next' }

- exclude was set to ['node_modules']


The following mandatory changes were made to your tsconfig.json:


- module was set to esnext (for dynamic import() support)

- esModuleInterop was set to true (requirement for SWC / babel)

- moduleResolution was set to node (to match webpack resolution)

- resolveJsonModule was set to true (to match webpack resolution)

- isolatedModules was set to true (requirement for SWC / Babel)

- jsx was set to preserve (next.js implements its own optimized jsx transform)
"
`)
})

it('does not warn about disabled strict mode if strict mode was already enabled', async () => {
await writeFile(
tsConfigPath,
JSON.stringify({ compilerOptions: { strict: true } }),
{ encoding: 'utf8' }
)

await writeConfigurationDefaults(
ts,
tsConfigPath,
isFirstTimeSetup,
hasAppDir,
distDir,
hasPagesDir
)

expect(
consoleLogSpy.mock.calls
.flat()
.join('\n')
// eslint-disable-next-line no-control-regex
.replace(/\x1B\[\d+m/g, '') // remove color control characters
).not.toMatch('Strict-mode is set to false by default.')
})

describe('with tsconfig extends', () => {
let tsConfigBasePath: string
let nextAppTypes: string

beforeEach(() => {
tsConfigBasePath = join(tmpDir, 'tsconfig.base.json')
nextAppTypes = `${distDir}/types/**/*.ts`
})

it('should support empty includes when base provides it', async () => {
const include = ['**/*.ts', '**/*.tsx', nextAppTypes]
const content = { extends: './tsconfig.base.json' }
const baseContent = { include }

await writeFile(tsConfigPath, JSON.stringify(content, null, 2))
await writeFile(tsConfigBasePath, JSON.stringify(baseContent, null, 2))

await expect(
writeConfigurationDefaults(
ts,
tsConfigPath,
isFirstTimeSetup,
hasAppDir,
distDir,
hasPagesDir
)
).resolves.not.toThrow()

const output = await readFile(tsConfigPath, 'utf-8')
const parsed = JSON.parse(output)

expect(parsed.include).toBeUndefined()
})

it('should replace includes when base is missing appTypes', async () => {
const include = ['**/*.ts', '**/*.tsx']
const content = { extends: './tsconfig.base.json' }
const baseContent = { include }

await writeFile(tsConfigPath, JSON.stringify(content, null, 2))
await writeFile(tsConfigBasePath, JSON.stringify(baseContent, null, 2))

await expect(
writeConfigurationDefaults(
ts,
tsConfigPath,
isFirstTimeSetup,
hasAppDir,
distDir,
hasPagesDir
)
).resolves.not.toThrow()

const output = await readFile(tsConfigPath, 'utf8')
const parsed = JSON.parse(output)

expect(parsed.include.sort()).toMatchInlineSnapshot(`
[
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
]
`)
})

it('should not add strictNullChecks if base provides it', async () => {
const content = { extends: './tsconfig.base.json' }

const baseContent = {
compilerOptions: { strictNullChecks: true, strict: true },
}

await writeFile(tsConfigPath, JSON.stringify(content, null, 2))
await writeFile(tsConfigBasePath, JSON.stringify(baseContent, null, 2))

await writeConfigurationDefaults(
ts,
tsConfigPath,
isFirstTimeSetup,
hasAppDir,
distDir,
hasPagesDir
)
const output = await readFile(tsConfigPath, 'utf8')
const parsed = JSON.parse(output)

expect(parsed.compilerOptions.strictNullChecks).toBeUndefined()
})
})
})
})
11 changes: 7 additions & 4 deletions packages/next/src/lib/typescript/writeConfigurationDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,7 @@ export async function writeConfigurationDefaults(
cyan(optionKey) +
' was set to ' +
bold(check.suggested) +
check.reason
? ` (${check.reason})`
: ''
(check.reason ? ` (${check.reason})` : '')
)
}
} else if ('value' in check) {
Expand Down Expand Up @@ -337,8 +335,13 @@ export async function writeConfigurationDefaults(
Log.info(
`We detected TypeScript in your project and reconfigured your ${cyan(
'tsconfig.json'
)} file for you. Strict-mode is set to ${cyan('false')} by default.`
)} file for you.${
userTsConfig.compilerOptions?.strict
? ''
: ` Strict-mode is set to ${cyan('false')} by default.`
}`
)

if (suggestedActions.length) {
Log.info(
`The following suggested values were added to your ${cyan(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"baseUrl": "."
"baseUrl": ".",
"plugins": [
{
"name": "next"
}
]
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
"include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", ".next/types/**/*.ts"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
{
"name": "next"
}
]
],
"target": "ES2017"
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"name": "next"
}
],
"strictNullChecks": true
"strictNullChecks": true,
"target": "ES2017"
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
{
"name": "next"
}
]
],
"target": "ES2017"
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/app-dir/metadata-suspense/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
{
"name": "next"
}
]
],
"target": "ES2017"
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/app-dir/modularizeimports/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"name": "next"
}
],
"strictNullChecks": true
"strictNullChecks": true,
"target": "ES2017"
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
Expand Down
Loading