Skip to main content

ES modules in Node.js 12, from experimental to release

ES Modules In Node.js 12
Different forms of modularization have been around in the JavaScript ecosystem for years. Developers have used well-defined specifications such as AMD or CommonJS as well as simple coding patterns like the revealing module pattern to gain the benefits of a well-modularized solution.
Modules can be used on the client side in browsers or on the server side in Node.js. Sometimes code is transpiled from one module format to another using tools like Babel. All of this makes for a messy and complex JavaScript module state.
Enter ES modules — more specifically, ES modules in Node.js.
Tip: This article focuses on ES modules in Node.js. Check out “CommonJS vs AMD vs RequireJS vs ES6 Modules” for an excellent comparison of module systems not specific to Node.js.

A brief history

Let’s look at some of the key milestones for ES module support:
  • June 2015 – September 2017: Major browsers add experimental support for ES modules hidden behind developer flags. The primary means of developing JavaScript using ES modules is by transpiling code using tools like Babel.
  • September 2017: Node.js v8.5 includes experimental support for ES modules.
  • September 2017 – May 2018: Major browsers begin to support the ES module specification without developer flags, including:
    1. Chrome 61, on 5 September 2017
    2. Safari 11, on 18 September 2017
    3. Firefox 60, on 8 May 2018
  • October 2018: A new module implementation plan is created. The plan includes several phases for replacing the current experimental implementation with a new implementation, following three guiding principles from day one:
    1. Comply with the ES specification
    2. Node should do things the same way browsers do as much as possible
    3. Don’t break existing CommonJS modules
Tip: The Node.js Modules team has provided a more detailed set of guiding principles for the new implementation.
  • October 2019 (tentative): Node 12 is expected to enter long-term support. According to the official plan, the goal is to release full support for ES modules at this time.

Why is the full ES module support milestone so important for Node.js?

For a couple reasons. For one thing, all major browsers already support ES modules — you can see for yourself here. Supporting ES modules on the server side in Node.js out of the box will allow full-stack developers to naturally write modular, reusable JavaScript for both the client and server.
For another thing, experimental features in Node.js are subject to non-backward-compatible changes or removal in future versions. That being said, experimental ES module support has been around in Node for a few years and is not expected to change dramatically before October 2019.

Modules in Node.js: Current state

CommonJS modules

The de facto standard for modules in Node.js currently (mid-2019 at the time of writing) is CommonJS. CommonJS modules are defined in normal .js files using module.exports. Modules can be used later within other .js files with the require() function. For example:
// foo.js
module.exports = function() { 
  return 'Hello foo!';
}

// index.js
var foo = require('./foo');
console.log(foo()); // Hello foo!
Use Node to run this example with node index.js.

ES modules

Since Node v8.5, developers have been able to run variations of support for the ES modules specification using the --experimental-modules flag. As of Node v12.4, modules can be defined in .mjs files (or .js files under certain circumstances). For example:
// foo.mjs
export function foo() { 
  return 'Hello foo!'; 
}

// index.mjs
import { foo } from './foo.mjs';
console.log(foo()); // Hello foo!
Use Node to run this example with node --experimental-modules index.mjs.

Using CommonJS and ES modules in the same application

In some ways, supporting ES modules in browsers may have been a little simpler than supporting ES modules in Node because Node already had a well-defined CommonJS module system. Luckily, the community has done a fantastic job of ensuring developers can work with both types of modules at the same time and even import from one to the other.
For example, let’s say we have two modules. The first is a CommonJS module, and the second is an ES module (note the different file extensions):
// cjs-module-a.js
module.exports = function() {
  return 'I am CJS module A';
};

// esm-module-a.mjs
export function esmModuleA() {
  return 'I am ESM Module A';
};
export default esmModuleA;
To use the CommonJS module in an ES module script (note the .mjs extension and use of the import keyword):
// index.mjs
import esmModuleA from './esm-module-a.mjs';
import cjsModuleA from './cjs-module-a.js';
console.log(`esmModuleA loaded from an ES Module: ${esmModuleA()}`);
console.log(`cjsModuleA loaded from an ES Module: ${cjsModuleA()}`);
Use Node to run this example with node --experimental-modules index.mjs.
To use the ES module in a standard CommonJS script (note the .js extension and use of the require() function):
// index.js
// synchronously load CommonJS module
const cjsModuleA = require('./cjs-module-a');
console.log(`cjsModuleA loaded synchronously from an CJS Module: ${cjsModuleA()}`);

