Node: "Cannot find module" when main file not `index.js` with experimental-specifier-resolution=node

Created on 5 Mar 2020  ·  10Comments  ·  Source: nodejs/node

  • Version: 13.9.0
  • Platform: Linux

What steps will reproduce the bug?

  1. Clone https://github.com/dandv/node-cant-find-module-with-main-not-index.js
  2. npm start

What is the expected behavior?

The script should display Success!, and does do so if mypackage/Lib.js is renamed to mypackage/index.js.

What do you see instead?

internal/modules/esm/resolve.js:61
  let url = moduleWrapResolve(specifier, parentURL);
            ^

Error: Cannot find module /home/dandv/prg/node-cant-find-module-with-main-not-index.js/mypackage imported from /home/dandv/prg/node-cant-find-module-with-main-not-index.js/run.js
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:61:13)
    at Loader.resolve (internal/modules/esm/loader.js:85:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:191:28)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:42:40)
    at link (internal/modules/esm/module_job.js:41:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

Additional information

I'm trying to run node with -experimental-specifier-resolution=node because TypeScript can't output .mjs files and I want to use extension-less import statements. I prefer to use Lib.js instead of index.js to distinguish in my IDE between the main files of multiple packages in my monorepo that otherwise would all look like index.js.

ES Modules confirmed-bug help wanted

Most helpful comment

I'm trying to run node with -experimental-specifier-resolution=node because TypeScript can't output .mjs files and I want to use extension-less import statements.

TypeScript has only limited support for ES modules so far. But in many cases the following workaround works:

  1. Add "type": "module" to package.json.
  2. Use import './some-file.js' in your TypeScript source code. TypeScript will find .ts files if you use .js in the specifier.
  3. Run the app without the experimental resolution flag.

(That's the more verbose version of Jordan's "If you use explicit extensions". :))

All 10 comments

/cc @nodejs/modules

The repro link seems correct to me; if so, this seems like a bug.

If you use explicit extensions, and don't use experimental specifier resolution, what happens?

I'm trying to run node with -experimental-specifier-resolution=node because TypeScript can't output .mjs files and I want to use extension-less import statements.

TypeScript has only limited support for ES modules so far. But in many cases the following workaround works:

  1. Add "type": "module" to package.json.
  2. Use import './some-file.js' in your TypeScript source code. TypeScript will find .ts files if you use .js in the specifier.
  3. Run the app without the experimental resolution flag.

(That's the more verbose version of Jordan's "If you use explicit extensions". :))

If you use explicit extensions, and don't use experimental specifier resolution, what happens?

Then the script runs correctly.

So the issue appears to be that to support that we introduced for experimental-specifier-resolution does not respect the package.json main field. I can get the example to work by changing the Lib.js file to be index.js. This is definitely a bug in the support for experimental resolution. I'm not 100% where the bug lives but this is where we should be resolving package main for experimental resolution

https://github.com/nodejs/node/blob/master/src/module_wrap.cc#L1180-L1186

This is where it seems like where we are doing the resolution itself

https://github.com/nodejs/node/blob/master/src/module_wrap.cc#L826-L862

It is possible that some order of operations bug is not even checking for the package.main... and tbh the work we've done around exports is going to confuse this a bit too. I'll try and find some time to dig in but this is going to have to be lower priority for me personally, so if anyone else wants to pick this up please go ahead!

as an aside, thanks so much for these amazing bug reports @dandv

I figured out the exact reason.
Note: Related C++ code is rewritten using JS in this commit, new related code location link:
https://github.com/nodejs/node/blob/master/lib/internal/modules/esm/resolve.js#L594
More comments can be found in the historical C++ file in the commit link above.

function moduleResolve(specifier /* string */, base /* URL */) { /* -> URL */
  // Order swapped from spec for minor perf gain.
  // Ok since relative URLs cannot parse as URLs.
  let resolved;
  if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
    resolved = new URL(specifier, base);
  } else {
    try {
      resolved = new URL(specifier);
    } catch {
      return packageResolve(specifier, base);
    }
  }
  return finalizeResolution(resolved, base);
}

The parameter specifier is ./mypackage here, so it passes the relative path check and returns as is directly. So the package main resolve related code won't run.

I am working on this, maybe I will submit a PR in about a few days.

EDIT: This is working as intended, I misunderstood the spec. See https://nodejs.org/api/esm.html#esm_import_specifiers

Leaving the below for history only.

Seeing the same thing on Node 14.1, OSX. In my package.json I have

"type": "module",

I then have one file, server.js, which in turn imports another file, serverModule.js:

server.js

import startServer from './serverModule';

startServer({});

serverModule.js

import args from './get-args';
// ......
export default startServer;

Now, I notice that adding .js to any import in the chain solves it for that particular file. For example, this solves the import of serverModule.js in server.js:

import startServer from './serverModule.js';

But then in turn it complains about import args from './get-args'; in that file, and so on. It seems like file endings are required, but that runs contrary to the description here: https://medium.com/@nodejs/announcing-core-node-js-support-for-ecmascript-modules-c5d6dc29b663

Files ending in .js, or extensionless files, when the nearest parent package.json file contains a top-level field “type” with a value of “module”.

Thanks! This solution worked for me.

This seems resolved, closing. Feel free to reopen if something needs to be discussed further, thank you.

@ryzokuken I believe this actually still remains an implementation bug and I believe my suggestion in https://github.com/nodejs/node/pull/32612#discussion_r403340432 might be related to the fix here.

That said, we might end up deprecating this flag before the fix at this rate...

Was this page helpful?
0 / 5 - 0 ratings