Exploring Node.js Modules: Using CommonJS and ES6 modules together

Node.js supports two module formats for JavaScript code: the CommonJS format originally developed for Node.js, and the new ES6 module format. The two are conceptually similar, but there are many practical differences. Because of this, we will face situations of using both in the same application and will need to know how to proceed.

First is the question of file extensions and recognizing which module format to use. The ES6 module format is used in the following situations:

Files where the filename ends in .mjs.

If the package.json has a field named type with the value module, then filenames ending with .js.

If the node binary is executed with the –input-type=module flag, then any code passed through the –eval or –print argument, or piped in via STDIN (the standard input), is interpreted as ES6 module code.

That’s fairly straight-forward. ES6 modules are in files named with the .mjs extension, unless you’ve declared in the package.json that the package defaults to ES6 modules, in which case files named with the .js extension are also interpreted as ES6 modules.

The CommonJS module format is used in the following situations:

  • Files where the file name ends in .cjs.
  • If the package.json does not contain a type field, or if it contains a type field with a value of commonjs, the filenames will end with .js.
  • If the node binary is executed with the –input-type flag or with the — type-type=commonjs flag, then any code passed through the –eval or –-print argument, or piped in via STDIN (the standard input), is interpreted as CommonJS module code.

Again this is straight-forward, with Node.js defaulting to CommonJS modules for the .js files. If the package is explicitly declared to default to CommonJS modules, then Node.js will interpret the .js files as CommonJS.

The Node.js team strongly recommends that package authors include a type field in package.json, even if the type is commonjs.

Consider a package.json with this declaration:

{

“type”: “module” …

}

This, of course, informs Node.js that the package defaults to ES6 modules. Therefore, this command interprets the module as an ES6 module:

$ node my-module.js

 This command will do the same, even without the package.json entry:

$ node –input-type=module my-module.js 

If instead, the type field had the commonjs, or the –input-type flag specified as commonjs, or if both those were completely missing, then my-module.js would be interpreted as a CommonJS module.

These rules also apply to the import statement, the import() function, and the require() function. We will cover those commands in more depth in a later section. In the meantime, let’s learn how the import() function partly resolves the inability to use ES6 modules in a CommonJS module.

1. Using ES6 modules from CommonJS using import()

The import statement in ES6 modules is a statement, and not a function like require(). This means that import can only be given a static string, and you cannot compute the module identifier to import. Another limitation is that import only works in ES6 modules, and therefore a CommonJS module cannot load an ES6 module. Or, can it?

Since the import() function is available in both CommonJS and ES6 modules, that means we should be able to use it to import ES6 modules in a CommonJS module.

To see how this works, create a file named simple-dynamic-import.js containing the following:

async function simpleFn() {

const simple2 = await import(‘./simple2.mjs’);

console.log(simple2.hello());

console.log(simple2.next());

console.log(simple2.next());

console.log(‘count = ${simple2.default()}’);

console.log(‘Meaning: ${simple2.meaning}’);

}

simpleFn().catch(err => { console.error(err); });

This is a CommonJS module that’s using an ES6 module we created earlier. It simply calls a few of the functions, nothing exciting except that it is using an ES6 module when we said earlier import only works in ES6 modules. Let’s see this module in action:

$ node simple-dynamic-import.js

Hello, world!

1

2

count = 2

Meaning: 42

 

This is a CommonJS module successfully executing code contained in an ES6 module simply by using import().

Notice that import() was called not in the global scope of the module, but inside an async function. As we saw earlier, the ES6 module keyword statements like export and import must be called in the global scope. However, import() is an asynchronous function, limiting our ability to use it in the global scope.

The import statement is itself an asynchronous process, and by extension the import() function is asynchronous, while the Node.js require() function is synchronous.

In this case, we executed import() inside an async function using the await keyword. Therefore, even if import() were used in the global scope, it would be tricky getting a global-scope variable to hold the reference to that module. To see, why let’s rewrite that example as simple-dynamic-import-fail.js:

const simple2 = import(‘./simple2.mjs’);

console.log(simple2);

console.log(simple2.hello());

console.log(simple2.next());

console.log(simple2.next());

console.log(‘count = ${simple2.default()}’);

console.log(‘Meaning: ${simple2.meaning}’);

It’s the same code but running in the global scope. In the global scope, we cannot use the await keyword, so we should expect that simple2 will contain a pending Promise. Running the script gives us this failure:

$ node simple-dynamic-import-fail.js Promise { <pending> }

/home/david/Chapter03/simple-dynamic-import-fail.js:4

console.log(simple2.hello());

^

TypeError: simple2.hello is not a function

at Object.<anonymous> (/home/david/Chapter03/simple-dynamic-import- fail.js:4:21)

at Module._compile (internal/modules/cjs/loader.js:1139:30) at Object.Module._extensions..js

(internal/modules/cjs/loader.js:1159:10)

at Module.load (internal/modules/cjs/loader.js:988:32)

at Function.Module._load (internal/modules/cjs/loader.js:896:14)

at Function.executeUserEntryPoint [as runMain]

(internal/modules/run_main.js:71:12)

at internal/main/run_main_module.js:17:47

We see that simple2 does indeed contain a pending Promise, meaning that import() has not yet finished. Since simple2 does not contain a reference to the module, attempts to call the exported function fail.

The best we could do in the global scope is to attach the .then and .catch handlers to the import() function call. That would wait until the Promise transitions to either a success or failure state, but the loaded module would be inside the callback function. We’ll see this example later in the chapter.

Let’s now see how modules hide implementation details.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *