Deploy: Publishing a package

Jérôme Beau
4 min readJun 11, 2024

--

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 binary 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 .."
}
}

--

--