Deploy: Publishing a package
Publishing a package for anybody to use is a kind of milestone in the career of a developer. It means that you think you have some added value to share, with a decent quality level.
Access
First things first, you need to specify in your package.json
that it is aimed for public access using the publishConfig
property. For instance:
{
"name": "@yourname/yourpackage",
"publishConfig": {
"access": "public"
}
}
Exports
Another important step is to declare what is exported from your package, that is, what will be visible by your users. You may declare a full set of export
mappings, but a more simple option is to just declare your main file:
"main": "index.js"
Barrel files
The idea behind this file pattern is that a directory default (index.js
) file exports everything from that directory, including sub-directories, which have their own barrel files, etc. For instance the 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.
To workaround this, you should add a “.js” extension in your TS sources imports, which is silently torelated by 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.
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": "./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
Publishing a package goes with npm publish
, provided you successfuly ran npm login
before (on the npm public registry by default, but you can specify another one).
It then 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).
It’s also a good idea to think twice about what is really needed in your published package: you may not want to add confidential info (should already be in .gitignore
), test code or Linters config to add unnecessary weight to your package. Use .npmignore
for that.
{
"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 .."
}
}