Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Building projects with @nrwl/js:tsc and @nrwl/js:swc fails when "moduleResolution": "nodenext", "type": "module", and "module": "ESNext", with error TS2307: Cannot find module or its corresponding type declarations. #15464

Closed
tefkah opened this issue Mar 6, 2023 · 9 comments
Assignees

Comments

@tefkah
Copy link

tefkah commented Mar 6, 2023

Current Behavior

When I try to build a project instantiated with @nrwl/js:lib with moduleResolution: nodenext (or node16) in my tsconfig.base.json, the build always fails claiming that it cannot find one of the dependent modules that are listed in the paths of tsconfig.base.json.

// libs/base/src/lib/base.ts
export function base(): string {
  return 'base';
}
// libs/test/src/lib/test.ts
import { base } from 'base';

export function test(): string {
  return base();
}

Specifically this only seems to happen if I try to create an ESM only module, if I compile to commonjs everythings fine.

When running tsc -P libs/test/tsconfig.lib.json the project builds as expected with no errors.

Specifically, when enabling traceResolution: true, I can see that when running nx build test (either when using @nrwl/js:tsc or @nrwl/swc:tsc), it tries to resolve the dependent module

Running pnpm tsc -P libs/test/tsconfig.lib.json

======== Resolving module 'base' from '/project/home/tefkah/workspace/libs/test/src/lib/test.ts'. ========
Explicitly specified module resolution kind: 'NodeNext'.
Resolving in ESM mode with conditions 'node', 'import', 'types'.
'baseUrl' option is set to '/project/home/tefkah/workspace', using this value to resolve non-relative module name 'base'.
'paths' option is specified, looking for a pattern to match module name 'base'.
Module name 'base', matched pattern 'base'.
Trying substitution 'libs/base/src/index.ts', candidate module location: 'libs/base/src/index.ts'.
File '/project/home/tefkah/workspace/libs/base/src/index.ts' exist - use it as a name resolution result.
======== Module name 'base' was successfully resolved to '/project/home/tefkah/workspace/libs/base/src/index.ts'. ========

Running pnpm nx build test

======== Resolving module 'base' from '/project/home/tefkah/workspace/libs/test/src/lib/test.ts'. ========
Explicitly specified module resolution kind: 'NodeNext'.
Resolving in ESM mode with conditions 'node', 'import', 'types'.
'baseUrl' option is set to '/project/home/tefkah/workspace', using this value to resolve non-relative module name 'base'.
'paths' option is specified, looking for a pattern to match module name 'base'.
Module name 'base', matched pattern 'base'.
Trying substitution 'dist/libs/base', candidate module location: 'dist/libs/base'.
Loading module as file / folder, candidate module location '/project/home/tefkah/workspace/dist/libs/base', target file type 'TypeScript'.
File '/project/home/tefkah/workspace/libs/test/src/lib/package.json' does not exist according to earlier cached lookups.
File '/project/home/tefkah/workspace/libs/test/src/package.json' does not exist according to earlier cached lookups.
File '/project/home/tefkah/workspace/libs/test/package.json' exists according to earlier cached lookups.
Loading module 'base' from 'node_modules' folder, target file type 'TypeScript'.
Directory '/project/home/tefkah/workspace/libs/test/src/lib/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/tefkah/workspace/libs/test/src/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/tefkah/workspace/libs/test/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/tefkah/workspace/libs/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/tefkah/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/node_modules' does not exist, skipping all lookups in it.
Directory '/project/node_modules' does not exist, skipping all lookups in it.
Directory '/node_modules' does not exist, skipping all lookups in it.
'baseUrl' option is set to '/project/home/tefkah/workspace', using this value to resolve non-relative module name 'base'.
'paths' option is specified, looking for a pattern to match module name 'base'.
Module name 'base', matched pattern 'base'.
Trying substitution 'dist/libs/base', candidate module location: 'dist/libs/base'.
Loading module as file / folder, candidate module location '/project/home/tefkah/workspace/dist/libs/base', target file type 'JavaScript'.
File '/project/home/tefkah/workspace/libs/test/src/lib/package.json' does not exist according to earlier cached lookups.
File '/project/home/tefkah/workspace/libs/test/src/package.json' does not exist according to earlier cached lookups.
File '/project/home/tefkah/workspace/libs/test/package.json' exists according to earlier cached lookups.
Loading module 'base' from 'node_modules' folder, target file type 'JavaScript'.
Directory '/project/home/tefkah/workspace/libs/test/src/lib/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/tefkah/workspace/libs/test/src/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/tefkah/workspace/libs/test/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/tefkah/workspace/libs/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/tefkah/node_modules' does not exist, skipping all lookups in it.
Directory '/project/home/node_modules' does not exist, skipping all lookups in it.
Directory '/project/node_modules' does not exist, skipping all lookups in it.
Directory '/node_modules' does not exist, skipping all lookups in it.
======== Module name 'base' was not resolved. ========

