Deploy: Publishing a package

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

--

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 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. 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/**/*

--

--

Jérôme Beau
Jérôme Beau

Written by Jérôme Beau

Sharing learnings from three decades of software development. https://javarome.com

No responses yet