Introduce ECMAScript Modules of Node.js

I talked about ECMAScript Modules at Dubin Node.js meetup. I made comments to deepen your understanding.

URLs

meetup, my slides

Slides

I will refer to ECMAScript Modules as ESM and CommonJS Modules as CJS.

First, Thanks to all the Dublin engineers for inviting me. I have been living in Dublin for one month now and I feel very comfortable.

Node.js v12 was released on 23rd April🎉🎉🎉
In addition, Node.js v6 will be End-of-life on 30th April.

Node v12.0.0 (Current)

First of all, let me introduce myself.

I’m Yuta Hiroto. Everyone calls me “hiroppy”. I’m living in Dublin until September and then I’m going to go back to Japan. My twitter is here. Currently, I’m a technical advisor at Mercari and working on improving webpack now. And, I’m a Node.js Core Collaborator.

This picture above is a famous shrine in Japan was taken from the article called “Why 2019 is the best year to visit Japan”. If you come to Japan at this time of year, you will see the cherry blossoms like in this picture.

Why 2019 is the best year to visit Japan

Please let me introduce my company.

Mercari is a C2C marketplace app that allows users to enjoy buying and selling. Offices in Japan and the United States have more than 1650 people work. Mercari’s mission is to “create value in a global marketplace where anyone can buy & sell,” and its focus on a global marketplace can be seen in the decision to expand overseas the year after its foundation. The company believes that succeeding in a market as large and diverse as the US is a key milestone in achieving its mission.

If you are interested in Mercari, see https://www.mercari.com/careers/

JavaScript has many module systems. For example, AMD, UMD, and CJS.
ECMAScript Modules is owned by WHATWG and TC39.
TC39 has ESM syntax and rules concerning javascript. For example, the default is strict mode and how to treat `this`, etc…
However, modules loading is owned by WHATWG because it is the difference between browsers and Node.js. On the other hand, dynamic import is JavaScript only syntax, so TC39 manages it. Now, dynamic import is stage-3 of TC39.

whatwg/loader, ECMAScript Language: Scripts and Modules,

tc39/proposal-dynamic-import

You can specify `module` to `type` attribute in `<script>` to make it recognized as ESM. However, browsers that don’t know `type: module` ignore this script tag. In other words, it is not executed.

So you use the `nomodule` attribute. Browsers that don’t support ESM can ignore this attribute and execute it as a normal script tag because `type` attribute has not changed. And browsers that support ESM don’t execute this script. I think that files into one using a bundler usually have this attribute.

All browsers can use modules other than Internet Explorer. We might not need bundlers like webpack in the future. However, it is still more efficient to bundle large code now.

I think many people are already using ESM. ESM has `import` and `export` syntax. Like this example.

I describe the features of ESM.

`import` and `export` can only be written at the top level. This restriction makes it possible to detect errors before execution. If you want to get modules as asynchronously, please use dynamic import which is stage-3 of TC39.

And, since `import` statement is hoisting, it is declared at the beginning of the module wherever it is written. So this is the same behavior as a function declaration. Also, the top level `this` in the module becomes `undefined` and a module which is written in ESM becomes strict mode.

Currently, the status of ESM is stability:1. Node.js has stability level. It has 3 phases, 0 is Deprecated, 1 is Experimental, and 2 is Stable. Since stability 1 is experimental, it will be changed sometimes. Therefore, you should not use it in production.

The goal is to “release” (drop the --experimental-modules flag) by when Node 12 starts LTS in October 2019.

Plan for New Modules Implementation

PR was submitted on 18th March to Node.js Core by Mr. Myles.

We discuss the specification at “nodejs/modules”. Then, implement in the repository called “nodejs/ecmascript-modules” which forked Node.js Core, and finally backport to Node.js Core.

nodejs/modules, nodejs/ecmascript-modules

Node.js spent a long time implementing ESM. Node.js had two major problems that browsers don’t have.

One is that Node.js cannot recognize if a file is written in ESM. In the case of browsers, it can be recognized because it is written as `module` in the `type` attribute. However, Node.js’ import does not have an attribute like the `type`. So we decided to look at the file extensions to be imported.

