Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: useTypeScriptCli
description: Run the project-local TypeScript CLI for type checking during production builds.
version: experimental
---

The `experimental.useTypeScriptCli` option makes `next build` run the project-local `tsc` command instead of loading the TypeScript JavaScript compiler API. You can use this option with TypeScript 6, and it enables [TypeScript 7](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/) support while its JavaScript API is unavailable.

Install TypeScript 7 in your project:

```bash package="pnpm"
pnpm add -D typescript@^7
```

```bash package="npm"
npm install -D typescript@^7
```

```bash package="yarn"
yarn add -D typescript@^7
```

```bash package="bun"
bun add -D typescript@^7
```

Then, explicitly enable the CLI checker:

```ts filename="next.config.ts" switcher
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
experimental: {
useTypeScriptCli: true,
},
}

export default nextConfig
```

```js filename="next.config.js" switcher
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
useTypeScriptCli: true,
},
}

module.exports = nextConfig
```

Next.js does not select the CLI checker automatically. If TypeScript 7 is installed without this option, `next build` exits with instructions to enable it or install a TypeScript version supported by the default checker.

## Behavior

- Next.js continues to generate `next-env.d.ts` and route types and to apply its recommended `tsconfig` settings before running the checker.
- TypeScript diagnostics are printed directly from `tsc`. Next.js-specific code frames and error rewriting are not applied.
- The complete project selected by the configured `tsconfig` file is checked, including test files and `.next/dev/types` when included. The [`--debug-build-paths`](/docs/app/api-reference/cli/next#next-build-options) option does not limit this set and produces a warning when combined with the CLI checker.
- [`typescript.tsconfigPath`](/docs/app/api-reference/config/typescript#custom-tsconfig-path) selects the project passed to `tsc`.
- [`typescript.ignoreBuildErrors`](/docs/app/api-reference/config/typescript#disabling-typescript-errors-in-production) skips the type-checking step, including the CLI checker.
Comment thread
lukesandberg marked this conversation as resolved.

Learn more about [using TypeScript 7 with Next.js](/docs/app/api-reference/config/typescript#using-typescript-7).
56 changes: 55 additions & 1 deletion docs/01-app/03-api-reference/05-config/02-typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,60 @@ To add TypeScript to an existing project, rename a file to `.ts` / `.tsx`. Run `

> **Good to know**: If you already have a `jsconfig.json` file, copy the `paths` compiler option from the old `jsconfig.json` into the new `tsconfig.json` file, and delete the old `jsconfig.json` file.

## Using TypeScript 7

[TypeScript 7](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/) does not currently provide the JavaScript compiler API that Next.js uses for type checking by default. To use TypeScript 7 during `next build`, install it in your project:

```bash package="pnpm"
pnpm add -D typescript@^7
```

```bash package="npm"
npm install -D typescript@^7
```

```bash package="yarn"
yarn add -D typescript@^7
```

```bash package="bun"
bun add -D typescript@^7
```

Then, opt in to running the project-local `tsc` CLI instead of the JavaScript API with [`experimental.useTypeScriptCli`](/docs/app/api-reference/config/next-config-js/useTypeScriptCli):

```ts filename="next.config.ts" switcher
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
experimental: {
useTypeScriptCli: true,
},
}

export default nextConfig
```

```js filename="next.config.js" switcher
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
useTypeScriptCli: true,
},
}

module.exports = nextConfig
```

Next.js does not enable this option automatically. If you install TypeScript 7 without enabling `experimental.useTypeScriptCli`, `next build` exits with instructions to enable the option or install a TypeScript version supported by the default checker.

> **Good to know**:
>
> - CLI type checking prints the native `tsc` diagnostics. It does not apply Next.js-specific code frames or rewrite errors for routes, pages, layouts, or route handlers.
> - The CLI checks the complete project selected by your `tsconfig` file. This includes test files and `.next/dev/types` when they are included by that configuration. [`next build --debug-build-paths`](/docs/app/api-reference/cli/next#next-build-options) does not narrow the files that are type checked and produces a warning when used with this option.
> - [`typescript.tsconfigPath`](#custom-tsconfig-path) continues to select the configuration passed to `tsc`. [`typescript.ignoreBuildErrors`](#disabling-typescript-errors-in-production) skips the type-checking step, including the CLI checker.
> - `experimental.useTypeScriptCli` is experimental and its behavior may change.

<AppOnly>

## IDE Plugin
Expand All @@ -31,7 +85,7 @@ You can enable the plugin in VS Code by:
height="637"
/>

Now, when editing files, the custom plugin will be enabled. When running `next build`, the custom type checker will be used.
Now, when editing files, the custom plugin will be enabled. By default, the custom type checker is used when running `next build`. When [`experimental.useTypeScriptCli`](#using-typescript-7) is enabled, the project-local `tsc` CLI is used instead.

The TypeScript plugin can help with:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: useTypeScriptCli
description: Run the project-local TypeScript CLI for type checking during production builds.
source: app/api-reference/config/next-config-js/useTypeScriptCli
version: experimental
---

{/* DO NOT EDIT. The content of this doc is generated from the source above. To edit the content of this page, navigate to the source page in your editor. You can use the `<PagesOnly>Content</PagesOnly>` component to add content that is specific to the Pages Router. Any shared content should not be wrapped in a component. */}
5 changes: 4 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -1414,5 +1414,8 @@
"1413": "Invalid \\`cacheLife()\\` option \"stale\" provided, expected a finite number of seconds or Infinity, received %s.",
"1414": "The %s option must be a number of seconds.",
"1415": "Unknown cacheLife option %s",
"1416": "Invalid \"%s\" provided, expected a finite number of seconds or Infinity, received %s"
"1416": "Invalid \"%s\" provided, expected a finite number of seconds or Infinity, received %s",
"1417": "Could not parse output from TypeScript's --showConfig.",
"1418": "TypeScript %s does not provide the compiler API required by Next.js. Enable %s in your Next.js config to use the TypeScript CLI, or install TypeScript 6 instead.",
"1419": "TypeScript CLI interrupted by %s"
}
65 changes: 47 additions & 18 deletions packages/next/src/build/load-jsconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ import * as Log from './output/log'
import { getTypeScriptConfiguration } from '../lib/typescript/getTypeScriptConfiguration'
import { readFileSync } from 'fs'
import isError from '../lib/is-error'
import { hasNecessaryDependencies } from '../lib/has-necessary-dependencies'
import { codeFrameColumns } from '../shared/lib/errors/code-frame'
import {
getTypeScriptApiMissingError,
getTypeScriptConfigurationCli,
getTypeScriptPackageInfo,
hasNativeTypeScriptPreview,
} from '../lib/typescript/runTypeScriptCli'
import { loadTsConfigOptions } from '../lib/typescript/loadTsConfig'

let TSCONFIG_WARNED = false

Expand Down Expand Up @@ -57,19 +63,24 @@ export default async function loadJsConfig(
jsConfigPath?: string
resolvedBaseUrl: ResolvedBaseUrl
}> {
let typeScriptPath: string | undefined
try {
const deps = hasNecessaryDependencies(dir, [
{
pkg: 'typescript',
file: 'typescript/lib/typescript.js',
exportsRestrict: true,
},
])
typeScriptPath = deps.resolved.get('typescript')
} catch {}
const useTypeScriptCli = Boolean(config.experimental.useTypeScriptCli)
const typeScriptPackage = getTypeScriptPackageInfo(dir)
const typeScriptPath = useTypeScriptCli
? typeScriptPackage?.tscPath
: typeScriptPackage?.apiPath
const tsConfigFileName = config.typescript.tsconfigPath || 'tsconfig.json'
const tsConfigPath = path.join(dir, tsConfigFileName)

if (
!useTypeScriptCli &&
typeScriptPackage &&
!typeScriptPackage.apiPath &&
!hasNativeTypeScriptPreview(dir) &&
fs.existsSync(tsConfigPath)
) {
throw getTypeScriptApiMissingError(typeScriptPackage.version)
}

const useTypeScript = Boolean(typeScriptPath && fs.existsSync(tsConfigPath))

let implicitBaseurl
Expand All @@ -81,11 +92,26 @@ export default async function loadJsConfig(
Log.info(`Using tsconfig file: ${tsConfigFileName}`)
}

const ts = (await Promise.resolve(
require(typeScriptPath!)
)) as typeof import('typescript')
const tsConfig = await getTypeScriptConfiguration(ts, tsConfigPath, true)
jsConfig = { compilerOptions: tsConfig.options }
if (useTypeScriptCli) {
const tsConfig = await getTypeScriptConfigurationCli({
baseDir: dir,
tsConfigPath,
tscPath: typeScriptPath!,
})
const configOrigins = loadTsConfigOptions(tsConfigPath)
jsConfig = {
compilerOptions: {
...tsConfig.compilerOptions,
pathsBasePath: configOrigins.pathsBasePath,
},
}
} else {
const ts = (await Promise.resolve(
require(typeScriptPath!)
)) as typeof import('typescript')
const tsConfig = await getTypeScriptConfiguration(ts, tsConfigPath, true)
jsConfig = { compilerOptions: tsConfig.options }
}
implicitBaseurl = path.dirname(tsConfigPath)
}

Expand All @@ -98,7 +124,10 @@ export default async function loadJsConfig(
let resolvedBaseUrl: ResolvedBaseUrl
if (jsConfig?.compilerOptions?.baseUrl) {
resolvedBaseUrl = {
baseUrl: path.resolve(dir, jsConfig.compilerOptions.baseUrl),
baseUrl: path.resolve(
implicitBaseurl ?? dir,
jsConfig.compilerOptions.baseUrl
),
isImplicit: false,
}
} else {
Expand Down
91 changes: 6 additions & 85 deletions packages/next/src/build/next-config-ts/transpile-config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { Options as SWCOptions } from '@swc/core'
import type { CompilerOptions } from 'typescript'

import path from 'node:path'
import { readFileSync, existsSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import * as CommentJson from 'next/dist/compiled/comment-json'
import { deregisterHook, registerHook, requireFromString } from './require-hook'
import { warn, warnOnce } from '../output/log'
import { getNodeOptionsArgs } from '../../server/lib/utils'

type RelevantCompilerOptions = Pick<CompilerOptions, 'paths' | 'baseUrl'>
import {
loadTsConfigOptions,
type RelevantCompilerOptions,
} from '../../lib/typescript/loadTsConfig'

function resolveSWCOptions(
cwd: string,
Expand All @@ -26,7 +26,7 @@ function resolveSWCOptions(
{ baseUrl: path.resolve(cwd, compilerOptions.baseUrl) }
: compilerOptions.paths
? // If paths is given, baseUrl is required.
{ baseUrl: cwd }
{ baseUrl: compilerOptions.pathsBasePath ?? cwd }
: {}),
},
module: {
Expand All @@ -42,85 +42,6 @@ function resolveSWCOptions(
} satisfies SWCOptions
}

function resolveExtends(extendsPath: string, currentConfigDir: string): string {
// Relative paths are resolved relative to the current config's directory
if (
extendsPath.startsWith('./') ||
extendsPath.startsWith('../') ||
path.isAbsolute(extendsPath)
) {
const resolved = path.resolve(currentConfigDir, extendsPath)
// TypeScript allows omitting .json extension
if (existsSync(resolved)) {
return resolved
}
if (!resolved.endsWith('.json') && existsSync(resolved + '.json')) {
return resolved + '.json'
}
return resolved
}

// Package paths - use require.resolve to find the package
try {
// Try resolving as a direct path within the package
return require.resolve(extendsPath, { paths: [currentConfigDir] })
} catch {
// If that fails, try appending tsconfig.json for package names like "@tsconfig/node18"
try {
return require.resolve(extendsPath + '/tsconfig.json', {
paths: [currentConfigDir],
})
} catch {
// Return the original path and let it fail later with a clear error
return path.resolve(currentConfigDir, extendsPath)
}
}
}

function loadTsConfigFile(
configPath: string,
visited: Set<string>
): RelevantCompilerOptions {
const resolvedPath = path.resolve(configPath)

if (visited.has(resolvedPath)) {
return {}
}
visited.add(resolvedPath)

if (!existsSync(resolvedPath)) {
return {}
}

const configContent = readFileSync(resolvedPath, 'utf8')
const config = CommentJson.parse(configContent)
const configDir = path.dirname(resolvedPath)

let mergedOptions: RelevantCompilerOptions = {}

// Note that config options from `extends` should get overwritten, not merged
if (config.extends) {
const extendsList = Array.isArray(config.extends)
? config.extends
: [config.extends]

for (const extendsPath of extendsList) {
const parentConfigPath = resolveExtends(extendsPath, configDir)
const parentOptions = loadTsConfigFile(parentConfigPath, visited)
mergedOptions = { ...mergedOptions, ...parentOptions }
}
}

const currentOptions = config.compilerOptions ?? {}
mergedOptions = {
...mergedOptions,
paths: currentOptions.paths ?? mergedOptions.paths,
baseUrl: currentOptions.baseUrl ?? mergedOptions.baseUrl,
}

return mergedOptions
}

async function loadTsConfig(dir: string): Promise<RelevantCompilerOptions> {
// NOTE: This doesn't fully cover the edge case for setting
// "typescript.tsconfigPath" in next config which is currently
Expand All @@ -133,7 +54,7 @@ async function loadTsConfig(dir: string): Promise<RelevantCompilerOptions> {
return {}
}

return loadTsConfigFile(resolvedTsConfigPath, new Set())
return loadTsConfigOptions(resolvedTsConfigPath)
}

export async function transpileConfig({
Expand Down
Loading
Loading