Skip to content

Latest commit

 

History

History
398 lines (296 loc) · 20.6 KB

12_authoring_libraries.md

File metadata and controls

398 lines (296 loc) · 20.6 KB

Authoring Libraries

npm is one of the reasons behind the popularity of Node.js. Even though it was used initially for managing back-end packages, it has become increasingly popular for front-end usage as well. As you saw in the earlier chapters, it is easy to consume npm packages through Webpack.

Eventually, you may want to author packages of your own. Publishing one is relatively easy. There are a lot of smaller details to know, though. This chapter goes through those so that you can avoid some of the common problems.

Anatomy of a npm Package

Most of the available npm packages are small and include just a select few files, such as:

  • index.js - On small projects it's enough to have the code at the root. On larger ones you may want to start splitting it up further.
  • package.json - npm metadata in JSON format
  • README.md - README is the most important document of your project. It is written in Markdown format and provides an overview. For simple projects the whole documentation can fit there. It will be shown at the package page at npmjs.com.
  • LICENSE - You should include licensing information within your project. You can refer to it from package.json.

In larger projects, you may find the following:

  • CONTRIBUTING.md - A guide for potential contributors. How should the code be developed and so on.
  • CHANGELOG.md - This document describes major changes per version. If you do major API changes, it can be a good idea to cover them here. It is possible to generate the file based on Git commit history, provided you write nice enough commits.
  • .travis.yml - Travis CI is a popular continuous integration platform that is free for open source projects. You can run the tests of your package over multiple systems using it. There are other alternatives of course, but Travis is very popular.
  • .gitignore - Ignore patterns for Git, i.e., which files shouldn't go under version control. It can be useful to ignore npm distribution files here so they don't clutter your repository.
  • .npmignore - Ignore patterns for npm. This describes which files shouldn't go to your distribution version. A good alternative is to use the files field at package.json. It allows you to maintain a whitelist of files to include into your distribution version.
  • .eslintignore - Ignore patterns for ESLint. Again, tool specific.
  • .eslintrc - Linting rules. You can use .jshintrc and such based on your preferences.
  • webpack.config.js - If you are using a simple setup, you might as well have the configuration at project root.

In addition, you'll likely have various directories for source, tests, demos, documentation, and so on.

Understanding package.json

All packages come with a package.json that describes metadata related to them. This includes information about the author, various links, dependencies, and so on. The official documentation covers them in detail.

I've annotated a part of package.json of my React component boilerplate below.

{
  /* Name of the project */
  "name": "react-component-boilerplate",
  /* Brief description */
  "description": "Boilerplate for React.js components",
  /* Who is the author + optional email + optional site */
  "author": "Juho Vepsalainen <email goes here> (site goes here)",
  /* Version of the package */
  "version": "0.0.0",
  /* `npm run <name>` */
  "scripts": {
    "start": "webpack-dev-server",

    "test": "karma start",
    "test:tdd": "karma start --auto-watch --no-single-run",
    "test:lint": "eslint . --ext .js --ext .jsx",

    "gh-pages": "webpack",
    "gh-pages:deploy": "gh-pages -d gh-pages",
    "gh-pages:stats": "webpack --profile --json > stats.json",

    "dist": "webpack",
    "dist:min": "webpack",
    "dist:modules": "babel ./src --out-dir ./dist-modules",

    "preversion": "npm run test && npm run test:lint && npm run dist && npm run dist:min && git commit --allow-empty -am \"Update dist\"",
    "prepublish": "npm run dist:modules",
    "postpublish": "npm run gh-pages && npm run gh-pages:deploy",
    /* If your library is installed through Git, you may want to transpile it */
    "postinstall": "node lib/post_install.js"
  },
  /* Entry point for terminal (i.e., <package name>) */
  /* Don't set this unless you intend to allow CLI usage */
  "bin": "./index.js",
  /* Entry point (defaults to index.js) */
  "main": "dist-modules",
  /* Package dependencies */
  "dependencies": {
    "react": "^0.14.0",
    "react-dom": "^0.14.0"
  },
  /* Package development dependencies */
  "devDependencies": {
    "babel": "^6.3.17",
    "babel-eslint": "^5.0.0-beta6",
    ...
    "webpack": "^1.12.2",
    "webpack-dev-server": "^1.12.0",
    "webpack-merge": "^0.7.0"
  },
  /* Package peer dependencies. The consumer will fix exact versions. */
  /* In npm3 these won't get installed automatically and it's up to the */
  /* user to define which versions to use. */
  "peerDependencies": {
    "lodash": ">= 3.5.0 < 4.0.0"
  }
  /* Links to repository, homepage, and issue tracker */
  "repository": {
    "type": "git",
    "url": "https://github.com/bebraw/react-component-boilerplate.git"
  },
  "homepage": "https://bebraw.github.io/react-component-boilerplate/",
  "bugs": {
    "url": "https://github.com/bebraw/react-component-boilerplate/issues"
  },
  /* Keywords related to package. */
  /* Fill this well to make the package findable. */
  "keywords": [
    "react",
    "reactjs",
    "boilerplate"
  ],
  /* Which license to use */
  "license": "MIT"
}

