nrwl / nx

Smart Monorepos · Fast CI
https://nx.dev
MIT License
22.73k stars 2.27k forks source link

TypeScript Executor Mangles Nested Subpaths #21699

Open aidant opened 5 months ago

aidant commented 5 months ago

Current Behavior

Importing a nested subpath export from a project in the same Nx workspace fails with a cannot find module or its corresponding type declarations typescript compilation error. This error is only present in the @nx/js:tsc executor and is not present in the typescript language server or when running typescript compiler directly (tsc --noEmit --project ...).

Expected Behavior

The Nx TypeScript executor should produce valid paths when magically swapping in the output directory.

GitHub Repo

No response

Steps to Reproduce

We have a package with a nested subpath export for example @nx/js/plugins/jest/local-registry. Except rather than a file at that path we are following the guide from your typescript executor to define additional entrypoints, our subpath export lives in the source directory for example packages/js/src/plugins/jest/local-registry.ts and we are relying on the generate exports field to correctly generate a ./plugins/jest/local-registry subpath export. This all works perfectly, where it falls apart is in the definition of the paths aliases for TypeScript, we have defined the following paths for the package:

      "@nx/js": ["packages/js/src/index.ts"],
      "@nx/js/plugins/jest/local-registry": ["packages/js/src/plugins/jest/local-registry.ts"],

When running the TypeScript executor with the NX_VERBOSE_LOGGING_PATH_MAPPINGS environment variable we see the following output for the respective packages:

 "@nx/js": [
    "dist/packages/js"
  ],
  "@nx/js/plugins/jest/local-registry": [
    "dist/packages/js/plugins/jest/local-registry",
    "dist/packages/js/src/plugins/jest/local-registry.ts"
  ],

In our case dist/packages/js/plugins/jest/local-registry is not a valid path and dist/packages/js/src/plugins/jest/local-registry.ts has a bogus file extension.

  1. configure project A for nested additional entrypoints like described in this doc
  2. update tsconfig paths to include the nested subpath exports
  3. update project B to use the nested subpath export from project A
  4. run the typescript executor for project B

Nx Report

   Node   : 18.17.0
   OS     : darwin-arm64
   npm    : 9.8.1

   nx                 : 18.0.0
   @nx/js             : 18.0.0
   @nx/jest           : 18.0.0
   @nx/linter         : 18.0.0
   @nx/eslint         : 18.0.0
   @nx/workspace      : 18.0.0
   @nx/angular        : 18.0.0
   @nx/cypress        : 18.0.0
   @nx/devkit         : 18.0.0
   @nx/eslint-plugin  : 18.0.0
   @nx/react          : 18.0.0
   @nrwl/tao          : 18.0.0
   @nx/web            : 18.0.0
   @nx/webpack        : 18.0.0
   typescript         : 5.3.3
   ---------------------------------------
   Community plugins:
   @ngrx/component-store : 17.0.1
   @ngrx/effects         : 17.0.1
   @ngrx/entity          : 17.0.1
   @ngrx/router-store    : 17.0.1
   @ngrx/store           : 17.0.1
   @ngrx/store-devtools  : 17.0.1

Failure Logs

No response

Package Manager Version

No response

Operating System

Additional Information

We are currently working around the issue by dropping the .ts file extension from our TypeScript path mappings for those nested subpath exports and applying the following patch:

diff --git a/node_modules/@nx/js/src/utils/buildable-libs-utils.js b/node_modules/@nx/js/src/utils/buildable-libs-utils.js
index 498537c..5ca00ec 100644
--- a/node_modules/@nx/js/src/utils/buildable-libs-utils.js
+++ b/node_modules/@nx/js/src/utils/buildable-libs-utils.js
@@ -282,7 +282,7 @@ function updatePaths(dependencies, paths) {
                 if (path.startsWith(nestedName)) {
                     const nestedPart = path.slice(nestedName.length);
                     // Bind secondary endpoints for ng-packagr projects
-                    let mappedPaths = dep.outputs.map((output) => `${output}/${nestedPart}`);
+                    let mappedPaths = [];
                     // Get the dependency's package name
                     const { root } = (dep.node?.data || {});
                     if (root) {
aidant commented 5 months ago

We have reverted the patch to Nx and instead using aliases to link the files up to their correct locations. This behaves better with the jest executor which prefers to have the .ts extensions in the path aliases, and behaves well in other packages outside the monorepo which have their module resolution set to node.

An example of the new solution we are using looks like the following:

// packages/js/plugins/jest/local-registry.js
moudle.exports = exports = require('./src/plugins/jest/local-registry.js')
// packages/js/plugins/jest/local-registry.d.ts
export * from './src/plugins/jest/local-registry'

With that said, it would still be good to resolve this issue without a workaround as I am treating the packages outside the monorepo as out of scope since the solution to that is to change their module resolution to either node16/nodenext or bundler.