Which Module Formats Should Your JavaScript Library Support?

As a web application grows and more features are added, modularizing the code improves readability and maintainability. In a basic web application, the application fetches and loads JavaScript files by having multiple <script /> tags in an HTML document. Often, these <script /> tags reference libraries from CDNs, such as cdnjs or unpkg , before the bundled application code. This approach involves manually ordering the <script /> tags correctly. For example, if you wanted to use a library like React in the browser (without any additional tooling, just plain HTML, CSS and JavaScript), then you would need to... Continually adding more JavaScript files becomes more tricky because not only do you need to ensure that their dependencies precede them, but also that there are no naming collisions as a result of variables sharing the same global scope and overriding each other. With no native, built-in features for namespacing and modules in early versions of JavaScript language, different module formats have been introduced over the years to fill this void until the ES6 specification, which includes official syntax for writing modules via ECMAScript (ES6) modules. Below, I'm going to provide a brief overview of the following module systems: For developers, understanding each module system allows them to better determine which one best suits their applications' needs. For library authors, choosing the module systems their library should be compatible with depends on the environments where their intended users' applications run. One of the earliest ways of exposing libraries in the web browser, immediately invoked function expression (IIFE) are anonymous functions that run right after being defined. Variables declared within the IIFE are scoped to the anonymous function, not to the global scope. This keeps variables inaccessible from outside the IIFE (restricts access only to the code within the IIFE) and prevents the pollution of the global scope. A common design pattern that leverages IIFEs is the Singleton pattern , which creates a single object instance and namespaces code. This object serves as a single point of access to a specific group of functionality. For real-world examples, look no further than the Math object or jQuery . For example... Writing modules this way is convenient and supported by older browsers. In fact, you can safely concatenate and bundle together multiple files that contain IIFEs without worrying about naming and scoping collisions. For modules with dependencies, you can pass those dependencies as arguments to the IIFE's anonymous function. If the dependency is of a primitive data type, then any changes made to the dependency in the global scope will not affect the IIFE module. If the dependency is an object, then any changes made to the dependency's properties in the global scope will affect the IIFE module, unless you destructure out the properties used in the IIFE module. For the previous example, if we override the Math object's pow method after running the IIFE, then subsequent calls to the module's calculateMethod method will return a different value. If we modify the IIFE module to accept the Math object as a dependency argument, then the module "captures a snapshot" of the object's properties via destructuring. Overriding these properties' values in later statements will have no affect on the module's methods. Nevertheless, IIFE modules load synchronously, which means correctly ordering the module files is critical. Otherwise, the application will break. For large projects, IIFE modules can be difficult to manage, especially if you have many overlapping and nested dependencies. The default module system of Node.js , CommonJS (CJS) uses the require syntax for importing modules and the module.exports and exports syntax for default and named exports respectively. Each file represents a module, and all variables local to the module are private since Node.js wraps the module within a function wrapper. For example, this module... Becomes... The module not only has its variables scoped privately to it, but it still has access to the globals exports , require and module . __filename and __dirname are module scoped and hold the file name and directory name of the module respectively. The require syntax lets you import built-in Node.js or third-party modules locally installed and referenced in package.json (if supplied a module name) or custom modules (if supplied a relative path). ( ./circle.js ) ( Some File in the Same Directory as ./circle.js ) CommonJS require statements are synchronous, which means CommonJS modules are loaded synchronously. Provided the application's single entry point, CommonJS automatically knows how to order the modules and dependencies and handle circular dependencies. If you decide to use CommonJS modules for your client-side applications, then you need additional tooling, such as Browserify , RollupJS or Webpack , to bundle and transpile the modules into a single file that can be understood and ran by a browser. Remember, CommonJS is not part of the official JavaScript specification. Much like IIFEs, CommonJS was not designed for generating small bundle sizes. Bundle size was not factored into CommonJS's design since CommonJS is mostly used for developing server-side applications. For client-side applications, the code must be downloaded first before running it. As a result, larger bundle sizes serve as performance bottlenecks that slow down an application. Plus, without tree-shaking, this makes CommonJS a non-optimal module system for client-side applications. Unlike IIFEs and CommonJS, Asynchronous Module Definition (AMD) asynchronously loads modules and their dependencies. Originating from the Dojo Toolkit, AMD is designed for client-side applications and doesn't require any additional tooling. In fact, all you need to run applications following the AMD module format is the RequireJS library, an in-browser module loader. That's it. Here's a simple example that runs a simple React application, structured with AMD, in the browser. ( index.html ) ( main.js ) Calling the requirejs or define methods registers the factory function (the anonymous function passed as the second argument to these methods). AMD executes this function only after all the dependencies have been loaded and executed. Each module references dependencies by name, not by their global variable. Each dependency name is mapped to the location of the dependency's source code. AMD allows multiple modules to be defined within a single file, and it supports older browsers. However, it is not as popular as more modern module formats, such as ECMAScript modules and Universal Module Definition. For libraries that support both client-side and server-side environments, the Universal Module Definition (UMD) offers a unified solution for making modules compatible with many different module formats, such as CommonJS and AMD. Regardless of whether an application consumes your library as a CommonJS, AMD or IIFE module, UMD conditionally checks for the module format being used at runtime and executes code specific to the detected module format. The UMD template code is an intimidating-looking IIFE, but it is not difficult to understand. Here's UMD in action from React 's development library. Here, the factory function contains the actual library code. ECMAScript modules (ESM), the most recently introduced and recommended module format, is the standard and official way of handling modules in JavaScript. Module bundlers support ESM and optimize the code using techniques like tree-shaking (removes unused code from the final output), which are not supported by other module formats. Loading and parsing modules is asynchronous, but executing them is synchronous. This module format is commonly used in TypeScript applications. Similar to CommonJS, ESM provides multiple ways for exporting code: default exports or named exports. A file can only have one default export ( default export syntax), but it can have any number of named exports ( export function , export const , etc. syntax). ( ./circle.js ) Importing these named exports separately tells the module bundler which portions of the imported module should be included in the outputted code. Any named exports not imported are omitted. This decreases the size of the library, which is useful if your library relies on a few methods from a large utility library like lodash . ( Some File in the Same Directory as ./circle.js ) Ask yourself the following questions while building your library: Once you answer these questions, you can tell the module bundler to output the code in specific module formats for users to consume it within their own libraries/projects. You can also check out our new course, The newline Guide to Creating React Libraries from Scratch , where we teach you everything you need to know to succeed in creating your own library. 

Thumbnail Image of Tutorial Which Module Formats Should Your JavaScript Library Support?