How to start using ECMAScript modules in a real-life Node.js project

Vitali Zaneuski
3 min readJan 15, 2023
Photo by Paul Esch-Laurent on Unsplash

NOTE: The simplified version of the project is available by the link. All examples are taken from it.

Periodically we at Indy invest time in analyzing and updating dependencies to the latest versions as a part of the maintenance routine. So there is nothing special. One day during such an activity I’ve noticed that it was impossible to start the project after the simple upgrade.

The logs clearly spotted the culprit — the library got changed its module system from the CommonJS to the ECMAScript modules.

/Users/vitalizaneuski/.nvm/versions/node/v16.15.0/bin/node /Users/vitalizaneuski/projects/esm-test/broken-require.js
/Users/vitalizaneuski/projects/esm-test/broken-require.js:3
const got = require("got");
^

Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/vitalizaneuski/projects/esm-test/node_modules/got/dist/source/index.js from /Users/vitalizaneuski/projects/esm-test/broken-require.js not supported.
Instead change the require of index.js in /Users/vitalizaneuski/projects/esm-test/broken-require.js to a dynamic import() which is available in all CommonJS modules.
at Object.<anonymous> (/Users/vitalizaneuski/projects/esm-test/broken-require.js:3:13) {
code: 'ERR_REQUIRE_ESM'
}

The author of the library did a good job and posted documentation on how to start using the new version of the library, plus Node.js logs were also helpful. So it seems the solution was simple, just to replace the sync require by async dynamic import, and this situation should be sorted out. And it worked!

/Users/vitalizaneuski/.nvm/versions/node/v16.15.0/bin/node /Users/vitalizaneuski/projects/esm-test/dynamic-import.js
{
"login": "mojombo",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://avatars.githubusercontent.com/u/1?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/mojombo",
"html_url": "https://github.com/mojombo",
"followers_url": "https://api.github.com/users/mojombo/followers",
"following_url": "https://api.github.com/users/mojombo/following{/other_user}",
"gists_url": "https://api.github.com/users/mojombo/gists{/gist_id}",
"starred_url": "https://api.github.com/users/mojombo/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/mojombo/subscriptions",
"organizations_url": "https://api.github.com/users/mojombo/orgs",
"repos_url": "https://api.github.com/users/mojombo/repos",
"events_url": "https://api.github.com/users/mojombo/events{/privacy}",
"received_events_url": "https://api.github.com/users/mojombo/received_events",
"type": "User",
"site_admin": false
}

Process finished with exit code 0

Yeah, the output above was the same as it was expected, but IDE was still complaining because of the ESLint.

> esm-node-js-test@1.0.0 lint
> eslint .


/Users/vitalizaneuski/projects/esm-test/broken-require.js
3:21 error Unable to resolve path to module 'got' import/no-unresolved

/Users/vitalizaneuski/projects/esm-test/dynamic-import.js
4:28 error Unable to resolve path to module 'got' import/no-unresolved

✖ 2 problems (2 errors, 0 warnings)

Many production-ready projects are using some kind of a linter and we at Indy are not an exception to the rule. One of the plugins that we’re using is an ESLint import plugin. The error in the snippet was caused by the violation of the import/no-unresolved rule that checks whether the dependency is installed and can be resolved in production or not.

This error is a bit tricky to fix because we need to understand why it happens and how ESLint works and resolves packages. By default, eslint-import-plugin uses a node resolver plugin that is looking for the package.json in the project and trying to resolve the project entry point defined by the main property in the package.json . But the trick here is that new packages that are using ECMAScript modules are shipped with exports property instead of main. And it makes ESLint check to fail.

Realizing the issue is easy to fix it — we just need to use another resolver plugin from the list of available ones. After a quick search in the npm, the following one was found — the exports resolver plugin that adds missing functionality. The final ESLint configuration is below.

"use strict";

module.exports = {
env: {
node: true,
es2020: true,
},
plugins: ["import"],
settings: {
"import/resolver": {
node: {
extensions: [".js"],
},
exports: {},
},
},
rules: {
"import/no-unresolved": ["error", { commonjs: true }],
},
};

That is it, the situation is resolved and the project now using packages baked by ECMAScript modules.

NOTE: The refactoring can be intricate and I just described the case from my work. If you need to have some sync code that depends on the package, you need to rework the logic flow entirely, cause Node.js in CommonJS does not support root level await while import('package-name') only works asynchronously.

--

--