DEV Community

Cover image for ๐Ÿง™โ€โ™‚๏ธ The Enchanted Tale of JavaScript's Magical Evolution - ESM & CJS ๐ŸŒŸ
nyxb
nyxb

Posted on

๐Ÿง™โ€โ™‚๏ธ The Enchanted Tale of JavaScript's Magical Evolution - ESM & CJS ๐ŸŒŸ

๐Ÿฐ Prologue - The Kingdoms of ESM & CJS


Once upon a time, in the vast universe of JavaScript, there existed two magical realms - ESM, the kingdom of ECMAScript modules, and CJS, the land of CommonJS. Over the past decade, due to the absence of a unified magical script (module system) of JavaScript, CJS (signified by the incantation require('xxx') and module.exports syntax) was the universal spell used in Node.js and NPM packages.

However, in the fateful year of 2015, ESM's powerful runes finally emerged as the standard incantation, prompting a gradual migration towards native ESM. This enabled named exports, better static analysis, tree-shaking, and native browser support. ๐ŸŒณ๐Ÿ“š

// CJS
const circle = require('./circle.js')
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`)
// ESM
import { area } from './circle.mjs'
console.log(`The area of a circle of radius 4 is ${area(4)}`)
Enter fullscreen mode Exit fullscreen mode

ESM was like the prophecy of a new dawn, offering a promising future for JavaScript. Node.js v12 introduced experimental support for native ESM, which was later stabilized in v12.22.0 and v14.17.0. As we approached the end of 2021, many packages began to be shipped in pure-ESM format or as a hybrid of CJS and ESM; meta-frameworks like Nuxt 3 and SvelteKit recommended users to adopt an ESM-first environment. ๐Ÿš€

However, this migration was akin to a grand quest, full of challenges and adventures. For most library authors, shipping dual formats became a safer, smoother path, enabling them to reap the benefits of both realms.


๐Ÿ‰ The Challenge of Compatibility

Despite ESM's promising future, the road was fraught with obstacles. One such hurdle was the inability to use ESM packages in CJS. Node.js was smart enough to allow CJS and ESM packages to co-exist, but attempting to use ESM packages in CJS would result in an error. ๐Ÿšง

// in CJS
const pkg = require('esm-only-package') // will throw an error
Enter fullscreen mode Exit fullscreen mode

The underlying issue was that ESM is asynchronous by nature, which means it couldn't be imported into the synchronous context of require. This generally meant that to use ESM packages, you also needed to use ESM. There was one exception though - ESM packages could be used in CJS using dynamic import().

// in CJS
const { default: pkg } = await import('esm-only-package')
Enter fullscreen mode Exit fullscreen mode

Meanwhile, if you were able to use ESM directly, it was significantly easier as import supports both ESM and CJS.

// in ESM
import { named } from 'esm-package'
import cjs from 'cjs-package'
Enter fullscreen mode Exit fullscreen mode

Some packages started shipping pure-ESM packages, advocating for the ecosystem to transition from CJS to ESM. While this might be the "right thing to do", considering the majority of the ecosystem was still on CJS and the migration wasn't a walk in the park, I found myself more inclined to ship both CJS and ESM formats for a smoother transition.


๐Ÿงช The Alchemical Recipe - package.json

Fortunately, Node allowed us to hold these two realms in balance, marking the scripts with "type": "module" in package.json. This made .js files to be interpreted as ESM by default. However, if we wanted to use both ESM and CJS in the same codebase, we needed to use .cjs and .mjs extensions for CJS and ESM respectively. This was akin to placing a magical seal to ensure harmony.

// package.json
{
  "type": "module",
  "main": "./index.cjs",
  "module": "./index.mjs",
  "exports": {
    "import": "./index.mjs",
    "require": "./index.cjs"
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above example, "main" and "module" entries pointed to our CJS and ESM entries respectively. The "exports" field was especially powerful. It allowed us to map different entry points for require and import, thus ensuring compatibility with both.

// index.mjs
import { named } from 'esm-package'
import cjs from 'cjs-package'
export default named + cjs

// index.cjs
const { named } = require('esm-package')
const cjs = require('cjs-package')
module.exports = named + cjs
Enter fullscreen mode Exit fullscreen mode

While this was a good start, it wasn't perfect. To fully support both ESM and CJS, you needed to ensure that your code and its dependencies were compatible with both module systems. This could be a significant challenge, but with careful planning and a thorough understanding of both systems, it was achievable.


๐Ÿงญ The Journey Ahead

The migration from CJS to ESM may seem like a long and winding journey, but it's a path that the JavaScript community is steadily traversing. With every challenge, there are new learnings, and with every step forward, we're making the ecosystem more future-ready.

As with any quest, patience and persistence are the keys to success. The future of JavaScript is bright with ESM, and the potential it holds is truly magical. So, let's brace ourselves for the exciting journey ahead and be a part of this evolution. ๐ŸŒˆ๐Ÿ’ก

// The future is here, and it's written in ESM!
import { future } from 'javascript'
console.log(`Welcome to the ${future} of JavaScript!`)
Enter fullscreen mode Exit fullscreen mode

๐ŸŒŸ Epilogue

This tale is an ongoing saga in the world of JavaScript. It serves as a testament to the dynamism of this language and the relentless pursuit of its community for better, more efficient coding practices.

Remember, whether you're an ESM enthusiast or a CJS loyalist, in the end, we're all JavaScript wizards weaving our own magic. Let's continue to evolve, innovate, and inspire each other in this enchanted tale of JavaScript's magical evolution. ๐Ÿง™โ€โ™‚๏ธ๐ŸŒŸ๐Ÿš€


๐Ÿ—ฃ The Oracles Speak - The Tools of Prophecy

In the quest of magical JavaScript transformation, there are two legendary tools that JavaScript wizards have been using โ€“ tsup and buildkarium. These tools help in creating a balance between the ESM and CJS realms, ensuring a smoother transition.

โšก tsup

The first magical tool, tsup by @egoist, offers a zero-config building experience for TypeScript projects, bringing harmony between CJS and ESM.

$ tsup src/index.ts --format cjs,esm
Enter fullscreen mode Exit fullscreen mode

The command above will create two files: dist/index.js (for CJS) and dist/index.mjs (for ESM), thus preserving the balance between the two realms.

๐Ÿ— buildkarium

The second tool in our arsenal, buildkarium by the @nyxblabs organization, is a more generalized, customizable, and powerful tool. It offers a unique feature called Stubbing, which eliminates the need for a watcher process. It's akin to a magical charm that ensures your code is always up-to-date.

$ buildkarium --stub
Enter fullscreen mode Exit fullscreen mode

Running the command above once is all you need to keep your library code up-to-date!

The buildkarium tool also enables Bundleless Build, which allows you to maintain the structure of your source code, enabling on-demand importing of submodules, thus optimizing performance.

// karium.config.ts
import { defineBuildConfig } from 'buildkarium'

export default defineBuildConfig({
  entries: [
    // bundling
    'src/index',
    // bundleless, or just copy assets
    { input: 'src/components/', outDir: 'dist/components' },
  ],
  declaration: true,
})
Enter fullscreen mode Exit fullscreen mode

๐ŸŒ‰ Bridging Context Misalignment

Despite these powerful tools, wizards must remain cautious of context misalignment between ESM and CJS. ESM does not recognize __dirname, __filename, require, require.resolve, instead it uses import.meta.url. However, with a little extra spellcasting, we can conjure up a solution:

import { dirname } from 'path'
import { fileURLToPath } from 'url'

const _dirname = typeof __dirname !== 'undefined'
  ? __dirname
  : dirname(fileURLToPath(import.meta.url))
Enter fullscreen mode Exit fullscreen mode

The code snippet above provides an isomorphic __dirname, working in both ESM and CJS contexts.


๐ŸŒŸ The Magical Epilogue

This tale of ESM and CJS is far from over, the JavaScript community continues its quest for the most efficient coding practices. As we venture forth, let's embrace the magical evolution of JavaScript, making the most of both the CJS past and the ESM future.

Remember, whether you're working in ESM or CJS, we're all JavaScript wizards, conjuring up our own magical code in this ever-evolving landscape. ๐Ÿง™โ€โ™€๏ธ๐ŸŒŸ๐Ÿš€


2023-PRESENT ยฉ Dennis Ollhoff

Top comments (0)