As you can see, package.json can contain a lot of information. You can attach non-npm specific metadata there that can be used by tooling. Given this can bloat package.json, it may be preferable to keep metadata at files of their own.

npm Workflow

Working with npm is surprisingly simple. To get started, you need to use npm adduser (aliased to npm login). It allows you to set up an account. After this process has completed, it will create ~/.npmrc and use that data for authentication. There's also npm logout that will clear the credentials.

T> When creating a project, npm init respects the values set at ~/.npmrc. Hence it may be worth your while to set reasonable defaults there to save some time.

Publishing a Package

Provided you have logged in, creating new packages is just a matter of executing npm publish. Given that the package name is still available and everything goes fine, you should have something out there! After this, you can install your package through npm install or npm i as we've done so many times before in this book.

An alternative way to consume a library is to point at it directly in package.json. In that case, you can do "depName": "<github user>/<project>#<reference>" where <reference> can be either commit hash, tag, or branch. This can be useful, especially if you need to hack around something and cannot wait for a fix.

T> If you want to see what files will be published to npm, consider using a tool known as irish-pub. It will give you a listing to review.

Bumping a Version

In order to bump your package version, you'll just need to invoke one of these commands:

  • npm version <x.y.z> - Define version yourself.
  • npm version <major|minor|patch> - Let npm bump the version for you based on SemVer.
  • npm version <premajor|preminor|prepatch|prerelease> - Same as previous expect this time it will generate -<prerelease number> suffix. Example: v2.1.2-2.

Invoking any of these will update package.json and create a version commit to git automatically. If you execute npm publish after doing this, you should have something new out there.

Note that in the example above I've set up version related hooks to make sure a version will contain a fresh version of a distribution build. I also run tests just in case.

T> Consider using semantic-release if you prefer more structured approach. It can take some pain out of the release process while automating a part of it. For instance, it is able to detect possible breaking changes and generate changelogs.

Publishing a Prerelease Version

Sometimes, you might want to publish something preliminary for other people to test. There are certain conventions for this. You rarely see alpha releases at npm. beta and *rc (release candidate) are common, though. For example, a package might have versions like this:

  • v0.5.0-alpha1
  • v0.5.0-beta1
  • v0.5.0-beta2
  • v0.5.0-rc1
  • v0.5.0-rc2
  • v0.5.0

The initial alpha release will allow the users to try out the upcoming functionality and provide feedback. The beta releases can be considered more stable. The release candidates (rc) are close to an actual release and won't introduce any new functionality. They are all about refining the release till it's suitable for general consumption.

The workflow in this case is straight-forward:

  1. npm version 0.5.0-alpha1 - Update package.json as discussed earlier.
  2. npm publish --tag alpha1 - Publish the package under alpha1 tag.

In order to consume the test version, your users will have to use npm i <your package name>@alpha1.

T> It can be useful to utilize npm link during development. That will allow you to use a development version of your library from some other context. Node.js will resolve to the linked version unless local node_modules happens to contain a version. If you want to remove the link, use npm unlink.

On Naming Packages

Before starting to develop, it can be a good idea to spend a little bit of time on figuring out a good name for your package. It's not very fun to write a great package just to notice the name has been taken. A good name is easy to find through a search engine, and most importantly, is available at npm.

As of npm 2.7.0 it is possible to create scoped packages. They follow format @username/project-name. Simply follow that when naming your project.

Dealing with npm Distribution Files

It's a good practice not to include npm specific distribution files to the version control. Normally you .gitignore them. The files will be included to the version uploaded to npm, though. The benefit of doing this is that it keeps your version history neat.

This approach becomes problematic when you want to consume your package through Git instead of npm using "depName": "<github user>/<project>#<reference>". This is true especially if you are experimenting with something, or need to patch things to work for now.

One way to solve this is to set up a postinstall script that will generate a local npm version of your library in case it doesn't exist. This can be achieved through a postinstall script like this:

package.json

{
  ...
  "scripts": {
    ...
    "postinstall": "node lib/post_install.js"
  },
  "devDependencies": {
    ...
    /* You should install sync-exec through `npm i` to get a recent version */
    "sync-exec": "^0.6.2"
  }
}

In addition, we need to define a little script to do the work for us. It will check whether our package contains the directory we expect and will then act based on that. If it doesn't exist, we'll generate it:

lib/post_install.js

// adapted based on rackt/history (MIT)
// Node 0.10+
var execSync = require('child_process').execSync;
var stat = require('fs').stat;

// Node 0.10 check
if (!execSync) {
  execSync = require('sync-exec');
}

function exec(command) {
  execSync(command, {
    stdio: [0, 1, 2]
  });
}

stat('dist-modules', function(error, stat) {
  // Skip building on Travis
  if (process.env.TRAVIS) {
    return;
  }

  if (error || !stat.isDirectory()) {
    exec('npm i babel-cli babel-preset-es2015 babel-preset-react');
    exec('npm run dist-modules');
  }
});

You may need to tweak the script to fit your exact purposes, but it gives you the basic idea.

Respect the SemVer

Even though it is simple to publish new versions out there, it is important to respect the SemVer. Roughly, it states that you should not break backwards compatibility, given certain rules are met. For example, if your current version is 0.1.4 and you do a breaking change, you should bump to 0.2.0 and document the changes. You can understand SemVer much better by studying the online tool and how it behaves.

Version Ranges

npm supports multiple version ranges. I've listed the common ones below:

  • ~ - Tilde matches only patch versions. For example, ~1.2 would be equal to 1.2.x.
  • ^ - Caret is the default you get using --save or --save-dev. It matches to It matches minor versions. This means ^0.2.0 would be equal to 0.2.x.
  • * - Asterisk matches major releases. This is the most dangerous of the ranges. Using this recklessly can easily break your project in the future and I would advise against using it.
  • >= 1.3.0 < 2.0.0 - Range between versions. This can be particularly useful if you are using peerDependencies.

You can set the default range using npm config set save-prefix='^' in case you prefer something else than caret. Alternatively you can modify ~/.npmrc directly.

T> Sometimes, using version ranges can feel a little dangerous. What if some future version is broken? npm shrinkwrap allows you to fix your project versions and have stricter control over the versions you are using in a production environment.

Library Formats

I output my React component in various formats at my boilerplate. I generate a version that's convenient to consume from Node.js by processing my component code through Babel. That will convert ES6 and other goodies to a format which is possible to consume from vanilla Node.js. This allows the user to refer to some specific module within the whole if needed.

In addition, I generate so called distribution bundles: .js and .min.js. There's a sourcemap (.map) useful for debugging for both. It is possible to consume these bundles standalone as they come with an UMD wrapper.

UMD makes it possible to consume them from various environments including global, AMD, and CommonJS (Node.js format). You can refresh your memory with these by checking the Getting Started chapter for examples.

It is surprisingly easy to generate the aforementioned bundles using Webpack. The following example should give you the basic idea:

webpack.config.js

...

var config = {
  paths: {
    dist: '...',
    src: '...',
  },
  filename: 'demo',
  library: 'Demo'
};

