File-based module naming scheme breaks type-checker for node module resolution
See original GitHub issueSteps to reproduce
- 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)
- Put the following in
a/index.js
:
/** @interface */
class A {
/** @return {string} */
getString() {}
}
module.exports = A;
- 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()}`;
}
};
- 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);
- 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
- 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
- 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:
- Created 6 years ago
- Comments:9
Top GitHub Comments
@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.
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?