Repository: vercel/next.js
Pull request: Fix TS6 baseUrl deprecation for extended tsconfig

## Summary
- handle `compilerOptions.baseUrl` inherited via `extends` in TS6 config normalization
- rewrite `paths` using the resolved base URL and remove `baseUrl` from parsed compiler options
- keep existing behavior for direct `baseUrl` while avoiding TS5101 in typecheck

follow-up to https://github.com/vercel/next.js/pull/91847

## Validation
- `IS_WEBPACK_TEST=1 NEXT_TEST_MODE=start NEXT_TEST_REACT_VERSION=18.3.1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm testheadless test/e2e/app-dir/next-config-ts/tsconfig-extends/next-config-ts-tsconfig-extends-esm.test.ts`
- `IS_WEBPACK_TEST=1 NEXT_TEST_MODE=start NEXT_TEST_REACT_VERSION=18.3.1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm testheadless test/e2e/app-dir/next-config-ts/import-alias-paths-with-baseurl/next-config-ts-import-alias-paths-with-baseurl-esm.test.ts`


Changed files:
filename: packages/next/src/build/index.ts
patch: @@ -219,6 +219,7 @@ import {
   writeRouteTypesManifest,
   writeValidatorFile,
 } from '../server/lib/router-utils/route-types-utils'
+import { writeCacheLifeTypes } from '../server/lib/router-utils/cache-life-type-utils'
 import { Lockfile } from './lockfile'
 import {
   buildPrefetchSegmentDataRoute,
@@ -1391,6 +1392,11 @@ export default async function build(
         .traceAsyncFn(async () => {
           const routeTypesFilePath = path.join(distDir, 'types', 'routes.d.ts')
           const validatorFilePath = path.join(distDir, 'types', 'validator.ts')
+          const cacheLifeFilePath = path.join(
+            distDir,
+            'types',
+            'cache-life.d.ts'
+          )
           await mkdir(path.dirname(routeTypesFilePath), { recursive: true })
 
           const routeTypesManifest = await createRouteTypesManifest({
@@ -1416,6 +1422,7 @@ export default async function build(
             validatorFilePath,
             Boolean(config.experimental.strictRouteTypes)
           )
+          writeCacheLifeTypes(config.cacheLife, cacheLifeFilePath)
         })
 
       // Turbopack already handles conflicting app and page routes.

filename: packages/next/src/lib/typescript/getTypeScriptConfiguration.ts
patch: @@ -31,6 +31,50 @@ function resolvePathAliasTarget(baseUrl: string, target: string): string {
   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,
@@ -79,27 +123,10 @@ export async function getTypeScriptConfiguration(
       const hasBaseUrl = typeof baseUrl === 'string' && baseUrl.length > 0
 
       if (hasBaseUrl) {
-        const originalPaths = configToParse.compilerOptions?.paths
-        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, '*')]
-        }
+        const rewrittenPaths = rewritePathAliasesWithoutBaseUrl(
+          baseUrl,
+          configToParse.compilerOptions?.paths
+        )
         const { baseUrl: _baseUrl, ...restCompilerOptions } =
           configToParse.compilerOptions ?? {}
 
@@ -130,6 +157,25 @@ 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 }) =>

filename: test/production/app-dir/types-gen-nounuselocal/types-gen-nounuselocal.test.ts
patch: @@ -31,6 +31,12 @@ describe('types-gen-nounuselocal', () => {
     expect(routeTypes).not.toContain('ApiRouteConfig')
 
     if (strictRouteTypes) {
+      const cacheLifeTypes = await fsp.readFile(
+        path.join(next.testDir, '.next', 'types', 'cache-life.d.ts'),
+        'utf-8'
+      )
+      expect(cacheLifeTypes).toContain("declare module 'next/cache'")
+
       expect(routeTypes).toMatchInlineSnapshot(`
        "// This file is generated automatically by Next.js with experimental.strictRouteTypes
        // Do not edit this file manually