Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Marco Gonzalez
Marco Gonzalez

Posted on

     

Node.js: A brief history of cjs, bundlers, and esm

Introduction

If you're a Node.js developer, you've probably heard ofcjs andesm modules but may be unsure why there's two and how do these coexist in Node.js applications. This blogpost will briefly walk you through the history of JavaScript modules in Node.js (with examples 🙂) so you can feel more confident when dealing with these concepts.

The global scope

Initially JavaScript only had a global scope were all members were declared. This was problematic when sharing code because two independent files may use the same name for a member. For example:

greet-1.js

functiongreet(name){return`Hello${name}!`;}
Enter fullscreen modeExit fullscreen mode

greet-2.js

vargreet="...";
Enter fullscreen modeExit fullscreen mode

index.html

<!DOCTYPE html><html><head><metacharset="utf-8"><title>Collision example</title></head><body><!-- After this script, `greet` is a function --><scriptsrc="greet-1.js"></script><!-- After this script, `greet` is a string --><scriptsrc="greet-2.js"></script><script>// TypeError: "greet" is not a functiongreet();</script></body></html>
Enter fullscreen modeExit fullscreen mode

CommonJS modules

Node.js formally introduced the concept of JavaScript modules withCommonJS (also known ascjs). This solved the collision problem of shared global scopes since developers could decide what to export (viamodule.exports) and import (viarequire()). For example:

src/greet.js

// this remains "private"constGREETING_PREFIX="Hello";// this will be exportedfunctiongreet(name){return`${GREETING_PREFIX}${name}!`;}// `exports` is a shortcut to `module.exports`exports.greet=greet;
Enter fullscreen modeExit fullscreen mode

src/main.js

// notice the `.js` suffix is missingconst{greet}=require("./greet");// logs: Hello Alice!console.log(greet("Alice"));
Enter fullscreen modeExit fullscreen mode

npm packages

Node.js development exploded in popularity thanks tonpm packages which allowed developers to publish and consume re-usable JavaScript code.npm packages get installed in anode_modules folder by default. Thepackage.json file present in allnpm packages is especially important because it can indicate Node.js which file is the entry point via the"main" property. For example:

node_modules/greeter/package.json

{"name":"greeter","main":"./entry-point.js"//...}
Enter fullscreen modeExit fullscreen mode

node_modules/greeter/entry-point.js

module.exports={greet(name){return`Hello${name}!`;}};
Enter fullscreen modeExit fullscreen mode

src/main.js

// notice there's no relative path (e.g. `./`)const{greet}=require("greeter");// logs: Hello Bob!console.log(greet("Bob"));
Enter fullscreen modeExit fullscreen mode

Bundlers

npm packages dramatically sped up the productivity of developers by being able to leverage other developers' work. However, it had a major disadvantage:cjs was not compatible with web browsers. To solve this problem, the concept of bundlers was born.browserify was the first bundler which essentially worked by traversing an entry point and "bundling" all therequire()-ed code into a single.js file compatible with web browsers. As time went on, other bundlers with additional features and differentiators were introduced. Most notablywebpack,parcel,rollup,esbuild andvite (in chronological order).

ECMAScript modules

As Node.js andcjs modules became mainstream, theECMAScript specification maintainers decided to include themodule concept. This is why native JavaScript modules are also known as ESModules oresm (short for ECMAScript modules).

esm defines new keywords and syntax for exporting and importing members as well as introduces new concepts like default export. Over time,esm modules gained new capabilities likedynamic import() andtop-level await. For example:

src/greet.js

// this remains "private"constGREETING_PREFIX="Hello";// this will be exportedexportfunctiongreet(name){return`${GREETING_PREFIX}${name}!`;}
Enter fullscreen modeExit fullscreen mode

src/part.js

// default export: new conceptexportdefaultfunctionpart(name){return`Goodbye${name}!`;}
Enter fullscreen modeExit fullscreen mode

src/main.js

// notice the `.js` suffix is requiredimportpartfrom"./part.js";// dynamic import: new capability// top-level await: new capabilityconst{greet}=awaitimport("./greet.js");// logs: Hello Alice!console.log(greet("Alice"));// logs: Bye Bob!console.log(part("Bob"));
Enter fullscreen modeExit fullscreen mode

Over time,esm became widely adopted by developers thanks to bundlers and languages likeTypeScript since they are capable of transformingesm syntax intocjs.

Node.js cjs/esm interoperability

Due to growing demand, Node.js officially added support foresm in version12.x. Backwards compatibility withcjs was achieved as follows:

  • Node.js interprets.js files ascjs modules unless thepackage.json sets the"type" property to"module".
  • Node.js interprets.cjs files ascjs modules.
  • Node.js interprets.mjs files asesm modules.

When it comes tonpm package compatibility,esm modules can importnpm packages withcjs andesm entry points. However, the opposite comes with some caveats. Take the following example:

node_modules/cjs/package.json

{"name":"cjs","main":"./entry.js"}
Enter fullscreen modeExit fullscreen mode

node_modules/cjs/entry.js

module.exports={value:"cjs"};
Enter fullscreen modeExit fullscreen mode

node_modules/esm/package.json

{"name":"esm","type":"module","main":"./entry.js"}
Enter fullscreen modeExit fullscreen mode

node_modules/esm/entry.js

exportdefault{value:"esm"};
Enter fullscreen modeExit fullscreen mode

The following runs just fine:

src/main.mjs

importcjsfrom"cjs";importesmfrom"esm";// logs: { value: 'cjs' }console.log(cjs);// logs: { value: 'esm' }console.log(esm);
Enter fullscreen modeExit fullscreen mode

However, the following fails to run:

src/main.cjs

// OKconstcjs=require("cjs");// Error [ERR_REQUIRE_ESM]:// require() of ES Module (...)/node_modules/esm/entry.js// from (...)/src/main.cjs not supportedconstesm=require("esm");console.log(cjs);console.log(esm);
Enter fullscreen modeExit fullscreen mode

The reason why this is not allowed is becauseesm modules allow top-levelawait whereas therequire() function is synchronous. The code could be re-written to use dynamicimport(), but since it returns aPromise it forces to have something like the following:

src/main.cjs

(async()=>{const{default:cjs}=awaitimport("cjs");const{default:esm}=awaitimport("esm");// logs: { value: 'cjs' }console.log(cjs);// logs: { value: 'esm' }console.log(esm);})();
Enter fullscreen modeExit fullscreen mode

To mitigate this compatibility problem, somenpm packages expose bothcjs andmjs entry points by leveragingpackage.json's"exports" property withconditional exports. For example:

node_modules/esm/entry.cjs:

// usually this would be auto-generated by a toolmodule.exports={value:"esm"};
Enter fullscreen modeExit fullscreen mode

node_modules/esm/package.json:

{"name":"esm","type":"module","main":"./entry.cjs","exports":{"import":"./entry.js","require":"./entry.cjs"}}
Enter fullscreen modeExit fullscreen mode

Notice how"main" points to thecjs version for backwards compatibility with Node.js versions that do not support the"exports" property.

Conclusion

That's (almost) all you need to know aboutcjs andesm modules (as of Dec/2024 🙃). Let me know your thoughts below!

Top comments(1)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
angela_oakley_e0c4698f227 profile image
Angela Oakley
My name Angela aim learning new role in devsec role I love learning about Pipe lines and how to set them up I know it will take a lot of study be I can do it love doing technologies and project.
  • Joined

Thankyou so much enjoy your video very good with helping me understand you are so clever, my self find it hard because of the letter writing more with colour code I have dyslexia.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Joined

More fromMarco Gonzalez

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp