Skip to content

Instantly share code, notes, and snippets.

@murraco
Last active January 12, 2024 00:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save murraco/9b99d2cf7b4efedcee70ef466edcfd21 to your computer and use it in GitHub Desktop.
Save murraco/9b99d2cf7b4efedcee70ef466edcfd21 to your computer and use it in GitHub Desktop.

JavaScript Modules

  1. How do I make the tree shakeable?
  2. What JS module systems should I target (CommonJS, AMD, ES6).
  3. Should I transpile the source?
  4. Should I bundle the source?
  5. What files should I publish?

Let's try to address all the above questions now.

Different Types of JS Module Systems

1. CommonJS

  • Implemented by Node.js
  • Used for the server side when you have modules installed.
  • No runtime/async module loading.
  • Import via “require”.
  • Export via “module.exports”.
  • When you import you get back an object.
  • No tree shaking, because when you import you get an object.
  • No static analyzing, as you get an object, so property lookup is at runtime.
  • You always get a copy of an object, so no live changes in the module itself.
  • Poor cyclic dependency management.
  • Simple syntax
// log.js
function log() {
  console.log('Example of CJS module system');
}

// Expose log to other modules
module.exports = { log }
// index.js
var logModule = require('./log')

logModule.log()

2. AMD: Async Module Definition

= Implemented by RequireJs.

  • Used for the client side (browser) when you want dynamic loading of modules.
  • Import via “require”.
  • Complex syntax.
// log.js
define(['logModule'], function() {
  // Export (expose) foo for other modules
  return {
    log: function() {
      console.log('Example of AMD module system')
    }
  }
})
// index.js
require(['log'], function (logModule) {
  logModule.log()
})

3. UMD: Universal Module Definition

  • Combination of CommonJS + AMD (that is, syntax of CommonJS + async loading of AMD).
  • Can be used for both AMD/CommonJS environments
  • UMD essentially creates a way to use either of the two, while also supporting the global variable definition. As a result, modules are capable of working on both client and server.
// log.js
(function (global, factory) {
  if (typeof define === 'function' && define.amd) {
    define('exports'], factory)
  } else if (typeof exports !== 'undefined') {
    factory(exports)
  } else {
    var mod = {
      exports: {}
    }
    factory(mod.exports)
    global.log = mod.exports
  }
})(this, function (exports) {
  'use strict'

  function log() {
    console.log('Example of UMD module system')
  }
  // expose log to other modules
  exports.log = log
})

4. ES6 (ES2015)

  • Used for both server/client side.
  • Runtime/static loading of modules supported.
  • When you import, you get back bindings value (actual value).
  • Import via “import” and export via “export”.
  • Static analyzing - you can determine imports and exports at compile time (statically) — you only have to look at the source code, you don’t have to execute it.
  • Tree shakeable, because of static analyzing supported by ES6.
  • Always get an actual value so live changes in the module itself.
  • Better cyclic dependency management than CommonJS.
// log.js
const log = () => {
  console.log('Example of ES module system');
}
export default log
// index.js
import log from './log'

log()

This is all about different types of JS module systems and how they have evolved.

Best practices before publishing

1. Tree Shaking

Tree shaking is a term commonly used in the context of JavaScript or dead-code elimination. It relies on the static structure of ES2015 module syntax, that is, import and export. The name and concept have been popularized by the ES2015 module bundler rollup.

Webpack and Rollup both support tree shaking, though we need to keep certain things in mind so that our code is tree shakeable.

  1. Can be tree shaken as we export as ES modules and only shake is included in the import:
// shakebake.js
const shake = () => console.log('shake')
const bake = () => console.log('bake')

export { shake, bake }
// index.js
import { shake } from './shakebake.js'
  1. Cannot be tree shaken as we have exported an object, and therefore both shake and bake are included in the import:
// shakebake.js
const shake = () => console.log('shake')
const bake = () => console.log('bake')

export default { shake, bake }
// index.js
import { shake } from './shakebake.js'

2. Publish all module variants

We should publish all the module variants, like UMD and ES, because we never know which browser/webpack version our consumers might use this library/package in.

// package.json
{
  "name": "js-module-system",
  "version": "0.0.1",
  ...
  "main": "dist/index.js",
  "module": "dist/index.es.js"
}
  • The main field of the package.json file is usually used to point to the UMD version of the library/package.
  • The module field of the package.json is used to point to the ES version of the library/package. Previously, many fields were used like js:next and js:main, but module is now standardized and is used by bundlers as a lookup for the ES version of the library/package.

Always try to publish the ES version of your library/package as well, because all the modern browsers now support ES modules. So you can transpile less, and ultimately you’ll end up shipping less code to your users. This will boost your application’s performance.

Webpack vs Rollup vs Babel?

Each tool has it’s own benefits and serves different purpose based on your needs.

Webpack

Webpack is a gret module bundler that is widely accepted and mostly used for building SPAs. It gives you all the features out of the box like code splitting, async loading of bundles, tree shaking, and so on. It uses the CommonJS module system.

RollupJS

Rollup is also a module bundler similar to Webpack. However, the main advantage of rollup is that it follows new standardized formatting for code modules included in the ES6 revision, so you can use it to bundle the ES module variant of your library/package. It doesn't support async loading of bundles.

Babel

Babel is a transpiler for JavaScript best known for its ability to turn ES6 code into code that runs in your browser (or on your server) today. It just transpiles and doesn't bundle your code.

Use Rollup for libraries and Webpack for apps.

...

What should we publish?

  • LICENSE
  • README.md
  • Changelog
  • Metadata (main, module, bin) - package.json
  • Control through the files property - package.json

In package.json, the files field is an array of file patterns that describes the entries to be included when your package is installed as a dependency. If you name a folder in the array, then it will also include the files inside that folder.

We will include the lib and dist folders in files field in our case.

// package.json
{
  ...
  "files": ["dist", "lib"]
}

Finally the library is ready to publish. Just type the npm run build command in the terminal, and you can see the following output. Closely look at the dist and lib folders.

Wrap up

  • Enable tree shaking.
  • Target at least ES6 and CommonJS module systems.
  • Use Babel and bundlers for libraries.
  • Use bundlers for core packages.
  • Set the module field of package.json to point to the ES version of your module (it helps in tree shaking).
  • Publish the folders which have transpiled as well as bundled versions of you module.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment