question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

File-based module naming scheme breaks type-checker for node module resolution

See original GitHub issue

Steps to reproduce

  1. In an empty directory run the following commands:
mkdir -p a b/node_modules c/node_modules
touch a/index.js b/index.js c/index.js
(cd a && npm init -y) && (cd b && npm init -y) && (cd c && npm init -y)
  1. Put the following in a/index.js:
/** @interface */
class A {
  /** @return {string} */
  getString() {}
}

module.exports = A;
  1. Put the following in b/index.js:
const A = require('a');

module.exports = class B {
  /**
   * @param {!A} a
   * @return {string}
   */
  getStringFromA(a) {
    return `String from a called from B: ${a.getString()}`;
  }
};
  1. Put the following in c/index.js:
const A = require('a');
const B = require('b');

/** @implements {A} */
class C {
  getString() {
    return 'result of calling getString() from class C, which implements A';
  }
}

/**
 * @param {!B} b
 * @param {!A} a
 */
function thisWillNotWork(b, a) {
  console.log(b.getStringFromA(a));
}

/**
 * @param {!A} a
 */
function thisWillWork(a) {
  console.log(a.getString());
}

const b = new B();
const c = new C();

thisWillNotWork(b, c);
thisWillWork(c);
  1. Run the following commands:
cp -rf a b/node_modules && cp -rf a c/node_modules && cp -rf b c/node_modules

Your directory should now have the following structure:

.
├── a
│   ├── index.js
│   └── package.json
├── b
│   ├── index.js
│   ├── node_modules
│   │   └── a
│   │       ├── index.js
│   │       └── package.json
│   └── package.json
└── c
    ├── index.js
    ├── node_modules
    │   ├── a
    │   │   ├── index.js
    │   │   └── package.json
    │   └── b
    │       ├── index.js
    │       ├── node_modules
    │       │   └── a
    │       │       ├── index.js
    │       │       └── package.json
    │       └── package.json
    └── package.json
  1. Run the c/index.js script to ensure everything is working properly:
node c/index.js
# String from a called from B: result of calling getString() from class C, which implements A
# result of calling getString() from class C, which implements A
  1. Run the following command and observe the output:
java -jar /path/to/compiler.jar \
  --js $(find . -type f -name '*.js') \
  --js $(find . -type f -name 'package.json') \
  --compilation_level ADVANCED \
  --module_resolution NODE \
  --process_common_js_modules \
  --checks_only

Expected result

The code compiles without error (see this equivalent appspot script with no imports)

Actual Result

./c/index.js:16: WARNING - actual parameter 1 of module$c$node_modules$b$index.prototype.getStringFromA does not match formal parameter
found   : module$c$node_modules$a$index
required: module$c$node_modules$b$node_modules$a$index
  console.log(b.getStringFromA(a));
                               ^

0 error(s), 1 warning(s), 100.0% typed

Additional Info

I suspect this is happening because closure is naming modules based on their file path, which will not work for NodeJS code. The way in which NPM installs dependencies means there may very well be the same module that’s referenced in two different locations. Closure seems to fail to take this into account when naming modules using node’s module resolution algorithm. Because of this, closure treats the two equivalent modules differently, thus a false positive is given.

Note also that this directly impacts a project we’re working on - https://github.com/material-components/material-components-web. We symlink all of our dependencies in packages/ together using lernaJS (note that we follow symlinks when passing our --js flags), but because they are in “different locations”, their modules are named differently, and thus we need to create a lot of extra effort around rewriting all of our imports to be relative to one another, which is less-than-optimal 😃

Possible Solution: Name modules based on package.json info

A reliable way to uniquely identify a module in node is to combine its name and its version. By doing so, you can almost guarantee that you will never have a collision between two separate modules. Perhaps the way the modules are named could be changed to reflect this - or some other - strategy when --module_resolution NODE is given?

Finally, FWIW here is how Typescript resolves its modules when --module-resolution node is given as a flag: https://www.typescriptlang.org/docs/handbook/module-resolution.html#node

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:9

github_iconTop GitHub Comments

1reaction
ChadKillingsworthcommented, Mar 23, 2017

@traviskaufman We do account for the node_module structure not being flat: https://github.com/google/closure-compiler/blob/35e1e1b102eea96f8a3aa23adf1763a1e6f9acbe/test/com/google/javascript/jscomp/deps/ModuleLoaderTest.java#L186-L191

I’ll try to dig into this soon though to see what’s going on.

0reactions
ChadKillingsworthcommented, Mar 24, 2017

This isn’t a compiler limitation per se - it’s now node modules work. Even two modules in separate folders with the same package.json name and version can be completely different (you can’t assume they were all installed from npm).

Have you tried yarn’s flat mode?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Documentation - Module Resolution - TypeScript
Module resolution is the process the compiler uses to figure out what an import refers to. Consider an import statement like import {...
Read more >
JS Modules · google/closure-compiler Wiki - GitHub
CommonJS and ES modules are file based and a module is imported by its path. ... See the node module resolution algorithm for...
Read more >
Common TypeScript module problems and how to solve them
Enabling the compiler module resolution tracing in TypeScript can provide insight in diagnosing and solving problems.
Read more >
Webpack 5 release (2020-10-10)
But shipping features without breaking changes also has a cost: We can't do ... While this makes using modules written for Node.js easier, ......
Read more >
How To Use Node.js Modules with npm and package.json
json file based on your answers. Using the init Command. First, set up a project so you can practice managing modules. In your...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found