// asynchronously load ES module using CommonJS
async function main() {
  const {esmModuleA} = await import('./esm-module-a.mjs');
  console.log(`esmModuleA loaded asynchronously from a CJS module: ${esmModuleA()}`);
}
main();
These examples provide a basic demonstration of how to use CommonJS and ES modules together in the same application. Check out “Native ES Modules in NodeJS: Status and Future Directions, Part I” by Gil Tayar for a deeper dive into CommonJS and ES Module interoperability.

Modules in Node.js: Future state

At the time of writing, the new module implementation plan is in its third and final phase. Phase 3 is planned to be completed at the same time that Node 12 LTS is released and when ES module support will be available without the -experimental-modules flag.
Phase 3 will likely bring a few big improvements to round out the ES module implementation.

Loaders solution

Developers expect module loading systems to be flexible and full-featured. Here are a few of the key features in development in the Node.js module loader solution:
  • Code coverage/instrumentation: Enable developer tools to retrieve data about CJS and ESM module usage.
  • Pluggable loaders: Allow developers to include loader plugins in their packages that can define new behaviors for loading modules from specific file extensions or mimetypes, or even files without extensions.
  • Runtime loaders: Allow files referenced in import statements to be transpiled at import time (runtime).
  • Arbitrary sources for modules: Allow modules to be loaded from sources other than the file system (e.g., load a module from a URL).
  • Mock modules: Allow modules to be replaced with mocks while testing.

"exports" object in package.json

While the naming and syntax is not final, the idea here is to have an object somewhere in the package.json file that allows packages to provide “pretty” entry points for different components within the package. Take this package.json as an example of a possible implementation:
{
  "name": "@myorg/mypackage",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "exports": {
    ".": "./src/mypackage.mjs",
    "./data": "./data/somedir/someotherdir/index.mjs"
  }
}
Developers would be able to import the data component of @myorg/mypackage like this:
import { MyModule } from '@myorg/mypackage/data

Referencing the package root with the package’s name

When referencing one module from another module within the same package, you may end up with a lot of backtracking that looks like this:
import coolcomponent from '../../../coolcomponent/module.js
If this change is implemented, then backtracking can be replaced with a reference to the package’s name as defined in package.json. The new code would look like this:
import coolcomponent from 'mypackage/coolcomponent/module.js

Supporting dual ESM/CommonJS packages

Allowing an npm package to contain CJS and ES modules alongside each other is important to ensure there is a backwards-compatible, developer-friendly path to migrate from CommonJS to ES modules. This has often been referred to as “dual-mode” support.
The status quo approach to dual-mode support is for the existing main entry point in package.json to point to a CommonJS entry point. If an npm package contains ES modules and the developer wants to use them, they need to use deep imports to access those modules (e.g., import 'pkg/module.mjs'). This is the dual-mode solution that is likely to ship with Node.js 12 LTS.
There were some other proposals for dual-mode support. This widely debated proposal includes some options for making it easier for developers to ship packages with two separate implementations (ESM and CJS), but this proposal failed to reach consensus.
A newer proposal for require of ESM suggests a different approach that allows developers to resolve ES modules with require(). This proposal is still open but has went silent and is unlikely to be included in Node 12 LTS.

Hello ES modules, goodbye CommonJS?

While the goal is for ES modules to eventually replace CommonJS modules in Node.js, no one knows what the future holds — nor how long before CommonJS module support disappears. But one thing is for sure: Node developers have spent considerable time and effort ensuring a seamless transition to a future without CommonJS.
They have done a fantastic job striking a balance between ensuring both module types interoperate with each other while trying not to introduce too many new dual-mode APIs that would become useless once the critical mass has migrated and it comes time to remove support for CommonJS from Node.
So when will CommonJS be removed from Node.js? Let’s make a wild, baseless prediction and say Node 18 with an --experimental-no-commonjs-modules and Node 20 for the final sunset. The future of modular JavaScript across browsers, servers and everywhere else JavaScript runs is an exciting one!

Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...