Best practices for publishing your npm package

How to master the art of npm packaging

You want to publish your brand new javascript library using npmjs ? In this article I will give you some valuable tips on how to build the perfect, slick and efficient package.

Firstly, why do you want to publish to npmjs ? You should read this : become a better developer.

Ok are you ready ?

Publish

$ npm publish

There are many articles explaining how to start publishing your npm package. So I will not detail it here.

You should read npmjs documentation at first :

Npm Publish

Once your are confident with publishing process, some points need to be validated :

To check these points and others I recommend you to use np a tool that will help you follow some good practices.

But it is not enough. As recent version of Node.js and browsers are compatibles with ESM (Import) and ES6+ syntax, you need a publishing strategy to target your audience.

Modern javascript : targeting ESM but stay compatible with CommonJS

This strategy will help you develop and publish using modern javascript with no boilerplate and maintain a retro compatibility with older javascript engines.

You should read this article to understand how it works : Hybrid npm packages.

I mainly use Rollup bundler to build a legacy package with support for ESM and CommonJS. Rollup will create two code bases:

Your package.json contains two entries:

json
{ "main": "index.js", "module": "index.mjs", }

A minimal file structure

A package should contain at least :

- index.js
- package.json
- README
- LICENSE

package.json is the only mandatory file, as it describes your package. But without index.js it is useless. You should also add some information with a README (README.md) file and a LICENSE file.

I recommend this structure for modern javascript bundles as we seen previously:

- index.js
- index.js.map
- index.mjs
- index.mjs.map
- package.json
- README.md
- CHANGELOG.md
- LICENSE

*.map files are useful for debugging purposes.

Package.json: some unnecessary items

package.json is used mainly in 2 stages:

But items used for development like scripts, devDependencies stay present in the published bundle.

Note: some scripts could be executed during installation/uninstallation steps. It could give you some extra control on how your library is used.. Like calling a webservice to count how many packages are really installed. See : Npm scripts

Package your library

You don't need to publish all stuff from your project. Some files or directories should be excluded. You have 3 methods to do so.

Keeping files out of your package

1 - Using .npmignore or .gitignore

You exclude files using patterns from the bundle.

2 - Using files field in package.json

It works the opposite of #1, files contains an array of file patterns.

Note : in CommonJS package spec, you should detail how the struct of your package is using a 'directories' object. But I don't use it anymore.

Npm package.json files

3 - Using a dist folder

You need to copy all necessary files to a dist folder, and then add this folder to npm publish command:

$ npm build ./dist
$ cp ./README.md ./dist/README.md
$ cp ./package.json./dist/package.json
$ ...
$ npm publish ./dist

By doing so you have a better control on what to publish. dist folder is only dedicated to publishing (and building) so you could make some changes, reordering on included files. For example, you could change package.json without corrupting project's one, see next.

Use Packito to clean your package before publishing it

I created this tool to go further in packaging npm module. It is a superset of previous step 3. In a dist folder, it will copy mandatory and selected files, but also refactor package.json to remove/change some fields.

Packito repository

So Packito will help you:

Here is a sample .packito.json.

json
{ "remove": { "devDependencies": "*", "scripts": "*", "type": true, "esm": true, "husky": true, "commitlint": true }, "replace": { "main": "index.js", "module": "index.mjs" }, "publisher": { "name": "yarn test" }, "output": "./dist", "copy": ["bin", "README.md", "LICENSE"] }

Here, I use esm module for dev, test and coverage. I also setup husky and commitlint. So all references to these tools in dist/packages.json are useless for publishing so they will be removed. Also I use different paths for main and module fields, as index.* files are not present at the root during development stage, instead of publishing stage.

Package.json extracted from packito :

json
{ "name": "packito", "version": "0.4.0", "description": "clean your package before publishing it !", "main": "dist/index.js", "module": "dist/index.mjs", "repository": "https://github.com/mikbry/packito.git", "bugs": "https://github.com/mikbry/packito/issues", "homepage": "https://github.com/mikbry/packito", "author": "Mik <mik@miklabs.com>", "license": "MIT", "scripts": { "build": "rollup -c && ./bin/packito.js", "dev": "rollup -c && cross-env NODE_ENV=development node ./dist", "lint": "$(yarn bin)/eslint src", "test": "cross-env NODE_ENV=test $(yarn bin)/mocha --require esm", "coverage": "cross-env NODE_ENV=test $(yarn bin)/nyc _mocha", "report-coverage": "$(yarn bin)/nyc report --reporter=text-lcov > coverage.lcov", "prepublishOnly": "yarn build" }, "bin": { "packito": "./bin/packito.js" }, "engines": { "node": ">=10" }, "dependencies": { "chalk": "^3.0.0", "minimist": "^1.2.0", "node-emoji": "^1.10.0" }, "devDependencies": { "@commitlint/cli": "^8.2.0", "@commitlint/config-conventional": "^8.2.0", "@rollup/plugin-json": "^4.0.0", "@rollup/plugin-node-resolve": "^6.0.0", "chai": "^4.2.0", "cross-env": "^6.0.3", "eslint": "^6.7.2", "eslint-config-airbnb-base": "^14.0.0", "eslint-config-prettier": "^6.7.0", "eslint-plugin-import": "^2.19.1", "eslint-plugin-jest": "^23.1.1", "eslint-plugin-prettier": "^3.1.1", "esm": "^3.2.25", "husky": "^3.1.0", "mocha": "^6.2.2", "nodemon": "^2.0.1", "nyc": "^14.1.1", "prettier": "^1.19.1", "rimraf": "^3.0.0", "rollup": "^1.27.9" }, "husky": { "hooks": { "pre-commit": "yarn lint", "commit-msg": "[[ -n $HUSKY_BYPASS ]] || commitlint -E HUSKY_GIT_PARAMS" }, "commitlint": { "extends": [ "@commitlint/config-conventional" ] } } }

You have a 65 lines json...

Generated package.json in ./dist

json
{ "name": "packito", "version": "0.4.0", "description": "clean your package before publishing it !", "main": "index.js", "module": "index.mjs", "repository": "https://github.com/mikbry/packito.git", "bugs": "https://github.com/mikbry/packito/issues", "homepage": "https://github.com/mikbry/packito", "author": "Mik <mik@miklabs.com>", "license": "MIT", "bin": { "packito": "./bin/packito.js" }, "engines": { "node": ">=10" }, "dependencies": { "chalk": "^3.0.0", "minimist": "^1.2.0", "node-emoji": "^1.10.0" } }

Now you get 23 lines !

And package structure is optimized to :

- index.js
- index.js.map
- index.mjs
- index.mjs.map
- package.json
- README.md
- LICENSE

Distributing a polished and slick package is a respectful act. It is like β€˜the cherry on the cake’. All pieces are well developed, tested, packaged and at the right place. Nothing is superfluous. And your code is much more clear for other team members working on it. You are a master-chief !

Don't hesitate to help me enhance Packito, star it and give me some feedback.

Star

Thanks for reading !