Expected Behavior

The build step should resolve the modules correctly.

GitHub Repo

https://github.com/tefkah/nx-nodenext-repro

Steps to Reproduce

  1. Go to the codesandbox
  2. Run pnpm install
  3. Run pnpm nx build test
  4. Run pnpm tsc -P libs/test/tsconfig.lib.json to see correctly functioning compilation.
  5. Look at tsc-log.txt and nx-log.txt for the logs of the operations.

Nx Report

>  NX   Report complete - copy this into the issue template

   Node : 16.17.0
   OS   : linux x64
   pnpm : 7.1.0
   
   nx                      : 15.8.5
   @nrwl/js                : 15.8.5
   @nrwl/linter            : 15.8.5
   @nrwl/workspace         : 15.8.5
   @nrwl/cli               : 15.8.5
   @nrwl/devkit            : 15.8.5
   @nrwl/eslint-plugin-nx  : 15.8.5
   @nrwl/tao               : 15.8.5
   @nrwl/vite              : 15.8.5
   typescript              : 4.9.5

Failure Logs

Compiling TypeScript files for project "test"...
libs/test/src/lib/test.ts:1:22 - error TS2307: Cannot find module 'base' or its corresponding type declarations.

1 import { base } from 'base';
                       ~~~~~~

 ————————————————————————————————————————————————————————————————————————————————————————————

 >  NX   Ran target build for project test and 1 task(s) they depend on (2s)

Additional Information

tsconfig.base.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "rootDir": ".",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "nodeNext",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "ESNext",
    "module": "esnext",
    "lib": ["es2017", "dom"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "base": ["libs/base/src/index.ts"],
      "test": ["libs/test/src/index.ts"]
    }
  },
  "exclude": ["node_modules", "tmp"]
}

libs/test/project.json['targets']['build']

    "build": {
      "executor": "@nrwl/js:tsc",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/libs/test",
        "main": "libs/test/src/index.ts",
        "tsConfig": "libs/test/tsconfig.lib.json",
        "assets": ["libs/test/*.md"]
      }
    }

libs/test/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "types": ["vitest"]
  },
  "files": [],
  "include": [],
  "references": [
    {
      "path": "./tsconfig.lib.json"
    },
    {
      "path": "./tsconfig.spec.json"
    }
  ]
}

libs/test/tsconfig.lib.json

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "../../dist/out-tsc",
    "declaration": true,
    "traceResolution": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts"],
  "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}

libs/test/package.json

{
  "name": "test",
  "version": "0.0.1",
  "type": "module"
}
@tefkah
Copy link
Author

tefkah commented Mar 6, 2023

