Deploy: Publishing a package
Publishing an package is a kind of career milestone for a JavaScript developer: it assumes that you reached a level of maturity and code quality that makes you pretend to bring some added value to others.
Basically, this consists in uploading your project code to the NPM public repository so that others can reference it as their own project (dev) dependencies
. This is something that is natively supported by the npm
CLI tool, through this command:
npm publish
But wait. Is that all? No. Before, you need to:
- set up access
- define your exports
- define your executable (if any)
- define your TypeScript types (if any)
- setup your build scripts
Access
Public registry
First of all, you need to be registered as a NPM user before publishing packages as an author. Once your NPM account is created, you need to npm login
to authenticate yourself as a package author.
Project
Secondly, you need to allow your project to be published, which is not the case by default. To do so, edit the publishConfig
property of your package.json
. For instance:
{
"name": "@yourname/yourpackage",
"publishConfig": {
"access": "public"
}
}
Exports
Another important step is to declare what will be publicly visible from your package.
Barrel files
You may declare a full set of export
mappings in your package.json
:
{
"exports": "./dist/index.js"
}
However a more simple (but controversed) option is to just declare a main file that might contain its own exports:
{
"main": "index.js"
}
The idea behind this file pattern is that all directory default files (index.js
) exports every public item that this directory contains, and so on recursively. For instance, a rootindex.js
may contain:
export * from "./MyAPI.js"
export * from "./MyOtherFile.js"
Then, should you have a sub
directory with other things to export, you’d add export * from "./sub/index.js"
to this main export file, then declare the own export of that sub-dir in the sub/index.js
file:
export * from "./SubFile.js"
export * from "./subsub/index.js
This way your declarations are really modular (i.e. the children directories do not know about the parent one, and vice versa).
This is a very convenient file pattern for developers, but not the most optimal one, however: importing such indexes implies loading all types declared in it. At worse, importing your “@yourusername/yourpackage” package will load every types in it, even if you use only one.
To avoid this, you should import specific files directly.
TypeScript
For some reason the transpilation of TS exports (in barrel files, typically) doesn’t specify file extension, and so your generated JS files will fail at runtime in importing such directories without files, or files without a .js
extension.
export * from "./SomeType" // .ts extension assumed
export * from "./subdir" // subdir/index.ts assumed
To workaround this, you should add a .js
extension in your TS sources imports, which is silently torrelated by TypeScript:
export * from "./SomeType.js" // Works even if the file is .ts
export * from "./subdir/index.js" // Explicit import of index.ts
Executable
Should you package being a CLI tool, it’s usually convenient to allow to run it just as any other regular shell command.
To do so, you need to provide an implementation of your command, with a “sha-bang” referencing your Node install, so that the shell will know which executable will be able to interpret your JavaScript command. For instance in a src/cli/index.js
source file:
#!/usr/bin/env node
import { MyTool } from '../MyTool.js'
const param = process.argv[2]
new MyTool(param).execute()
then declare this code as a bin
ary command in your package.json
:
"bin": {
"mytool": "cli/index.js"
},
Note that the path is expected from the built root. Also note you can describe multiple commands. In case of TypeScript, you can even choose to execute TypeScript code directly, provided you require a TypeScript interpreter like tsx
to be installed:
#!/usr/bin/env tsx
// TypeScript CLI tool code...
This will make sure the installation process of your package will add an additional shortcut, you users will be able to run mytool
with args directly from the command line.
Scripts
You can add in your package.json
:
"scripts": {
"prebuild": "npm install",
"build": "rm -Rf dist && cp -R src dist && cp README.md dist && cp package*.json dist",
"test": "node --test",
"prepublishOnly": "npm test",
"publish": "npm run build && npm publish dist"
},
Types
It’s always a good idea to publish your package along with types declarations to make your users life easier.
Those can be generated by the TypeScript compiler (tsc
) if you ask it in your tsconfig.json:
{
"compilerOptions": {
"declaration": true
}
}
Then declare their root in your package.json
:
"types": "./dist/index.d.ts",
JavaScript
Even when not using TypeScript, to can declare many typing hints through JSDoc comments that will help both your IDE and the TypeScript compiler to generate typing files for your users.
To do so, just add a tsconfig.json
file as if you were writing typescript, but allowJs
and ask to emitDeclarationOnly
:
{
"include": ["src/**/*"],
"compilerOptions": {
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "dist",
"declarationMap": true,
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "nodenext"
}
}
then run tsc
.
Publish process
As said in the beginning of this article, publishing goes with a npm publish
after login. At this time, the tool reads your package.json
for a possible prepublish
script to execute, which is a good opportunity to trigger a quality chain (publish implies build which implies install and test).
{
"scripts": {
"prebuild": "npm install",
"build": "npm test && rm -Rf dist && cp -R src dist && tsc && cp README.md dist && cp package*.json dist",
"test": "node --test src/**/*.test.js src/*.test.js",
"publish": "npm run build && cd dist && npm publish && cd .."
}
}
.npmignore
It’s also a good idea to think twice about what is really needed in your published package: you may not want to add a number of things in your published package to add unnecessary weight to your package:
- confidential info (should already be in
.gitignore
) - test code
- Linters config
- non-JS original source files (
.ts
files for instance, but dont exclude typing.d.ts
ones)
Use .npmignore
for that. For instance:
.idea/**/*
.circleci/*
jest*.*
*.test.*
**/*Test.*
test/**/*
src/**/*
dist/test/**/*