Publishing on npm is weird

Navigating the peculiarities of publishing on npm can be a wild ride.

This is a list of a few things I’ve learned while publishing my JavaScript shenanigans on npm. Or, more accurately, it's a list of things I’ve banged my head against while trying to publish on npm. Some of it is logical, some of it is inscrutable. All of it was necessary. I’m documenting it here so that next time I go to publish something I can avoid going down a hundred-open-tabs search-engine rabbit hole (again!).

The scenario

I really enjoy writing scripts in Node. I long ago accepted the fact that JavaScript is the language I’m most fluent in, and now I “lean in” to being able to write JS everywhere. Other people might use Bash or Python to solve their little problems but when I want to automate something or inspect some data, I turn to Node.js.

And when I want to share my work, the npm registry is the first place I think of. It’s great to be able to run yarn add my-thing (or npm install my thing if you prefer) and instantly be able to reuse my little scripts.

So what has consistently tripped me up? And what have I learned after publishing a few things?

Put it in the bin

If a script is to be run by an end user (i.e. they write npm run your-cool-script or yarn your-cool-script), you need to add that script to the bin section of your package.json

{
    "bin": {
	    "your-cool-script": "./path/to/the/script.js"
    }
}

Type = module

If you’re writing using ESM syntax (import foo, { bar } from “baz”, etc...), you need to declare your package as a “module” in your package.json

{
    "type": "module"
}

Shebang

If you want your script to run correctly on Unix-like systems (which MacOS and Linux both are) then the file should start with a “shebang”: #!/usr/bin/env node.

This is a comment that specifies which interpreter the system should use for the script, and uses the user’s env command to find the node interpreter in the system’s PATH. Note that bin is again being used as a place to store executable scripts, this time at a system level.

Shebang get’s its name from a concatenation of “sharp” (i.e. #) and “bang” (i.e. !) as interpreter comments all start with #!.

If you’re using a tool like esbuild to compile your script from multiple source files, then you’ll need to make use of the “banner” configuration option. Setting a “banner” is a way to inject arbitrary text at the start of your final built file.

const esbuildConfig = {
    // ...
    banner: {
        js: "#!/usr/bin/env node"
    }
};

esbuild’s format and platform

Speaking of esbuild, there are a couple of other configuration options that you should set: format and platform.

You need to set format to be “esm” because even now at the end of 2023 ESM is still not the default. CommonJS just will not die, and it will never cease to confound me as to why.

Setting platform to “node” makes more sense, however, as the esbuild is primarily a tool for building things for the web. By setting this config value we’re telling it not to worry too much about built-in Node packages like path and fs (which it would freak out about if we tried to compile those for the web).

const esbuildConfig = {
    // ...
    format: "esm",
    platform: "node",
    banner: {
        js: "#!/usr/bin/env node"
    }
};

Let the npm CLI do the work

Versioning is an important part of reliably releasing your code, especially if you’re like me and constantly George Lucas-ing your creations. SemVer (a.k.a. semantic versioning) is our friend here, so stick to the {major}.{minor}.{patch} format.

I very quickly learned to stop manually tweaking the version value in my package.json. It’s much more straightforward to run npm version {major|minor|patch}. This takes care of bumping the package version and cutting a new “tag”. If these get out of sync, then you’ll have problems when pushing the code to npm, so the CLI is a much more reliable way to handle this process. Just remember to git push --tags to ensure the new tag gets pushed to your repo.

Note: Watch out if you’re in a sub-folder of a monorepo, npm version won’t do the tagging and commit for you!

Always start at zero

As an aside to versioning, I find it helpful to always start with a “major” version of 0 (so commit #1 is usually version 0.1.0). When in the early stages of a new project I’m frequently adding “breaking changes” as I find my feet, so if I struck strictly to SemVer and began at v1.0.0 I’d be on version three hundred before my code ever saw the light of day.

The pre-version-one freedom to play fast-and-loose with backwards compatibility is essential, in my opinion.

Go forth and publish

This is not a comprehensive list of All The Things You Need To Know To Publish On npm™️, but everything include here is something I’ve had to learn through trial and error. I’ll hit more obstacles, I’m sure, and when I do I might remember to add them to this page (maybe). But at least I’ve got these ones out of my working memory and into long term storage. And is that not the main purpose of technical blogging after all?



Signup to my newsletter

Join the dozens (dozens!) of people who get my writing delivered directly to their inbox. You'll also hear news about my miscellaneous other projects, some of which never get mentioned on this site.