Don't suppress usage of deprecated TypeScript compilerOptions (#93377)

Co-authored-by: Dmitrii Troitskii <[email protected]>
This commit is contained in:
Sebastian "Sebbie" Silbermann
2026-05-05 17:23:34 +02:00
committed by GitHub
co-authored by Dmitrii Troitskii
parent 47bcfa0956
commit 5150cafadb
9 changed files with 96 additions and 132 deletions
@@ -1,79 +1,9 @@
import { bold, cyan } from '../picocolors'
import os from 'os'
import path from 'path'
import semver from 'next/dist/compiled/semver'
import isError from '../is-error'
function resolvePathAliasTarget(baseUrl: string, target: string): string {
if (
path.isAbsolute(target) ||
target.startsWith('./') ||
target.startsWith('../')
) {
return target
}
if (baseUrl === '.' || baseUrl === './') {
return `./${target}`
}
const resolvedTarget = path.join(baseUrl, target)
if (
path.isAbsolute(resolvedTarget) ||
resolvedTarget.startsWith('./') ||
resolvedTarget.startsWith('../')
) {
return resolvedTarget
}
return `./${resolvedTarget}`
}
function rewritePathAliasesWithoutBaseUrl(
baseUrl: string,
originalPaths: unknown
): Record<string, unknown> {
const rewrittenPaths: Record<string, unknown> =
originalPaths && typeof originalPaths === 'object'
? Object.fromEntries(
Object.entries(originalPaths).map(([key, values]) => [
key,
Array.isArray(values)
? values.map((value) =>
typeof value === 'string'
? resolvePathAliasTarget(baseUrl, value)
: value
)
: values,
])
)
: {
'*': [resolvePathAliasTarget(baseUrl, '*')],
}
if (!Object.prototype.hasOwnProperty.call(rewrittenPaths, '*')) {
rewrittenPaths['*'] = [resolvePathAliasTarget(baseUrl, '*')]
}
return rewrittenPaths
}
function getNormalizedBaseUrlForPaths(
baseUrl: string,
tsConfigPath: string
): string {
const tsConfigDir = path.resolve(path.dirname(tsConfigPath))
const absoluteBaseUrl = path.isAbsolute(baseUrl)
? baseUrl
: baseUrl.startsWith('./') || baseUrl.startsWith('../')
? path.resolve(tsConfigDir, baseUrl)
: path.resolve(baseUrl)
const relativeBaseUrl = path.relative(tsConfigDir, absoluteBaseUrl)
return relativeBaseUrl || '.'
}
export async function getTypeScriptConfiguration(
typescript: typeof import('typescript'),
tsConfigPath: string,
@@ -95,49 +25,6 @@ export async function getTypeScriptConfiguration(
}
let configToParse: any = config
if (semver.gte(typescript.version, '6.0.0')) {
const target = configToParse.compilerOptions?.target
if (
typeof target === 'string' &&
(target.toLowerCase() === 'es3' || target.toLowerCase() === 'es5')
) {
const { target: _target, ...restCompilerOptions } =
configToParse.compilerOptions ?? {}
// TypeScript 6 deprecates ES3/ES5 targets. Rewrite deprecated
// targets in-memory to keep typechecking working without requiring
// `ignoreDeprecations`.
configToParse = {
...configToParse,
compilerOptions: {
...restCompilerOptions,
target: 'es2015',
},
}
}
const baseUrl = configToParse.compilerOptions?.baseUrl
const hasBaseUrl = typeof baseUrl === 'string' && baseUrl.length > 0
if (hasBaseUrl) {
const rewrittenPaths = rewritePathAliasesWithoutBaseUrl(
baseUrl,
configToParse.compilerOptions?.paths
)
const { baseUrl: _baseUrl, ...restCompilerOptions } =
configToParse.compilerOptions ?? {}
// TypeScript 6 deprecates `baseUrl`; rewrite aliases to explicit
// relative paths so path mapping still works without this option.
configToParse = {
...configToParse,
compilerOptions: {
...restCompilerOptions,
paths: rewrittenPaths,
},
}
}
}
const result = typescript.parseJsonConfigFileContent(
configToParse,
@@ -154,25 +41,6 @@ export async function getTypeScriptConfiguration(
path.dirname(tsConfigPath)
)
if (semver.gte(typescript.version, '6.0.0')) {
const parsedBaseUrl = result.options.baseUrl
if (typeof parsedBaseUrl === 'string' && parsedBaseUrl.length > 0) {
const normalizedBaseUrl = getNormalizedBaseUrlForPaths(
parsedBaseUrl,
tsConfigPath
)
// `baseUrl` can come from extended tsconfigs. Rewrite paths from the
// fully-resolved baseUrl and remove it so TS6 deprecation checks do not
// fail type checking.
result.options.paths = rewritePathAliasesWithoutBaseUrl(
normalizedBaseUrl,
result.options.paths
) as import('typescript').MapLike<string[]>
delete (result.options as { baseUrl?: unknown }).baseUrl
}
}
if (result.errors) {
result.errors = result.errors.filter(
({ code }) =>
@@ -0,0 +1 @@
export const greet = (name: string): string => 'Hello, ' + name + '!'
@@ -0,0 +1,2 @@
packages:
- web
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"paths": {
"@shared/*": ["packages/shared/src/*"]
}
}
}
@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,5 @@
import { greet } from '@shared/index'
export default function Page() {
return <p>{greet('world')}</p>
}
@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const config = {
typescript: {
ignoreBuildErrors: false,
},
}
module.exports = config
@@ -0,0 +1,21 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
@@ -0,0 +1,40 @@
import path from 'path'
import { FileRef, nextTestSetup } from 'e2e-utils'
// Reproduces https://github.com/vercel/next.js/issues/93336
describe('typescript paths with deprecated, inherited baseUrl', () => {
const fixtureDir = path.join(__dirname, 'fixtures/inherited')
const { skipped, next } = nextTestSetup({
files: {
'../pnpm-workspace.yaml': new FileRef(
path.join(fixtureDir, 'pnpm-workspace.yaml')
),
'../tsconfig.base.json': new FileRef(
path.join(fixtureDir, 'tsconfig.base.json')
),
'../packages': new FileRef(path.join(fixtureDir, 'packages')),
'next.config.js': new FileRef(
path.join(fixtureDir, 'web/next.config.js')
),
'tsconfig.json': new FileRef(path.join(fixtureDir, 'web/tsconfig.json')),
app: new FileRef(path.join(fixtureDir, 'web/app')),
},
subDir: 'web',
dependencies: {
// Specifically testing compilerOptions that were deprecated in 6.0.
// This test would fail in 7.0
typescript: '^6.0.0',
},
// Only next-build is interesting here.
skipDeployment: true,
})
if (skipped) {
return
}
it('should render the page that uses the aliased module', async () => {
const html = await next.render('/')
expect(html).toContain('Hello, world!')
})
})