splash image

January 23, 2021

Migrating from CommonJS to ESM

Node.js opened the door for developers to build performant web servers using JavaScript.

The explosion of CommonJS modules which followed, created a massive new ecosystem. Building a typical website today involves hundreds, if not thousands, of modules.

To publish a module, you set module.exports in your code, create a package.json file, and run npm publish.

To consume a module, you add a dependency to your package.json file, run npm install, and call require('module-name') from your code.

Modules can depend on other modules.

Npm moves module files between a central registry and the machines running Node.js.

ESM modules

In 2015, import and export statements were added to JavaScript. ESM module loading is now a built-in feature of all major browsers (sorry IE.)

ESM removes the need for package.json files, and uses URLs instead of npm module names -- but it does not preclude those from being used with ESM, say in a Node.js context.

To publish an ESM module, use export in your code, and make the file fetchable by URL.

To consume an ESM module, use import { ... } from URL. See MDN for more details.

Using import instead of require() allows ESM modules to be loaded independently, without running the code where they are used. A variant of the import statement, is the dynamic import() function. This allows for modules to be loaded asynchronously at run-time.

ESM is the basis for exciting new developer tools like Snowpack and Vite.

So, why are most modules still published with CommonJS?

Even before ESM, developers could use npm modules in front-end code. Tools like browserify or webpack bundle modules into a single script file, loadable by browsers.

On the server side, it has taken Node.js a few years to arrive at ESM support. Unfortunately, the 2 standards are not fully interoperable.

Despite everyone's best intentions, the Node.js docs are unclear about what to do. For a deeper explanation, I recommend this article by Dan Fabulich.

Here is a summary of some interop scenarios:

require() from default Node.js context

import statement from Node.js ESM context - E.g. in a server.mjs file.

Dynamic Import as a fallback

Node's inability to require() ESM modules prevents simple upgrades from CommonJS to ESM.

Publishing dual ESM-CJS packages is messy because it involves wrapping CommonJS modules in ESM. Writing a module using ESM and then wrapping it for CommonJS is not possible.

Fortunately, dynamic import() provides an alternative.

Dynamic import() works from the default Node.js context as well as from an ESM context. You can even import() CJS modules. The only gotcha is that it returns a promise, so it is not a drop-in replacement for require().

Here is an example showing require() and import() together.

I published shortscale v1 as CommonJS. For v2 and later the module is only available as ESM. This means that later releases can no longer be loaded using Node.js require().

This fastify server loads both module versions from a CJS context.

// minimal fastify server based on:
// https://www.fastify.io/docs/latest/Getting-Started/#your-first-server

const fastify = require('fastify')({ logger: true });

fastify.register(async (fastify) => {
  let shortscale_v1 = require('shortscale-v1');
  let shortscale_v4 = (await import('shortscale-v4')).default;

  // e.g. http://localhost:3000/shortscale-v1?n=47
  fastify.get('/shortscale-v1', function (req, res) {
    let num = Number(req.query.n);
    let str = '' + shortscale_v1(num);
    res.send({num, str});
  });

  // e.g. http://localhost:3000/shortscale-v4?n=47
  fastify.get('/shortscale-v4', function (req, res) {
    let num = Number(req.query.n);
    let str = '' + shortscale_v4(num);
    res.send({num, str});
  });
});

// Run the server!
fastify.listen(3000, function (err, address) {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
  fastify.log.info(`server listening on ${address}`);
});

For this demo, package.json installs both versions of shortscale.

{
  "name": "demo-fastify-esm",
  "version": "1.0.0",
  "description": "Demonstrate ESM dynamic import from non-ESM server",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "author": "Jürgen Leschner",
  "license": "MIT",
  "dependencies": {
    "fastify": "^3.11.0",
    "shortscale-v1": "npm:shortscale@^1.1.0",
    "shortscale-v4": "npm:shortscale@^4.0.0"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/jldec/demo-fastify-esm"
  }
}

I plan to migrate my modules to ESM. Other module authors are too.

📦📦

To leave a comment
please visit dev.to/jldec

debug

user: anonymous

