Skip to content

getTypeScriptConfiguration: TypeScript 6 path rewriting deletes baseUrl but does not update pathsBasePath, breaking module resolution in monorepos with shared base tsconfig #93336

Description

@gardsa

Link to the code that reproduces this issue

https://github.com/gardsa/my-minimal-nextjs-issue-reproduction

To Reproduce

  1. Clone the reproduction repository (linked above)
  2. From the repo root, install dependencies:
    npm install
  3. Run the build from the app directory:
    cd apps/my-app && npx next build
  4. Observe that Turbopack/webpack compilation succeeds but the TypeScript type-check step immediately after fails:
    Type error: Cannot find module '@scope/shared-lib/index' or its corresponding type declarations.
    

Current vs. Expected behavior

Expected: next build completes successfully. Path aliases defined in the shared tsconfig.base.json resolve correctly for both webpack and the TypeScript type-checker.

Actual: Webpack/Turbopack compilation succeeds, but the TypeScript check step that runs immediately after fails:

Running TypeScript ...
Failed to type check.

./src/app/page.tsx:1:26
Type error: Cannot find module '@scope/shared-lib/index' or its corresponding type declarations.

This affects every path alias defined in the shared base tsconfig. Aliases defined directly in the app-level tsconfig are not affected.

Provide environment information

Operating System:
  Platform: linux
  Arch: arm64
Binaries:
  Node: 24.14.1
  npm: 11.11.0
  Yarn: 1.22.22
Relevant Packages:
  next: 16.2.4
  react: 19.2.5
  react-dom: 19.2.5
  typescript: 6.0.3

Which area(s) are affected? (Select all that apply)

TypeScript, Module Resolution

Which stage(s) are affected? (Select all that apply)

next build (local)

Additional context

Root cause

getTypeScriptConfiguration.js adds a TypeScript 6 compatibility block that rewrites paths to be relative to the app dir and deletes baseUrl:

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);
        result.options.paths = rewritePathAliasesWithoutBaseUrl(normalizedBaseUrl, result.options.paths);
        delete result.options.baseUrl;
        // ← pathsBasePath is NOT updated here
    }
}

For an app at apps/my-app/, normalizedBaseUrl is '../..', so "libs/shared-lib/src/*" becomes "../../libs/shared-lib/src/*" — correct relative to the app dir.

After baseUrl is deleted, TypeScript's module resolver calls getPathsBasePath:

// typescript/lib/typescript.js
function getPathsBasePath(options, host) {
    if (!options.paths) return undefined;
    return options.baseUrl ??
        options.pathsBasePath ||
        host.getCurrentDirectory();
}

TypeScript sets pathsBasePath to the directory of the tsconfig that defines the paths key. In a monorepo this is the repo root (where tsconfig.base.json lives), not the app directory.

TypeScript therefore resolves "../../libs/shared-lib/src/*" relative to the repo root:

path.join('/repo-root', '../../libs/shared-lib/src/foo')
// → '/libs/shared-lib/src/foo'   ← does not exist

Why webpack/Turbopack works but TypeScript does not: load-jsconfig.js uses path.dirname(tsConfigPath) (the app dir) as the implicit baseUrl fallback when baseUrl is absent. JsConfigPathsPlugin resolves the rewritten ../../... paths relative to the app dir, which is correct. The TypeScript checker goes through the same getTypeScriptConfiguration call but uses pathsBasePath instead, which points at the wrong base directory.

Proposed fix

After deleting baseUrl, set pathsBasePath to path.dirname(tsConfigPath) so TypeScript uses the same resolution base as webpack:

result.options.paths = rewritePathAliasesWithoutBaseUrl(normalizedBaseUrl, result.options.paths);
delete result.options.baseUrl;
result.options.pathsBasePath = path.dirname(tsConfigPath); // ← add this

This ensures the rewritten ../../libs/... paths are resolved from the app's tsconfig directory, producing the correct absolute path regardless of where in the extends chain paths were originally defined.

Workaround

None that avoids modifying node_modules. Pinning to Next.js 16.1.0 is not viable because that version predates the TypeScript 6 compatibility code entirely — TypeScript 6 rejects baseUrl as deprecated before Next.js can influence resolution. The getTypeScriptConfiguration.js compat code in 16.2.x is the correct approach; it is just missing the one-line pathsBasePath update.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Module ResolutionModule resolution (CJS / ESM, module resolving).TypeScriptRelated to types with Next.js.locked

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions