NPM Package Template
You wrote a sweet piece of code! Releasing it on NPM seems like the obvious next step. Right?
Try it! Not as easy to do as you might think. At high quality. From scratch.
So here’s a plug-and-play NPM package template that offers the following features:
-
Tree-shakable support for the latest ES6 goodies with
eslint
uber alles. -
CJS distributions targeting specific browser support scenarios.
-
Command line interfaces for your widget with
commander
. -
Automated
lodash
cherry-picking withbabel-plugin-lodash
. -
mocha
&chai
for testing, with examples, and a sweet testing console. -
Code formatting at every save & paste with
prettier
. -
Automated documentation of your API with
jsdoc-to-markdown
and assembly of your README withconcat-md
. -
One-button release to GitHub & publish to NPM with
release-it
.
See it on GitHub! Clone the Repo!
If you want to create a React component in an NPM package, try my React Component NPM Package Template instead!
Setting Up Your Dev Environment
Use VS Code as your code editor! Not an absolute requirement, but you’ll be glad you did.
-
Click here to generate a new repository from this template.
-
Clone the new repository to your local machine.
-
VS Code will ask to install a bunch of recommended extensions. Accept all of them. If you miss this prompt, follow these steps:
- Open the VS Code Extensions tab
- Enter
@recommended
into the search box -
Click the Download link.
-
Zero the package version and install dependencies by running these commands:
npm version 0.0.0 npm install
This may produce an audit report. See Vulnerabilities below for more info.
-
Run your tests from the command line:
npm run test # foo # ✔ with input # ✔ without input # # 2 passing (5ms)
If you installed the VS Code extensions referenced above, use the
Testing
panel to visualize & run your unit tests. -
Package your code and link it locally by running:
npm run package npm link
-
Enter a few of your package CLI commands:
mycli # foo nil! mycli -f bar # foo bar! mycli -v # 0.0.0
-
Clean up by unlinking your package.
npm unlink -g @karmaniverous/npm-package-template
Create Local Environment Variable File
Look for
.env.local.template
in your project directory. Copy this file and remove the .template
extension
from the copy.
Do not simply rename this file! Anybody who pulls your repo will need this template to create the same file in his own local environment.
Connect to GitHub
This template supports automated release management with
release-it
.
If you use GitHub, create a
Personal Access Token
(release-it
only needs “repo” access; no “admin” or other scopes). Add it as
the value of GITHUB_TOKEN
in .env.local
.
If you use GitLab, follow these instructions and place your token in the same file.
For other release control systems, consult the
release-it
README.
You can now create a release at GitHub and optionally publish it to NPM with this command:
npm run release
Vulnerabilities
At the time of this writing, running npm install
will generate the following
vulnerability warning:
6 vulnerabilities (3 high, 3 critical)
If you run npm audit
, you will find that all of these vulnerabilities relate
to the following dev dependencies, all of which are to do with docs generation:
npm list underscore
# @karmaniverous/npm-package-template@0.5.1-0
# ├─┬ concat-md@0.5.0
# │ └─┬ doctoc@1.4.0
# │ └── underscore@1.8.3
# └─┬ jsdoc-to-markdown@8.0.0
# └─┬ jsdoc-api@8.0.0
# └─┬ jsdoc@4.0.0
# └── underscore@1.13.6
npm list trim
# @karmaniverous/npm-package-template@0.5.1-0
# └─┬ concat-md@0.5.0
# └─┬ doctoc@1.4.0
# └─┬ @textlint/markdown-to-ast@6.0.9
# └─┬ remark-parse@5.0.0
# └── trim@0.0.1
These vulnerable dependencies will NOT be included in your NPM package!
Package Structure
This template produces a hybrid NPM package that features both native ES and platform-specific CommonJS module entry points as well as a native ES CLI.
The template supports tree-shakable import
from its native ES library entry
point at at
lib/index.js
.
This is where you write your source code! See
import
Entry Points for more info on
developing to this entry point and adding new ones.
The template supports require
from its platform-specific CommonJS library
entry point configured at
dist/default
.
This code is generated by Babel and not pushed to the repository, so you
won’t see it here. See
require
Entry Points for more
info on configuring this entry point and adding new ones.
The template features a native ES command-line interface (CLI) entry point at
bin/mycli/index.js
.
This is where you write your CLI! See
bin
Entry Points below for more info on
configuring this entry point and adding new ones.
After you’ve set up your dev environment,
your package will look like this. The git
& npm
columns indicate which files
participate in git push
and npm publish
, respectively.
git npm Item Description
---------------------------------------------------------------------------
└─ npm-package-template ....... package root
x ....... ├─ .babelrc ................ global Babel config
x . x ... ├─ .env .................... global env variables
......... ├─ .env.local .............. create from template
x . x ... ├─ .env.local.template ..... local env secrets template
x ....... ├─ .eslintrc.json .......... global ESLint config
x ....... ├─ .gitignore .............. package git ignore
x ....... ├─ .npmignore .............. package npm ignore
x ....... ├─ .vscode ................. VS Code project config
x ....... │ ├─ extensions.json ...... recommended extensions
x ....... │ └─ settings.json ........ project settings
x . x ... ├─ bin ..................... all CLI source code (ES)
x . x ... │ └─ mycli ................ mycli code (ES)
x . x ... │ └─ index.js .......... mycli entry point (ES)
x . x ... ├─ dist .................... all CommonJS distributions
x . x ... │ ├─ default .............. specific CommonJS distribution
x ....... │ │ ├─ .babelrc .......... Babel target settings
x ....... │ │ ├─ .gitignore ........ don't send the lib to git...
x ....... │ │ ├─ .npmignore ........ but DO send it to NPM
x . x ... │ │ └─ lib/ .............. dist code (generated by Babel)
x . x ... │ └─ package.json ......... dist code is CommonJS
x ....... ├─ doc ..................... all documentation config
x ....... │ ├─ .prettierignore ...... ignore handlebars formatting
x ....... │ ├─ 1-main.md ............ your main README content
x ....... │ ├─ 2-cli.md ............. your CLI documentation
x ....... │ ├─ 3-api.md ............. api docs (generated by jsdoc)
x ....... │ ├─ 4-footer.md .......... your README footer
x ....... │ ├─ api-template.hbs ..... your API docs template
x ....... │ └─ jsdoc.config.json .... ignore test code
x . x ... ├─ lib ..................... your library source code (ES)
x . x ... │ ├─ foo .................. structure your code how you like
x . x ... │ │ ├─ foo.js ............ source code module (ES)
x ....... │ │ └─ foo.test.js ....... test script (ES)
x . x ... │ ├─ index.js ............. library import entry point (ES)
x ....... ├─ package-lock.json ....... dependencies (managed by npm)
x . x ... ├─ package.json ............ package config
x . x ... └─ README.md ............... generated by jsdoc
NPM Scripts
Script | Description |
---|---|
npm run lint |
Executes eslint to find errors in your code. |
npm run test |
Executes mocha to run all unit tests. |
npm run build |
Executes babel to build all CJS distributions. |
npm run doc |
Executes jsdoc2md to build your README file from the doc directory. |
npm run package |
Runs lint , test , build & doc to exercise your full packaging process. |
npm run release |
Runs package & executes release-it to create a GitHub release & publish your code to NPM. |
Package Entry Points
import
Entry Points
All custom library code lives in the
lib
directory. This is native ES code and you can structure it however you like.
Package import
entry points are defined in
package.json
like this:
{
"exports": {
".": {
"import": "./lib/index.js"
}
}
}
There is currently a single import
entry point defined at the package root,
which points to
lib/index.js
.
The entry point will expose whatever this module exports, and can be referenced
in an ES module like this:
import { foo, PACKAGE_INFO } from '@karmaniverous/npm-package-template`;
Some key guidelines:
- Avoid using module default exports, even internally. This can create unexpected weirdness in the Babel-generated CJS distributions. Use named exports instead.
See the Node Package Entry Points documentation for more info on defining additional entry points.
Do not move or rename
lib/index.js
without making matching changes in
package.json
,
or your ES exports will not work!
require
Entry Points
The CJS distributions behind your require
entry points are generated by Babel
from your ES source code in
lib
.
Each distribution is optimized for a set of browser constraints as expressed by
a browserslist query.
The template’s default distribution is located at
dist/default
and uses
.babelrc
to set browserslist
query "defaults"
.
Package require
entry points are defined in
package.json
like this:
{
"exports": {
".": {
"require": "./dist/default/lib/index.js"
}
}
}
There is currently a single require
entry point defined at the package root,
which points to dist/default/lib/index.js
. The entry point will expose
whatever this module exports, and can be referenced in CJS code like this:
const { foo, PACKAGE_INFO } = require('@karmaniverous/npm-package-template`);
See the Node Package Entry Points documentation for more info on defining additional entry points.
To create a new CJS distribution:
-
Duplicate the
default
directory indist
and give the copy a meaningful name (e.g.mydist
). -
In the new directory’s
.babelrc
file, change thetargets
value to reflect this distribution’sbrowserslist
query:{ "targets": "new query" }
-
Add a new
require
entry point and a newbuild
script step to your project’spackage.json
:{ "exports": { "./mydist": { "require": "./dist/mydist/lib/index.js" } }, "scripts": { "build": "babel lib -d dist/default/lib --delete-dir-on-start --config-file ./dist/default/.babelrc && babel lib -d dist/mydist/lib --delete-dir-on-start --config-file ./dist/mydist/.babelrc" } }
-
Build the new distribution with
npm run build
.
Do not move or rename
dist/default.js
without making matching changes in
package.json
,
or your CJS exports will not work!
bin
Entry Points
Each command-line interface (CLI) entry point is a subdirectory of your
bin
directory. These are native ES code and you can structure them internally
however you like.
CLI entry points are defined in
package.json
like this:
{
"bin": {
"mycli": "bin/mycli/index.js"
}
}
There is currently a single CLI entry point defined at the package root, which
points to
bin/mycli/index.js
and exposes the mycli
command.
See the NPM documentaion documentation for more info on defining CLI entry points.
To create a new CLI entry point:
-
Create a new subdirectory under
bin
(e.g.myothercli
). -
Add an
index.js
file expressing your command logic, along with other supporting code. Usecommander
for great results! -
Add the new CLI entry point to your
package.json
like this:
{
"bin": {
"myothercli": "bin/myothercli/index.js"
}
}
Do not move or rename
bin/mycli/index.js
without making matching changes in
package.json
,
or your CLI will not work!
Unit Testing
By default, this template supports
mocha
tests using the
chai
assertion library. The included
sample tests express the should
assertion syntax.
The default configuration will recognize any file as a test file that…
- has
.test.
just before its file name extension (i.e.example.test.js
). - is not located in the
node_modules
ordist
directories.
The sample code packages tests next to the source code they exercise. If you
prefer to segregate your tests and related artifacts into a test
directory,
that will work as well.
Either way, all test files meeting the above conditions and anything under a
test
directory will be excluded from the build.
To enable mocha
-specific linting in your test files, add the following
directive at the top of every test file:
/* eslint-env mocha */
The recommended
Mocha Test Explorer
extension will suface all of your tests into a sidebar console, nested to
reflect your describe
hierarchy. It will also decorate your test source code
with test running and status reporting controls.
Package Documentation
When you run npm run doc
, these two steps happen in order:
-
jsdoc2md
parses the source code inlib
for JSDoc-formatted comments, and generates API documenation indoc/3-api.md
usingdoc/api-template.hbs
as a template. It ignores test files and anything with the@private
tag. -
concat-md
concatenates every.md
file indoc
into your mainREADME
file. Files are concatenated in filename order (hence the numbered files).
You can exert fine control over the final result by doing the following:
-
Edit
1-main.md
. This is the main content of your README file. At a minimum it should contain a header and a brief description of the project, but it can be whatever you want. -
Edit or delete
2-cli.md
. This is your CLI documentation. The easiest way to get this is to run your CLI’s help function on the command line & copy the result (e.g.mycli -h
). If your package has no CLI, just delete this file. -
Edit
api-template.hbs
. This is your API documentation template.jsdoc2md
will render documentation based on the JSDoc-formatted comments in your source code and render them at the{{>main}}
tag in the template. Add any other content in Markdown format. -
Edit
4-footer.md
. This footer will appear at the bottom of yourREADME
file.
Some tips:
-
Add whatever additional
.md
files you like! Just remember they will be rendered in filename order. -
If your main content is really long, consider linking to a blog post instead (like this one!). That way you can update your documentation without having to create a new release.
-
Use
commander
to create your CLI and take full advantage of its help system (e.g. program & option descriptions). This gives you a more usable CLI, and as a side bonus you just need to runmycli -h
to copy & paste your full CLI documentation.> mycli -h Usage: mycli [options] Foos your bar. Options: -b, --bar <string> foo what? -v, --version display package version -h, --help display help for command
-
Take full advantage of JSDoc by leveraging
@typedef
syntax (an example) and importing types from a common file (another example).
Don’t edit your
README
file directly! Any changes you make will be lost the next time you run
npm run doc
.
Integration Testing
Your unit tests exercise your code internally. Integration testing validates your code externally, and might include the following:
-
Testing your
import
entry points from external code. -
Testing your
require
entry points from external code. -
Testing your CLI entry points from the command line.
There are several ways to accomplish these:
-
Run
npm run link
to install a global symlink to your local package. This has an effect similar tonpm install -g my-package
, and will enable you to test your CLI entry points from your local command line without publishing to NPM. -
Run
npm install c:/package-path/my-package
to install your package as a file dependency in another package. This will enable you to test yourimport
andrequire
entry points without publishing to NPM. Run a global install or usenpx mycli
to test your CLI entry points. -
Publish a release to NPM with a pre-release version number. Then run
npm install my-package 0.0.1-0
(reflecting the pre-release version) to install your package into another package as a dependency, and test as required.
Each of these approaches has its trade-offs, but a smart plan is to skip the first and do the last two, in that order.
If the second approach passes, why bother with the third?
Because your local project contains all your files, whereas your package
.npmignore
files determine which ones actually get published to NPM. That last
approach validates that your published package has the expected contents.
As a final check, review your package’s Code tab at NPM in order to validate
that your .npmignore
files blocked everything they should have (e.g.
environment secrets, dev tool configs).
Create & Publish a Release
Before you can publish a package to NPM, you’ll need to set up an NPM account.
Package Scope & Access
Your NPM user name is a scope. If you create an organization, its unique organization name is also a scope.
Unscoped packages have names like
lodash
. An unscoped package name must
be unique across NPM.
Scoped packages have names like
@karmaniverous/serify-deserify
.
@karmaniverous
in this case is the scope. A scoped package name only needs to
be unique within its scope.
NPM packages may be public or private. A public package can be seen and used by anyone. A private package can only be seen & used by your collaborators or other users with access to your organization scope.
Only scoped packages can be private. Only paid NPM accounts can create private packages.
Even if you are only creating public packages, it is a good idea to create scoped packages because it groups them logically and gives you much more flexibility in naming them.
Click here for more info about NPM package scope & access.
Configuring package.json
When you publish an NPM package, NPM gets most of its info from your
package.json
file.
Set the following values in
package.json
,
using the template file as an example.
This info is critical. You can’t publish your package properly without it:
-
name
– The desired package name on NPM. Include scope if relevant. See Package Scope & Access for more info. -
version
– Your package version. Uses semantic versioning. Set this initially to0.0.0
and the template’s release process will manage it from there. -
publishConfig.access
–restricted
for private packages, otherwisepublic
. See Package Scope & Access for more info. -
exports
– Yourimport
andrequire
entry points. Click the links for more info. -
bin
– Your CLI entry points. Seebin
Entry Points above for more info.
This info is important but you can always update it in the next release:
-
author
– Your name, however you’d like it to appear. -
bugs.url
– A URL for users to report bugs. By default, use the issues page of your GitHub repo. -
description
– A text description of your package. Will be used as the META description of your NPM package page, so keep it under 160 chars. -
homepage
– The main web page of your project. By default, use your GitHub repo’s README link. -
keywords
– An array of strings that will appear as tags on the NPM package page. -
license
– The license associated with your package. See this list of valid license identifiers. -
repository.url
– GitHub repository URL.
Generating the Release
Before you begin, ensure you have committed all changes to your working branch.
Run this command:
npm run package
This will check your code for errors, run all of your tests, generate all of your documentation, and create your build. If there are any issues, fix them. If you make any changes, commit them.
Now run this command:
npm run release
This will generate your package again, just to validate there are no more changes. You will then be asked to select a release increment. Otherwise accept all defaults.
Your release will be generated on GitHub and published to NPM.
Note that if you have configured Two-Factor Authentication at NPM you will be asked to enter a One-Time Password (OTP).
Add other release-it
options after ---
(Windows) or --
(Mac/Linux). For example, to specify a
patch release, and accept all defaults with no user interaction, run this
command:
# Windows only.
npm run release --- patch --ci
# Mac/Linux.
npm run release -- patch --ci
See the release-it
README
for more info on available options.
Integrate a Template Update
Follow these instructions.
Issues
- The documentation system is choking on dynamic import assertions for for now I’ve added an exclusion to the one directory that uses them (packageInfo).
Leave a comment