{
  "path": "/blog/migrating-from-cjs-to-esm",
  "attrs": {
    "title": "Migrating from CommonJS to ESM",
    "splash": {
      "image": "/images/calm.jpg"
    },
    "date": "2021-01-23",
    "layout": "BlogPostLayout",
    "excerpt": "How to migrate from CommonJS to EcmaScript Modules."
  },
  "md": "# Migrating from CommonJS to ESM\n\n[Node.js](https://nodejs.org/en/docs/guides/getting-started-guide/) opened the door for developers to build performant web servers using JavaScript.\n\nThe explosion of [CommonJS](https://nodejs.org/docs/latest/api/modules.html#modules_modules_commonjs_modules) modules which followed, created a massive new ecosystem. Building a typical website today involves hundreds, if not thousands, of modules.\n\nTo publish a module, you set `module.exports` in your code, create a `package.json` file, and run `npm publish`.\n\nTo consume a module, you add a dependency to your `package.json` file, run `npm install`, and call `require('module-name')` from your code.\n\nModules can depend on other modules.\n\n[Npm](https://docs.npmjs.com/about-npm) moves module files between a central registry and the machines running Node.js.\n\n## ESM modules\n\nIn [2015](https://262.ecma-international.org/6.0/#sec-ecmascript-language-scripts-and-modules), `import` and `export` statements were added to JavaScript. ESM module loading is now a built-in feature of [all major browsers](https://caniuse.com/mdn-javascript_statements_import) (sorry IE.)\n\nESM removes the need for package.json files, and uses URLs instead of npm module names -- but it does not preclude those from being used with ESM, say in a Node.js context.\n\nTo publish an ESM module, use `export` in your code, and make the file fetchable by URL.\n\nTo consume an ESM module, use `import { ... } from URL`. See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) for more details.\n\nUsing `import` instead of `require()` allows ESM modules to be loaded independently, without running the code where they are used. A variant of the `import` statement, is the [dynamic import()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports) function. This allows for modules to be loaded asynchronously at run-time.\n\n> ESM is the basis for exciting new developer tools like [Snowpack](https://github.com/snowpackjs/snowpack#readme) and [Vite](https://github.com/vitejs/vite#readme).\n\n## So, why are most modules still published with CommonJS?\n\nEven before ESM, developers could use npm modules in front-end code.  Tools like [browserify](https://github.com/browserify/browserify#readme) or [webpack](https://github.com/webpack/webpack#readme) bundle modules into a single script file, loadable by browsers.\n\nOn the server side, it has taken Node.js a few years to arrive at [ESM support](https://nodejs.org/api/packages.html#packages_determining_module_system). Unfortunately, the 2 standards are not fully interoperable.\n\nDespite everyone's best intentions, the [Node.js docs](https://nodejs.org/api/esm.html#esm_interoperability_with_commonjs) are unclear about what to do. For a deeper explanation, I recommend [this article](https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1) by Dan Fabulich.\n\nHere is a summary of some interop scenarios:\n\n#### require() from default Node.js context\n- require(\"CommonJS-module\") - **Yes ✅**, this has always worked and is the default.\n- require(\"ESM-module\") - **No ❌**.\n- require(\"Dual-ESM-CJS-module\") - **Yes ✅**, but be careful with state.\n\n#### import statement from Node.js ESM context - E.g. in a server.mjs file.\n- import from \"ESM-module\" - **Yes ✅**.\n- import default from \"CommonJS-module\" - **Yes ✅**.\n- import { name } from \"CommonJS-module\" - **No ❌**, get default.name from 2.\n\n## Dynamic Import as a fallback\nNode's inability to require() ESM modules prevents simple upgrades from CommonJS to ESM.\n\nPublishing [dual](https://nodejs.org/dist/latest-v15.x/docs/api/packages.html#packages_dual_commonjs_es_module_packages) ESM-CJS packages is messy because it involves [wrapping](https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1#6b50) CommonJS modules in ESM. Writing a module using ESM and then wrapping it for CommonJS is not possible.\n\nFortunately, [dynamic import()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports) provides an alternative.\n\nDynamic import() works from the default Node.js context as well as from an ESM context. You can even import() CJS modules. The only gotcha is that it returns a promise, so it is not a drop-in replacement for require().\n\nHere is an example showing require() and import() together.\n\nI published [shortscale](https://github.com/jldec/shortscale) v1 as CommonJS. For [v2 and later](https://github.com/jldec/shortscale/pull/2) the module is only available as ESM. This means that later releases can no longer be loaded using Node.js require().\n\nThis [fastify server](https://github.com/jldec/demo-fastify-esm) loads both module versions from a CJS context.\n\n```js\n// minimal fastify server based on:\n// https://www.fastify.io/docs/latest/Getting-Started/#your-first-server\n\nconst fastify = require('fastify')({ logger: true });\n\nfastify.register(async (fastify) => {\n  let shortscale_v1 = require('shortscale-v1');\n  let shortscale_v4 = (await import('shortscale-v4')).default;\n\n  // e.g. http://localhost:3000/shortscale-v1?n=47\n  fastify.get('/shortscale-v1', function (req, res) {\n    let num = Number(req.query.n);\n    let str = '' + shortscale_v1(num);\n    res.send({num, str});\n  });\n\n  // e.g. http://localhost:3000/shortscale-v4?n=47\n  fastify.get('/shortscale-v4', function (req, res) {\n    let num = Number(req.query.n);\n    let str = '' + shortscale_v4(num);\n    res.send({num, str});\n  });\n});\n\n// Run the server!\nfastify.listen(3000, function (err, address) {\n  if (err) {\n    fastify.log.error(err);\n    process.exit(1);\n  }\n  fastify.log.info(`server listening on ${address}`);\n});\n```\n\nFor this demo, `package.json` installs both versions of shortscale.\n\n```json\n{\n  \"name\": \"demo-fastify-esm\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Demonstrate ESM dynamic import from non-ESM server\",\n  \"main\": \"server.js\",\n  \"scripts\": {\n    \"start\": \"node server.js\"\n  },\n  \"author\": \"Jürgen Leschner\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"fastify\": \"^3.11.0\",\n    \"shortscale-v1\": \"npm:shortscale@^1.1.0\",\n    \"shortscale-v4\": \"npm:shortscale@^4.0.0\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/jldec/demo-fastify-esm\"\n  }\n}\n```\n\n> I plan to migrate my modules to ESM. Other [module authors](https://blog.sindresorhus.com/get-ready-for-esm-aa53530b3f77) are too.\n\n> 📦📦\n\n_To leave a comment  \nplease visit [dev.to/jldec](https://dev.to/jldec/migrating-from-commonjs-to-esm-2p24)_\n\n\n",
  "html": "<h1>Migrating from CommonJS to ESM</h1>\n<p><a href=\"https://nodejs.org/en/docs/guides/getting-started-guide/\">Node.js</a> opened the door for developers to build performant web servers using JavaScript.</p>\n<p>The explosion of <a href=\"https://nodejs.org/docs/latest/api/modules.html#modules_modules_commonjs_modules\">CommonJS</a> modules which followed, created a massive new ecosystem. Building a typical website today involves hundreds, if not thousands, of modules.</p>\n<p>To publish a module, you set <code>module.exports</code> in your code, create a <code>package.json</code> file, and run <code>npm publish</code>.</p>\n<p>To consume a module, you add a dependency to your <code>package.json</code> file, run <code>npm install</code>, and call <code>require('module-name')</code> from your code.</p>\n<p>Modules can depend on other modules.</p>\n<p><a href=\"https://docs.npmjs.com/about-npm\">Npm</a> moves module files between a central registry and the machines running Node.js.</p>\n<h2>ESM modules</h2>\n<p>In <a href=\"https://262.ecma-international.org/6.0/#sec-ecmascript-language-scripts-and-modules\">2015</a>, <code>import</code> and <code>export</code> statements were added to JavaScript. ESM module loading is now a built-in feature of <a href=\"https://caniuse.com/mdn-javascript_statements_import\">all major browsers</a> (sorry IE.)</p>\n<p>ESM removes the need for package.json files, and uses URLs instead of npm module names -- but it does not preclude those from being used with ESM, say in a Node.js context.</p>\n<p>To publish an ESM module, use <code>export</code> in your code, and make the file fetchable by URL.</p>\n<p>To consume an ESM module, use <code>import { ... } from URL</code>. See <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import\">MDN</a> for more details.</p>\n<p>Using <code>import</code> instead of <code>require()</code> allows ESM modules to be loaded independently, without running the code where they are used. A variant of the <code>import</code> statement, is the <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports\">dynamic import()</a> function. This allows for modules to be loaded asynchronously at run-time.</p>\n<blockquote>\n<p>ESM is the basis for exciting new developer tools like <a href=\"https://github.com/snowpackjs/snowpack#readme\">Snowpack</a> and <a href=\"https://github.com/vitejs/vite#readme\">Vite</a>.</p>\n</blockquote>\n<h2>So, why are most modules still published with CommonJS?</h2>\n<p>Even before ESM, developers could use npm modules in front-end code.  Tools like <a href=\"https://github.com/browserify/browserify#readme\">browserify</a> or <a href=\"https://github.com/webpack/webpack#readme\">webpack</a> bundle modules into a single script file, loadable by browsers.</p>\n<p>On the server side, it has taken Node.js a few years to arrive at <a href=\"https://nodejs.org/api/packages.html#packages_determining_module_system\">ESM support</a>. Unfortunately, the 2 standards are not fully interoperable.</p>\n<p>Despite everyone's best intentions, the <a href=\"https://nodejs.org/api/esm.html#esm_interoperability_with_commonjs\">Node.js docs</a> are unclear about what to do. For a deeper explanation, I recommend <a href=\"https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1\">this article</a> by Dan Fabulich.</p>\n<p>Here is a summary of some interop scenarios:</p>\n<h4>require() from default Node.js context</h4>\n<ul>\n<li>require(&quot;CommonJS-module&quot;) - <strong>Yes ✅</strong>, this has always worked and is the default.</li>\n<li>require(&quot;ESM-module&quot;) - <strong>No ❌</strong>.</li>\n<li>require(&quot;Dual-ESM-CJS-module&quot;) - <strong>Yes ✅</strong>, but be careful with state.</li>\n</ul>\n<h4>import statement from Node.js ESM context - E.g. in a server.mjs file.</h4>\n<ul>\n<li>import from &quot;ESM-module&quot; - <strong>Yes ✅</strong>.</li>\n<li>import default from &quot;CommonJS-module&quot; - <strong>Yes ✅</strong>.</li>\n<li>import { name } from &quot;CommonJS-module&quot; - <strong>No ❌</strong>, get <a href=\"http://default.name\">default.name</a> from 2.</li>\n</ul>\n<h2>Dynamic Import as a fallback</h2>\n<p>Node's inability to require() ESM modules prevents simple upgrades from CommonJS to ESM.</p>\n<p>Publishing <a href=\"https://nodejs.org/dist/latest-v15.x/docs/api/packages.html#packages_dual_commonjs_es_module_packages\">dual</a> ESM-CJS packages is messy because it involves <a href=\"https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1#6b50\">wrapping</a> CommonJS modules in ESM. Writing a module using ESM and then wrapping it for CommonJS is not possible.</p>\n<p>Fortunately, <a href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports\">dynamic import()</a> provides an alternative.</p>\n<p>Dynamic import() works from the default Node.js context as well as from an ESM context. You can even import() CJS modules. The only gotcha is that it returns a promise, so it is not a drop-in replacement for require().</p>\n<p>Here is an example showing require() and import() together.</p>\n<p>I published <a href=\"https://github.com/jldec/shortscale\">shortscale</a> v1 as CommonJS. For <a href=\"https://github.com/jldec/shortscale/pull/2\">v2 and later</a> the module is only available as ESM. This means that later releases can no longer be loaded using Node.js require().</p>\n<p>This <a href=\"https://github.com/jldec/demo-fastify-esm\">fastify server</a> loads both module versions from a CJS context.</p>\n<pre><code class=\"language-js\">// minimal fastify server based on:\n// https://www.fastify.io/docs/latest/Getting-Started/#your-first-server\n\nconst fastify = require('fastify')({ logger: true });\n\nfastify.register(async (fastify) =&gt; {\n  let shortscale_v1 = require('shortscale-v1');\n  let shortscale_v4 = (await import('shortscale-v4')).default;\n\n  // e.g. http://localhost:3000/shortscale-v1?n=47\n  fastify.get('/shortscale-v1', function (req, res) {\n    let num = Number(req.query.n);\n    let str = '' + shortscale_v1(num);\n    res.send({num, str});\n  });\n\n  // e.g. http://localhost:3000/shortscale-v4?n=47\n  fastify.get('/shortscale-v4', function (req, res) {\n    let num = Number(req.query.n);\n    let str = '' + shortscale_v4(num);\n    res.send({num, str});\n  });\n});\n\n// Run the server!\nfastify.listen(3000, function (err, address) {\n  if (err) {\n    fastify.log.error(err);\n    process.exit(1);\n  }\n  fastify.log.info(`server listening on ${address}`);\n});\n</code></pre>\n<p>For this demo, <code>package.json</code> installs both versions of shortscale.</p>\n<pre><code class=\"language-json\">{\n  &quot;name&quot;: &quot;demo-fastify-esm&quot;,\n  &quot;version&quot;: &quot;1.0.0&quot;,\n  &quot;description&quot;: &quot;Demonstrate ESM dynamic import from non-ESM server&quot;,\n  &quot;main&quot;: &quot;server.js&quot;,\n  &quot;scripts&quot;: {\n    &quot;start&quot;: &quot;node server.js&quot;\n  },\n  &quot;author&quot;: &quot;Jürgen Leschner&quot;,\n  &quot;license&quot;: &quot;MIT&quot;,\n  &quot;dependencies&quot;: {\n    &quot;fastify&quot;: &quot;^3.11.0&quot;,\n    &quot;shortscale-v1&quot;: &quot;npm:shortscale@^1.1.0&quot;,\n    &quot;shortscale-v4&quot;: &quot;npm:shortscale@^4.0.0&quot;\n  },\n  &quot;repository&quot;: {\n    &quot;type&quot;: &quot;git&quot;,\n    &quot;url&quot;: &quot;https://github.com/jldec/demo-fastify-esm&quot;\n  }\n}\n</code></pre>\n<blockquote>\n<p>I plan to migrate my modules to ESM. Other <a href=\"https://blog.sindresorhus.com/get-ready-for-esm-aa53530b3f77\">module authors</a> are too.</p>\n</blockquote>\n<blockquote>\n<p>📦📦</p>\n</blockquote>\n<p><em>To leave a comment<br>\nplease visit <a href=\"https://dev.to/jldec/migrating-from-commonjs-to-esm-2p24\">dev.to/jldec</a></em></p>\n"
}