I fixed this with the following diff for @nrwl/workspace. This checks if moduleResolution is set to node16 or nodenext (which is 3 and 99 respectively for the parsed config), and, if so, tries to read the package.json of the dependency, find the entry file (by looking for exports['.']['import'], module, and main in that order, then appending that to the normal out folder.

This is not super resilient, could use some more checks etc, but it works for me. If there is interest in a PR that implements this I'd be happy to provide one :)

diff --git a/src/utilities/buildable-libs-utils.js b/src/utilities/buildable-libs-utils.js
index 1ec6eb948138ebb4ea2b86cf286a6ca99d38fc38..ac23420a413f324ea81ae2d1ec79c7788d577d0e 100644
--- a/src/utilities/buildable-libs-utils.js
+++ b/src/utilities/buildable-libs-utils.js
@@ -33,42 +33,42 @@ function calculateProjectDependencies(projGraph, root, projectName, targetName,
     }
     const dependencies = collectedDeps
         .map(({ name: dep, isTopLevel }) => {
-        let project = null;
-        const depNode = projGraph.nodes[dep] || projGraph.externalNodes[dep];
-        if (depNode.type === 'lib') {
-            if (isBuildable(targetName, depNode)) {
-                const libPackageJsonPath = (0, path_1.join)(root, depNode.data.root, 'package.json');
+            let project = null;
+            const depNode = projGraph.nodes[dep] || projGraph.externalNodes[dep];
+            if (depNode.type === 'lib') {
+                if (isBuildable(targetName, depNode)) {
+                    const libPackageJsonPath = (0, path_1.join)(root, depNode.data.root, 'package.json');
+                    project = {
+                        name: (0, fileutils_1.fileExists)(libPackageJsonPath)
+                            ? (0, devkit_1.readJsonFile)(libPackageJsonPath).name // i.e. @workspace/mylib
+                            : dep,
+                        outputs: (0, devkit_1.getOutputsForTargetAndConfiguration)({
+                            overrides: {},
+                            target: {
+                                project: projectName,
+                                target: targetName,
+                                configuration: configurationName,
+                            },
+                        }, depNode),
+                        node: depNode,
+                    };
+                }
+                else {
+                    nonBuildableDependencies.push(dep);
+                }
+            }
+            else if (depNode.type === 'npm') {
                 project = {
-                    name: (0, fileutils_1.fileExists)(libPackageJsonPath)
-                        ? (0, devkit_1.readJsonFile)(libPackageJsonPath).name // i.e. @workspace/mylib
-                        : dep,
-                    outputs: (0, devkit_1.getOutputsForTargetAndConfiguration)({
-                        overrides: {},
-                        target: {
-                            project: projectName,
-                            target: targetName,
-                            configuration: configurationName,
-                        },
-                    }, depNode),
+                    name: depNode.data.packageName,
+                    outputs: [],
                     node: depNode,
                 };
             }
-            else {
-                nonBuildableDependencies.push(dep);
+            if (project && isTopLevel) {
+                topLevelDependencies.push(project);
             }
-        }
-        else if (depNode.type === 'npm') {
-            project = {
-                name: depNode.data.packageName,
-                outputs: [],
-                node: depNode,
-            };
-        }
-        if (project && isTopLevel) {
-            topLevelDependencies.push(project);
-        }
-        return project;
-    })
+            return project;
+        })
         .filter((x) => !!x);
     dependencies.sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0));
     return {
@@ -116,10 +116,37 @@ function readTsConfigWithRemappedPaths(tsConfig, generatedTsConfigPath, dependen
  */
 function computeCompilerOptionsPaths(tsConfig, dependencies) {
     const paths = readPaths(tsConfig) || {};
-    updatePaths(dependencies, paths);
+    const moduleResolution = readModule(tsConfig) || 0
+    const needsDirectResolution = moduleResolution > 2
+    updatePaths(dependencies, paths, needsDirectResolution);
     return paths;
 }
 exports.computeCompilerOptionsPaths = computeCompilerOptionsPaths;
+function readModule(tsConfig) {
+    var _a;
+    if (!tsModule) {
+        tsModule = (0, typescript_1.ensureTypescript)();
+    }
+    try {
+        let config;
+        if (typeof tsConfig === 'string') {
+            const configFile = tsModule.readConfigFile(tsConfig, tsModule.sys.readFile);
+            config = tsModule.parseJsonConfigFileContent(configFile.config, tsModule.sys, (0, path_1.dirname)(tsConfig));
+        }
+        else {
+            config = tsConfig;
+        }
+        if ((_a = config.options) === null || _a === void 0 ? void 0 : _a.moduleResolution) {
+            return config.options.moduleResolution;
+        }
+        else {
+            return null;
+        }
+    }
+    catch (e) {
+        return null;
+    }
+}
 function readPaths(tsConfig) {
     var _a;
     if (!tsModule) {
@@ -164,7 +191,7 @@ function cleanupTmpTsConfigFile(tmpTsConfigPath) {
 function checkDependentProjectsHaveBeenBuilt(root, projectName, targetName, projectDependencies) {
     const missing = findMissingBuildDependencies(root, projectName, targetName, projectDependencies);
     if (missing.length > 0) {
-        console.error((0, devkit_1.stripIndents) `
+        console.error((0, devkit_1.stripIndents)`
       It looks like all of ${projectName}'s dependencies have not been built yet:
       ${missing.map((x) => ` - ${x.node.name}`).join('\n')}
 
@@ -193,15 +220,24 @@ function findMissingBuildDependencies(root, projectName, targetName, projectDepe
     return depLibsToBuildFirst;
 }
 exports.findMissingBuildDependencies = findMissingBuildDependencies;
-function updatePaths(dependencies, paths) {
+function updatePaths(dependencies, paths, moduleResolution) {
     const pathsKeys = Object.keys(paths);
     // For each registered dependency
     dependencies.forEach((dep) => {
         var _a;
         // If there are outputs
         if (dep.outputs && dep.outputs.length > 0) {
-            // Directly map the dependency name to the output paths (dist/packages/..., etc.)
-            paths[dep.name] = dep.outputs;
+            // if moduleResolution is node16 or nodenext, we need to add a direct path to the output file
+            // we do this by looking at the main entry of the package.json file of the already compiled package, as its the most reliable way
+            if (moduleResolution) {
+                const outputPackageJSON = (0, path_1.join)(dep.outputs[0], 'package.json')
+                const { main, module, exports } = (0, devkit_1.readJsonFile)(outputPackageJSON)
+                const entry = exports?.['.']?.['import'] ?? module ?? main ?? './index.js'
+                paths[dep.name] = [(0, path_1.join)(dep.outputs[0], entry)]
+            } else {
+                // Directly map the dependency name to the output paths (dist/packages/..., etc.)
+                paths[dep.name] = dep.outputs;
+            }
             // check for secondary entrypoints
             // For each registered path
             for (const path of pathsKeys) {

@tefkah
Copy link
Author

tefkah commented Mar 6, 2023

Scratch that actually, that didn't work at all

@AgentEnder AgentEnder added scope: core core nx functionality scope: js labels Mar 13, 2023
@Maxim-Mazurok
Copy link

Maxim-Mazurok commented Apr 13, 2023

I was having issues using "moduleResolution": "node16" and export * from "./lib/utils.js";

I kinda solved my issue using this patch:

diff --git a/node_modules/tsconfig-paths/lib/register.js b/node_modules/tsconfig-paths/lib/register.js
index e1cda8b..316c197 100644
--- a/node_modules/tsconfig-paths/lib/register.js
+++ b/node_modules/tsconfig-paths/lib/register.js
@@ -95,6 +95,9 @@ function register(params) {
         var isCoreModule = coreModules.hasOwnProperty(request);
         if (!isCoreModule) {
             var found = matchPath(request);
+            if (!found && request.startsWith('./lib/') && request.endsWith('.js')) {
+              found = request.replace('.js', '');
+            }
             if (found) {
                 var modifiedArguments = __spreadArray([found], [].slice.call(arguments, 1), true); // Passes all arguments. Even those that is not specified above.
                 return originalResolveFilename.apply(this, modifiedArguments);

However tsc still can't resolve stuff from dist (see tmpTsConfig).

What helped me is changing from "moduleResolution": "node16" to "moduleResolution": "bundler"

@github-actions
Copy link

This issue has been automatically marked as stale because it hasn't had any recent activity. It will be closed in 14 days if no further activity occurs.
If we missed this issue please reply to keep it active.
Thanks for being a part of the Nx community! 🙏

@kopach
Copy link
Contributor

kopach commented Oct 17, 2023

having the same issue

@kopach
Copy link
Contributor

kopach commented Oct 17, 2023

I found it. In my case, the problem was in name of package in package.json. When generating package in sub-folder with nx, name of package in package.json will be like this "name": "@prefix/sub/folder-package" which is not correct from node perspective (only 1 slash / is allowed). I've changed this into @prefix/sub-folder-package and build works just fine now. IMO this is bug in nx generator

@mackelito
Copy link

I found it. In my case, the problem was in name of package in package.json. When generating package in sub-folder with nx, name of package in package.json will be like this "name": "@prefix/sub/folder-package" which is not correct from node perspective (only 1 slash / is allowed). I've changed this into @prefix/sub-folder-package and build works just fine now. IMO this is bug in nx generator

Hmmm.. I have lots of feature libraries that have multiple "/" in the name and they work fine?
eg. "name": "@org/verifications/feature-extra-verification",

@jaysoo
Copy link
Member

jaysoo commented May 8, 2024

This should work, as demonstrated here https://github.com/jaysoo/issue-15464.

Please try with the newest Nx version, and open a new issue with repro if you have further problems.

@jaysoo jaysoo closed this as completed May 8, 2024
Copy link

github-actions bot commented Jun 8, 2024

This issue has been closed for more than 30 days. If this issue is still occuring, please open a new issue with more recent context.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 8, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants