From init to publish - Building a JavaScript library

Introduction

Since the ES6/ES2015 specification update back in (go figure) 2015, there has been an explosion in new and - quite frankly - confusing tooling. The JavaScript landscape has been in quite a bit of chaos and we're only now figuring out which tools we want to carry forward into the future.

Since 2015, Grunt, Gulp and Browserify have fallen to the wayside with considerable amounts of developers preferring to use npm scripts and a bundler like webpack or rollup (or more recently, Parcel).

This guide is going to take an extremely detailed approach to getting your module up and running, so depending on your knowledge, certain sections may be irrelevant, so feel free to use the table of contents to skip sections you're already familiar with.

Note that this guide is currently for MacOS/Unix only, but I'll look at updating with instructions for Windows.

Getting started (System dependencies)

First things first would be to make sure you have your system dependencies in order. Grab the latest stable node version from the node.js website and if it installed correctly, you should have also got npm bundled with it. You can check this by opening your terminal and typing `npm -v` or `npm which`. You should see somewhere in the neighbourhood of version 5.x.x.

Version control

  1. Create a repository on your git provider of choice, e.g. github, bitbucket, or gitlab
  2. Clone the repository locally
  3. In Terminal, go to the directory where your projects live
  4. Change to the project directory with cd javascript-module

For our first action in this project, we're going to initialise an npm package.

Run npm init. This will ask you a bunch of questions and if you're happy with all the defaults, you can run npm init -y and it will accept all defaults. In your project directory, you will now see apackage.json file with all the related metadata for your package.

package.json

Name

To play your part in improving our ecosystem, consider setting the package name to a scoped package, where you prefix the package name with your author name. For example, @stak-digital/javascript-module.

This has numerous benefits, including:

  • Scoped packages are private by default, meaning you have to opt in to making them public which could save you from accidental publishes. You can create an organisation and have your scoped packages associated with that organisation automatically (for example: https://www.npmjs.com/org/stak-digital).
  • It's harder for people to spoof your package name. There was a wave of malware packages with common misspellings of popular package names, so if you ever typed, for example, npm install angulra, you might get a malicious package.
  • You also have the option to unpublish scoped packages as long as they don't have any dependents, unlike un-scoped packages which npm will just take possession of and remove it from your account. This change was introduced after the left-pad massacre

Main

The main key refers to the file that should be targeted when users bundle your package. This should point to the production-ready distribution version of your package which is index.js in the root directory by default (a convention worth sticking to for simple packages).

Scripts

The scripts object is where much of the magic happens in a project. To add a new command, type the command name as the key and the command to execute as the value. There's also various script hooks you can use such as prepublishOnly which will run before you publish your package. You can use this to run linters or tests.

npm scripts

Then type npm run hello-world for the magic

running an npm script

For your custom commands, you have to type npm run, but for built in npm commands, you can just typenpm script [name]. For example:npm test, npm start

There are dozens of different scripts out of the box, and we'll go over a few of them as and when they become relevant, but if you'd like to do some light reading, you can see them here.

Repository

"repository": {
  "type": "git",
  "url": "git+https://github.com/stak-digital/javascript-module.git"
}

Since we initialised into an existing repository, npm was able to read the repository details from the .git file in the root. If you ever rename or move the repository, be sure to update all the links in here.

License

The two most common licenses for open source packages areMIT andISC. ISC is the default for npm packages which was adapted from MIT with simpler terms. They are both very permissive licenses, which is mostly suitable for open source, however, if you would like to learn more about licenses, you can visit https://choosealicense.com/ for a choose your own adventure license game. The license text is commonly included in the bottom of your readme or in a license.md file in the root directory.

I think this calls for a commit.

To stage our new package file, type git add package.jsonor git add -all to add everything

To commit staged changes: git commit -m "chore: initial commit"

To push to the cloud: git push

Telling npm and bundlers how to load our library

The package.json is responsible for telling bundlers where to read your library from. There's two important keys for our purposes. The most important is main. Because npm packages are supposed to be es5, you need to point your main key to the babel transformed script (e.g. ./index.js or./lib/index.js). The other important key is themodule key which points to the es-module version. Popular bundlers such as webpack and rollup will use the es-module version. Creating the es-module version

Create the source "module" file. This is where we will write in ES6+ which we will then convert to ES5.

Create a src directory and make an index.js file under src to fill with js content.

There's a few directions to go with exports, you can have a single default export, individual named exports or a combination of both.

Default export

export default function () {}

Will be ingested by a user as follows:

import someName from 'your-module';
Will import only the default export as a function called `someName` Named export
export function someFunction() {}
export function otherFunction () {}

Will be ingested as follows:

import {someFunction, otherFunction} from 'your-module'

Or:

import {someFunction as anotherNameForThatFunction} from 'your-module';

Combination:

export function someUtil() {}
export default function() {}
Will be used as follows:
import someFunction, {someUtil} from 'your-module';

Okay, so let's make the module.

What I want to do is create a function that takes a HTMLElement and and array of colours, and on click of the HTMLElement it will randomly pick colours and set the background colour of the element.

For making your functions, it can be helpful to write the interface first, to make sure you write a sane interface that you and others would enjoy using. It also gives you the benefit of writing your documentation first as a pseudo-specification and then worrying about the implementation after.

src/index.js
export default function(targetElement, colors) {

}

Now let's add some jsdoc to the function

/**
 * @param {HTMLElement} targetElement
 * @param {Array.<string>} colors
 */
export default function(targetElement, colors) {

}

And the real meat and potatoes

function changeEventTargetColor(colors, event) {
  // get a random item
  const color = colors[Math.floor(Math.random() * colors.length)];
  event.target.setAttribute('style'', `background-color: ${color}`);
}

/**
 * @param {HTMLElement} targetElement
 * @param {Array.<string>} colors
 */
export default function(targetElement, colors) {
  targetElement.addEventListener('click'', changeEventTargetColor.bind(null, colors));
}

Transpiling the library

I'm going to be using a bundler (the bundler of choice for making efficient flat distributables is Rollup) but you could just as easily use Babel CLI for a library this simple.

  • Run npm install -D rollup rollup-plugin-babel babel-core babel-preset-env
  • Create .babelrc in the root directory and fill it with the following:
.babelrc
{
  "presets": [
    [
      "env",
      {
      "modules": false
      }
    ]
  ]
}

Babel-preset-env has all supported babel plugins contained in it and it was made to prevent the need for includinges2015, es2017 etc presets in your config. Instead you tell babel what platforms you're targeting and it will transform accordingly. In the above config we are not specifying browser or node targets, so it will apply all transforms.

If I wanted to add specific browser support you could include"browsers": "last 2 versions". Themodules key is for rollup because it will bundle first and then transform afterwards, unlike other common bundlers such as webpack.

  • Create rollup.config.js in the root directory
  • Fill rollup config with config options
rollup.config.js
import babel from 'rollup-plugin-babel';

export default {
	// relative path to index
	input: './src/index.js',
	output: {
	file: './index.js',
	// commonjs e.g. module.exports
	format: 'cjs'
},
	plugins: [
		// takes a config object. Include presets or plugins to override babelrc
		babel({
			'exclude': 'node_modules/**',
		})
	]
}

Now we can build our library using rollup on the CLI, but you don't want to have to type it out every time, so let's add a build script to the package.json

package.json
"scripts": {
	"build": "rollup --config rollup.config.js"
}

Now in terminal, run npm run build and presto your library should be built in Commonjs in the root directory. Technically, the library is ready to publish, but we need to include instructions for people who want to use it.

Adding a readme

Create readme.md and fill it with the following information

  • Library name
  • Library purpose (what problem does it address)
  • API
readme.md
# Element color changer
## Purpose
This was made to change the color of a clicked element to a random color
from an array of color strings
## Usage
`npm install --save color-switch`
```
import colorSwitch from 'color-switch';
const yourElement = document.getElementById('el');
colorSwitch(yourElement, [
'#f0f',
'red',
'rgba(0, 0, 0, 0.4)',
'green'
]);
```

This is the basic requirement for your library. Time to publish!

In terminal: npm login and you will be prompted for a username and password. It will then publish to that user. Now run npm publish which will publish to the npm registry under the name from package.json at the version from package.json.

Running npm publish -> [email protected]

npm ERR! publish Failed PUT 403
npm ERR! code E403
npm ERR! You do not have permission to publish "color-switch". Are you logged in as the correct user? : color-switch

Uhoh, what happened? 403 means forbidden. If we go to the npm registry and search for color-switch we can see that the package name is already taken. If the package already exists, it will try and publish to that, and if you aren't an owner or contributor to the package it will fail.

We have two options:

  1. Rename the package to a more unique name
  2. Create a scoped package (as discussed earlier)

The preferable option is creating a scoped package which lets you keep your package name the same, but just prefix it with your author name. For more information about scoped packages, read the npm docs (https://docs.npmjs.com/misc/scope).

Let's change the package name in package.json. For me, I can make it my personal author name (e.g.@lukeboyle/color-switch) or an organisation namespace (e.g. @stak-digtal/color-switch). Organisation namespaces allow all members of an organisation to individually contribute to the packages.

For this one, I'll just change it to my personal one, so inname in package.json I'll make it@lukeboyle/color-switch. This will change the way people use the package, so in the readme usage section, update the package name:

readme.md
`npm install @lukeboyle/color-switch`
```
import colorSwitch from '@lukeboyle/color-switch';`
```

Now to publish it, for the first time you have to define the access as public. If you have a paid account and you usenpm publish on a scoped package it will publish it privately. Use npm publish --access=public to publish it to the registry.

That's enough to call it 'done', but there's many things that can be done to improve the visibility and appeal of your library.

Bonuses

Interactive Demo

Interactive demos are good because it gives instant legitimacy to your readme. You add a link to your readme and people are able to click it and check if it meets their needs without having to install it unnecessarily.

Recently I've been using the bundler Parcel to run my demos. The reason for this choice is because of the low amount of configuration overhead required to get going. You essentially make a html file that points to a js file of you using the repo and parcel will handle the rest. Another reason it is very good is because, unlike other solutions where you need to manually watch your library for changes, parcel will automatically watch for changes in your library. This allows you to set up your dream interface in the demo and then write the library with each save causing a reload in your demo to show off your changes.

Create a demo folder and in that folder, create anindex.html and index.js file. Optionally, you can create a style.css file if you want to apply styles and Parcel will take care of it. Otherwise, you can do these in a style tag in html.

demo/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Color switch demo</title>
    <script src="index.js" defer></script>
    <style>
      body {
        height: 100vh;
        width: 100vw;
        margin: 0;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      #the_box {
        height: 50vh;
        width: 50vw;
      }
    </style>
  </head>
  <body>
    <div id="the_box">
      <p>
        Here's your box.
      </p>
    </div>
  </body>
</html>
demo/index.js
import colorSwitch from '../src/index';
const element = document.getElementById('the_box');
colorSwitch(element, ['red', 'green', 'blue']);

npm install --save-dev parcel-bundler

In package.json, you'll have to update your scripts

{
"bundle": "rollup --config rollup.config.js",
"build": "npm run bundle && parcel build './demo/index.html' -d demo/dist",
"start": "npm run build && parcel './demo/index.html' -p 3000 -d demo/dist"
}

In terminal, run npm start and visit your browser at http://localhost:3000

You'll now see that:

  1. The library should be working as expected; and
  2. You'll be able to trigger a re-build by changingdemo/index.js or src/index.js

Note: parcel will generate a .cache directory in the root and you should add this to your .gitignore, but you should commit the dist folder if you plan to use github pages.

Now to upload your demo, visit your repo page on github and go to Settings.

Scroll down to the github pages section.

Choose master branch and click save. Now whatever you commit it will be on those github pages. This is the easiest way because you get free hosting and a free domain, but you have the option to set up your own domain and hosting, or you can redirect your domain's nameservers to github.

Now, when you want to visit your site, go tohttps://your-username/your-library which will land on a HTML rendering of the readme. For my library, it's https://stak-digital.github.io/javascript-module/

To get to the demo, the link is https://stak-digital.github.io/javascript-module/dist/

Now, you should add that link to the top of your readme.md!

e.g.

## Demo link: [https://stak-digital.github.io/javascript-module/dist/](https://stak-digital.github.io/javascript-module/dist/)
Or
## [Demo link](https://stak-digital.github.io/javascript-module/dist/)