We made a rule that a file with the `.mjs` extension is written in ESM. And another issue is compatibility with existing CJS Modules. Node.js values backward compatibility. However, Node.js already has a module system. It’s very difficult to implement ESM so as not to break the existing code.

Node.js defined the standard for implementation. We keep these standards and implement ESM in Node.js.

ESM’s path conforms to whatwg url.

See the example. As you can see, import syntax accepts URL object. Access using a relative path, an absolute path, a package name, and `file` protocol can be performed. Currently, protocols support only `file`.

You can not use Node.js specific variables that were available until now. Such as, `require`, `module`, `exports`, `__dirname`, and `__filename`. These variables will become `undefined` when you write in ESM. To use these variables in ESM, you can use `import.meta` or `createRequireFromPath` to reproduce the same behavior.

If you want to get a file path, you can use `import.meta.url`. This is an ESM specification and currently exists in stage-3 of TC39.

If you want to use `require` syntax in ESM, you can use `createRequireFromPath` method. By passing a based path as an argument, This function returns an executable `require` method based on the argument.

tc39/proposal-import-meta

CJS can omit `.js`, `.node`, `.json` extensions and the filename “index”. However, ESM doesn’t have this specification, so we also make this CJS specification unavailable in ESM by default. So, we prepared ` — es-module-specifier-resolution` flag. This flag has `explicit` and `node` options, and the default is `explicit`.

I think that most of the existing code written in ESM has omitted the extension and “index”. Then, please use `node` option.

In the future, you will not be able to execute anything other than JavaScript.

whatwg/html “JSON modules”

This example is written in CJS. It is impossible to call an ESM file using require syntax. On the other hand, if you use import syntax, you can call CJS files. However, the only dynamic import can call ESM files from CJS.

In conclusion, CJS cannot call ESM at the top level, but CJS can use dynamic import. Also, ESM can call CJS.

Let’s explain how Node.js distinguishes ESM and CJS.

I think many people have heard the word `.mjs` in the past.

Certainly, it is easy to distinguish by the extension. However, in the future, ESM will become the de-facto standard, so we do not want to change the extension from `.js` to `.mjs` it’s not needed. Probably, this is something that many people wanted. So we looked for another solution.

There is a way to specify the module type to package.json. This solution is already in the Core of Node.js, and probably will not be changed. The solution is very simple. Each file’s module type depends on the closest parent’s package.json.

See this picture. package.json on the root.js has `type: module`, so root.js will be read as ESM. index.js in “./esm” directory does not have package.json in the same hierarchy so index.js depends on package.json in the root hierarchy. Therefore, this file is also read as ESM. Finally, let’s look at the index.js in “./esm/cjs”. package.json exists in the same hierarchy but does not have `type` attribute. So, index.js is read as CJS.

Code: hiroppy/node-esm-example

You can specify the type attribute in package.json. This attribute has `commonjs` and `module`, and the default is `commonjs`.

However, this key name might be changed. See the issue.

The directory structure is the same as the previous slide. Since ESM does not have a `module` variable, it is ESM if `module` is `undefined`.

Look at the output results. At first, `./esm/index.js` is called and output as ESM. This file depends on package.json of the root directory. Next, `./esm/cjs/index.js` is called and output as CJS. This file depends on package.json which exists in the same hierarchy and this package.json doesn’t have `type` attribute. Finally, `./root.js` outputs as ESM because package.json in the same hierarchy has `type:module`.

If package.json in the root directory doesn’t have `type:module`, this file can not be executed. Because it is executed as CJS, it can not interpret import syntax.

If you don’t want to follow this rule for specific files, you specify the extension. You can use `.mjs` and `.cjs` extensions. If you want to read as ESM, use the `.mjs` extension. If you want to read as CJS, use the `.cjs` extension.

Currently, this is an ongoing task. These tasks will be improved before the `— experimental-modules` flag is removed.

Works in progress

Please look forward to future Node.js😄

References

Special Thanks to

Daijiro Wachi Thank you for reviewing my English.


If you have any questions, please feel free to ask me on Twitter.🙃

Thank you for your kind attention:)