Real example how to create a universal open-source javascript module that will work in both browser and NodeJS environments and publish to the NPM registry

Why Universal NPM Modules?

There are a lot of inconsistency in the JavaScript world. Since 1997 (ECMAScript 1) and before 2015 (ECMAScript 6) there were no standards for JavaScript modules.

ECMAScript specification is a standardised specification of the JavaScript language.

In ES 2015 (ES6) modules were added to the specification and new export and import statements introduced.

Before ECMAScript 2015, there were a few modules standards proposals, some of them are:

CommonJS

AMD

RequireJS

OSGI JavaScript Bundles and others

On the way to universal modules, UMD wrapper has being proposed.

Universal Module Definition (UMD) is an attempt to offer compatibility with the most popular scripts loaders (AMD and CommonJS).

UMD builds are available in both, browsers and NodeJS.

Open-Source

Open-source approach will help your project to accept improvements and fixes from the community.

In this example we will use a public GitHub repository async-assets-loader.

Project Initialisation

Working dir

Create a directory:

mkdir async-assets-loader

Change directory:

cd async-assets-loader/

Package config

Create a package.json file:

npm init for unscoped or npm init --scope=@scope-name for scoped package.

A scope allows you to create a package with the same name as a package created by another user or organisation without conflict. Scoped packages are preceded by their scope name.

It will ask questions about package name, version, description, entry point, test command, git repository, keywords, author, license. As a result package.json file will be generated, see example below:

package.json example

Source files

Main module source file is configured in package.json file, main property. In our case it will look for an async-assets-loader.js file in the dist folder.

In the file, add a function as a property of the exports object. This will make the function available to others code. Example:

exports.hello = function () {return "Hello World";}

exports is a reference to the module.exports that is shorter to type. However, be aware that like any variable, if a new value is assigned to exports, it is no longer bound to module.exports

module.exports.hello = true; // OK

exports.hello = true; // OK (shortcut)

exports = { hello: false }; // Not OK, exports is re-assigned

You can find the original module source code of index.js file here.

Build and Test

First of all we are using module.exports to export module as NodeJS module. This way is not compatible with browsers by default. To make it compatible with browsers we will use webpack and it’s umd library. To add libraries, run

npm i webpack webpack-cli --save-dev

After installation is done, lets create webpack config, see example below:

webpack.config.js

Check source code for webpack.config.js here. Now, we can run build command:

./node_modules/.bin/webpack

As result it will create async-assets-loader.js and place it into the dist folder:

webpack build results

Because of production mode in the webpack config file, it will also minify the resulting code of the module.

Now we are ready to test the module. First of all lets ensure it will be working in the browser. For test in browser let’s use karma, chrome-launcher and jasmine.

npm i karma karma-chrome-launcher karma-jasmine jasmine-core --save-dev

npm i jasmine --save-dev

Now we are ready to create karma config:

./node_modules/.bin/karma init karma.conf.js

See how it might looks like:

karma config init

Here is the link to the karma.conf.js file. Note, singleRun is set as true.

Let’s add some tests to the test/loadAssetsSpec.js file.

describe('async-assets-loader', () => {

// asyncAssetsLoader is loaded from karma.conf.js

let loader = new asyncAssetsLoader(); // all assets are prefixed with "/base/" path

let jsfile = '/base/test/stubs/testf.js'; it('should load single asset correctly', (done) => {

loader.load({uri: jsfile, type: "script"}, () => {

expect(testf).toBeDefined();

done();

});

});

}); // testf.js contains "function testf() {}"

It’s time to start testing our code in Chrome browser. If you want to run tests in another browser, check available browsers launchers page.

npm run runs an arbitrary command from a package’s "scripts" object. It is used by the test, start, restart, and stop commands, but can be called directly, as well.

npm run commands will automatically include local node_modules/.bin to the PATH.

Lets update npm test command in scripts.test property of package.json file to webpack && karma start karma.conf.js . This will make a new build and run tests:

npm run test

And it’s shortcut:

npm test

karmajs tests in browser

Awesome!

Time to test in NodeJS environment. Generate basic jasmine config:

./node_modules/.bin/jasmine init

It will create a file spec/support/jasmine.json, we will not change it. Lets create spec/testNodeSpec.js file:

describe('async-assets-loader', () => {

const assetsLoader = require("../dist/async-assets-loader");



it('assetsLoader is function', () => {

expect(typeof assetsLoader).toBe("function");

});

});

And update test command in package.json:

"scripts": {

"test": "webpack && karma start karma.conf.js && jasmine"

},

Now npm test will make a build, run browser tests and run basic NodeJS package test.

browser and npm tests

Publish to the Registry

After all tests have being passed, we are ready to update module version and publish to the NPM repository. If you didn’t make a login yet run the login command.

npm login

All files in the package directory are included if no local .gitignore or .npmignore file exists. If both files exist and a file is ignored by .gitignore but not by .npmignore then it will be included.

When you are ready to launch your first version, update your module version from 0.0.1 to 1.0.0:

npm version 1.0.0

And publish to the registry with command:

npm publish

All published packages are available on the npmjs website, example: https://www.npmjs.com/package/async-assets-loader

npm publish

Add Info Badges

After publishing to the NPM registry we can add info badges about status of our open-source project with https://shields.io/. It could be build status, code coverage, size, downloads, version, etc. Status is updating automatically.

Add such lines to the top of README.md file in your project root:

# async-assets-loader

[![NPM Version](https://img.shields.io/npm/v/async-assets-loader.svg?style=flat-square)](https://www.npmjs.com/package/async-assets-loader)

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)

[![NPM Downloads](https://img.shields.io/npm/dt/async-assets-loader.svg?style=flat-square)](https://www.npmjs.com/package/async-assets-loader)

The README file will be shown on the package page.

An npm package README file must be in the root-level directory of the package.

Badges example:

package badges example

Setup CI

When you are contributing with a team, a good option is to setup a continuous integration (CI). Popular tool for open-source projects is Travis CI. It has support of NodeJS and integration with GitHub and NPM. They have deploying to npm article with a short description of how to setup tests against a few NodeJS versions and deploy the package to the NPM.