var commonDist = {
  devtool: 'source-map',
  output: {
    path: config.paths.dist,
    libraryTarget: 'umd',
    library: config.library
  },
  entry: config.paths.src,
  externals: {
    react: 'react'
    /* more complicated mapping for lodash */
    /* we need to access it differently depending */
    /* on the environment */
    lodash: {
      commonjs: 'lodash',
      commonjs2: 'lodash',
      amd: '_',
      root: '_'
    }
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loaders: ['babel?cacheDirectory'],
        include: config.paths.src
      }
    ]
  }
};

if(TARGET === 'dist') {
  module.exports = merge(commonDist, {
    output: {
      filename: config.filename + '.js'
    },
  });
}

if(TARGET === 'dist-min') {
  module.exports = merge(commonDist, {
    output: {
      filename: config.filename + '.min.js'
    },
    plugins: [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false
        }
      })
    ]
  });
}

Most of the magic happens thanks to devtool and output declarations. In addition, I have set up externals as I want to avoid bundling React and lodash into my library. Instead, both will be loaded as external dependencies using the naming defined in the mapping.

T> The example uses the same merge utility we defined earlier on. You should check the boilerplate itself for the exact configuration.

T> If your library is using ES6 exclusively, rollup can be a valid, simple alternative to Webpack.

npm Lifecycle Hooks

npm provides various lifecycle hooks that can be useful. Suppose you are authoring a React component using Babel and some of its goodies. You could let the package.json main field point at the UMD version as generated above. This won't be ideal for those consuming the library through npm, though.

It is better to generate a ES5 compatible version of the package for npm consumers. This can be achieved using babel CLI tool:

babel ./lib --out-dir ./dist-modules

This will walk through the ./lib directory and output a processed file for each library it encounters to ./dist-modules.

Since we want to avoid having to run the command directly whenever we publish a new version, we can connect it to prepublish hook like this:

"scripts": {
  ...
  "prepublish": "babel ./lib --out-dir ./dist-modules"
}

Make sure you execute npm i babel --save-dev to include the tool into your project.

You probably don't want the directory content to end up in your Git repository. In order to avoid this and to keep your git status clean, consider this sort of .gitignore:

dist-modules/
...

Besides prepublish, npm provides a set of other hooks. The naming is always the same and follows the pattern pre<hook>, <hook>, post<hook> where <hook> can be publish, install, test, stop, start, restart, or version. Even though npm will trigger scripts bound to these automatically, you can trigger them explicitly through npm run for testing (i.e., npm run prepublish).

There are plenty of smaller tricks to learn for advanced usage. Those are better covered by the official documentation. Often all you need is just a prepublish script for build automation.

Keeping Dependencies Up to Date

An important part of maintaining npm packages is keeping their dependencies up to date. How to do this depends a lot on the maturity of your package. Ideally, you have a nice set of tests covering the functionality. If not, things can get a little hairier. There are a few ways to approach dependency updates:

  • You can update all dependencies at once and hope for the best. Tools, such as npm-check-updates, can do this for you.
  • Install the newest version of some specific dependency, e.g., npm i lodash@* --save. This is a more controlled way to approach the problem.
  • Patch version information by hand by modifying package.json directly.

It is important to remember that your dependencies may introduce backwards incompatible changes. It can be useful to remember how SemVer works and study dependency release notes. They might not always exist, so you may have to go through the project commit history. There are a few services that can help you to keep track of your project dependencies:

These services provide badges you can integrate into your project README.md. These services may email you about important changes. They can also point out possible security issues that have been fixed.

For testing your projects you can consider solutions, such as Travis CI or SauceLabs. Coveralls gives you code coverage information and a badge.

These services are valuable as they allow you to test your updates against a variety of platforms quickly. Something that might work on your system might not work in some specific configuration. You'll want to know about that as fast as possible to avoid introducing problems.

Sharing Authorship

As packages evolve, you may want to start developing with others. You could become the new maintainer of some project, or pass the torch to someone else. These things happen as packages evolve.

npm provides a few commands for these purposes. It's all behind npm owner namespace. More specifically, you'll find ls <package name>, add <user> <package name> and rm <user> <package name> there (i.e., npm owner ls). That's about it.

See npm documentation for the most up to date information about the topic.

Conclusion

You should have a basic idea on how to author npm libraries with the help of Webpack now. It takes a lot of effort out of the process. Just keep the basic rules in mind when developing and remember to respect the SemVer.