I figured out how to build usable library packages. Now I just have to work out how to get them published.

react-virtual-scroll published on NPM
×
react-virtual-scroll published on NPM

I want to make it as easy as possible for others to find and consume my packages. As with package build, I want the process to be lightweight and easy to maintain.

Public Repositories

The first step is deciding where to publish. NPM is the obvious choice. It’s the original and most popular registry for JavaScript packages. All the JavaScript package managers install from NPM by default. Publishing public packages is free.

Nothing is ever completely obvious. My project is hosted on GitHub. GitHub has its own integrated package feature. What’s more, NPM is owned by GitHub. Maybe I can publish via GitHub, using my existing credentials, and it will be accessible via NPM?

That’s not how it works. GitHub Packages is an independent package registry. Using it requires extra configuration in your package manager. Even worse, you need additional configuration in your package manager for each user or organization you want to consume packages from.

Why would anyone publish to GitHub packages if it adds all this extra friction for your users? The wisdom of the internet says that GitHub only makes sense for private packages. Enterprise users are used to all the additional crap that comes with working for a large enterprise. If you’re a small business, GitHub has a free tier for private packages, while NPM requires a monthly subscription.

NPM Scopes

You may have noticed in my last two posts that the sample app imports the react-virtual-scroll package from "@candidstartup/react-virtual-scroll". The “@candidstartup” part is an npm scope.

You can publish unscoped or scoped packages. Scoped packages are the natural choice for anything new. Scopes act as a namespace. I can use whatever name I like within a scope without worrying about clashing with other packages. Every conceivable name for a react library of virtual scrolling components is already taken at the global level, so I’d have to use a very contrived name if I was publishing an unscoped package.

There’s no extra config to consume scoped packages. Just include the scope in the name when importing or declaring a dependency.

Finally, scopes improve security. They reduce the risk of npm substitution attacks as no one else can publish to your scope.

Creating an NPM account

Before going any further I need to create an npm account and secure my “candidstartup” scope. Scopes follow the same naming rules as npm packages so no funny symbols or capital letters. Scopes are tied to npm organizations so I need to create one of those too.

Creating a user account was simple enough. Two-factor authentication was strongly recommended, so I turned that on. I already use Google Authenticator for other accounts and it’s supported by NPM.

Once you have a user account, you can create an organization. The organization needs to have the same name as the scope you want to use.

Package Metadata

Scope secured, I was ready to try publishing a package. Fortunately, I decided to have a look at some other NPM packages first. The UI displays lots of useful metadata, most of which I didn’t have in my package.json.

Here’s what I ended up adding

  "description": "Modern React components for lists and grids that scale to trillions of rows and columns",
  "author": "Tim Wiegand",
  "license": "BSD-3-Clause",
  "repository": {
    "type": "git",
    "url": "https://github.com/TheCandidStartup/infinisheet.git",
    "directory": "packages/react-virtual-scroll"
  },
  "bugs": "https://github.com/TheCandidStartup/infinisheet/issues",
  "homepage": "https://github.com/TheCandidStartup/infinisheet/blob/main/packages/react-virtual-scroll/README.md",
  "keywords": [
    "react",
    "reactjs",
    "virtual",
    "window",
    "windowed",
    "list",
    "scroll",
    "scrolling",
    "infinite",
    "virtualized",
    "table",
    "grid",
    "modern",
    "scalable"
  ],
  "types": "./dist/index.d.ts"

The repository properties are used to populate the link to GitHub. The directory property is there to support monorepo setups. Unfortunately, it doesn’t get used by the NPM UI, so the repository link goes to the root of the repo. There’s a separate “Homepage” link (which defaults to the same location as repository) which I pointed to the package README using the homepage property.

The types badge in the NPM UI requires the types property to be populated even though TypeScript doesn’t need it.

Conventional Commits

More prep that I didn’t know I needed. It’s customary to have a CHANGELOG.md in your repo that describes what changed in each release of your package. That’s a pain to maintain.

What if you didn’t have to?

Conventional Commits is a specification for adding additional structure to your commit messages. It dovetails with Semantic Versioning by describing the features, fixes and breaking changes made in commit messages.

Each commit starts with a type, optional scope and description. The body of the commit can be followed with optional footers that add more meaning.

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

The only defined types are fix for bug fixes (PATCH in semantic versioning) and feat for new features (MINOR in semantic versioning). You’re free to use additional types to describe other sorts of commit. In practice most people use a standard set of types.

Scope can be anything but usually identifies a particular feature or area of the repo.

The only defined footer is BREAKING CHANGE (MAJOR in semantic versioning). Again, you’re free to add others for your own purposes.

Versioning

Before we can publish, we need to version our package. I chose Lerna as my monorepo tool because it has workflows that automate versioning and publishing. It can use conventional commits to generate the new version number and update dependencies in package.json files to match. By default it will apply the same version number to all workspaces with changes.

It will also generate and update CHANGELOG.md files. One at the root of the repo containing all changes, and one in each workspace listing just the changes that effect that workspace. The change log includes the descriptions from each fix and feat commit, tagged by scope.

Lerna generated CHANGELOG
×
Lerna generated CHANGELOG

You can configure how versions are updated and change logs generated in your lerna.config. I’ve added entries to ignore changes to unit tests.

{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "0.1.2",
  "ignoreChanges": [
    "**/src/test/**",
    "**/src/*.test.*"
  ],
  "command": {
    "version": {
      "message": "chore(release): version %s"
    }
  }
}

Lerna will create a commit for all the changes that it’s made, tag everything with the version number and push the changes to GitHub. I added another config entry so that Lerna’s commit message uses the conventional commits chore type.

Lerna generated commits
×
Lerna generated commits

Don’t make the same mistake I did and create a “version” script in your top level package.json to run lerna version --conventional-commits for you. It turns out “version” is the name of a predefined lifecycle script that lerna version will run for you, leading to hilarious infinite recursion fun. Luckily it errored out the second time through.

Once I figured that out, I got a successful run.

% npx lerna version --conventional-commits
lerna notice cli v8.1.2
lerna info current version 0.0.1
lerna info Looking for changed packages since v0.0.1
lerna info ignoring diff in paths matching [ '**/src/test/**', '**/src/*.test.*' ]
lerna info getChangelogConfig Successfully resolved preset "conventional-changelog-angular"

Changes:
 - virtual-scroll-samples: 0.0.1 => 0.1.0 (private)
 - @candidstartup/react-virtual-scroll: 0.0.1 => 0.1.0

? Are you sure you want to create these versions? Yes
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna success version finished

Publishing

Now we’re finally ready to publish our newly versioned packages. Lerna can version and publish in one step if you prefer, but I like being able to review what it did before pushing out to the world. I used lerna publish from-git to publish the packages with the latest committed version. Alternatively, you can use lerna publish from-package to publish any packages newer than the latest packages on NPM.

% npm run lerna-publish

> lerna-publish
> npx lerna publish from-git

lerna notice cli v8.1.2

Found 1 package to publish:
 - @candidstartup/react-virtual-scroll => 0.1.2

? Are you sure you want to publish these packages? Yes
lerna info publish Publishing packages to npm...
lerna WARN notice Package failed to publish: @candidstartup/react-virtual-scroll
lerna ERR! E404 Not found
lerna ERR! errno "undefined" is not a valid exit code - exiting with code 1

That’s a really unhelpful error message. After some Googling I realized that I needed to run npm login on the command line first. Which in turn launches a web browser so that I can enter username, password and a Google Authenticator OTP (one-time password) code.

Let’s try that again.

% npm run lerna-publish

> lerna-publish
> npx lerna publish from-git

lerna notice cli v8.1.2

Found 1 package to publish:
 - @candidstartup/react-virtual-scroll => 0.1.2

? Are you sure you want to publish these packages? Yes
lerna info publish Publishing packages to npm...
? This operation requires a one-time password: ******
lerna success published @candidstartup/react-virtual-scroll 0.1.2
lerna notice 
lerna notice 📦  @candidstartup/react-virtual-scroll@0.1.2
lerna notice === Tarball Contents === 
lerna notice 1.5kB  LICENSE          
lerna notice 18.8kB dist/index.js    
lerna notice 1.5kB  package.json     
lerna notice 38.2kB dist/index.js.map
lerna notice 2.8kB  README.md        
lerna notice 2.9kB  dist/index.d.ts  
lerna notice === Tarball Details === 
lerna notice name:          @candidstartup/react-virtual-scroll         
lerna notice version:       0.1.2                                       
lerna notice filename:      candidstartup-react-virtual-scroll-0.1.2.tgz
lerna notice package size:  14.4 kB                                     
lerna notice unpacked size: 65.7 kB                                     
lerna notice shasum:        6cd84089f7b724d8c1d61f1b9ae910a3679f493e    
lerna notice integrity:     sha512-0zuqSwVhFtb8D[...]8YvmraVskjPqw==    
lerna notice total files:   6                                           
lerna notice 
Successfully published:
 - @candidstartup/react-virtual-scroll@0.1.2
lerna success published 1 package

If you have two factor authentication enabled, NPM requires you to enter the OTP code again when you publish. If that gets too annoying I can look at setting up an automation token for fully automated publish.

Back to npmjs.com.

react-virtual-scroll published on NPM
×
react-virtual-scroll published on NPM

Success. I have a published package released to the world.

NPM Tools

Once you have a package published, there are lots of tools out there to play with.

Badges

NPM Type Definitions NPM Version NPM bundle size

Shields.io generates those cool badges that people like to add to their READMEs, including ones dynamically populated with data from NPM.

I wonder how many more versions I’ll publish before you read this. Check the badge for the latest version.

Are the types wrong?

I was unsure whether the types were configured correctly in my package. The TypeScript compiler has too many options to try them all. Are the types wrong? will tell you what works.

Checking react-virtual-scroll types
×
Checking react-virtual-scroll types

It all looks as expected. I’ve only provided an ESM build, so if you insist on importing into a CommonJS module you’ll need to use a dynamic import.

CodeSandbox

The react-window library has lots of bare-bones examples that you can open in CodeSandbox with a single click. I found them to be tremendously useful. You can see exactly what you need to get started. You have the reassurance that the examples actually work. You can play around with them and see what happens when you change the input props.

CodeSandbox running a react-virtual-scroll sample
×
CodeSandbox running a react-virtual-scroll sample

There’s no account needed. All you need is a self-contained sample project in GitHub. Embed the path in a CodeSandbox URL and CodeSandbox will download the entry point, install dependencies from NPM (including @candidstartup/react-virtual-scroll) and bring up an editor and the running sample.

I was so impressed that I put together Sandbox samples for VirtualGrid and VirtualList. Obviously, I wanted to be able to test them locally before committing and pushing to GitHub. That turned out to be more difficult than I thought.

I could only get CodeSandbox to work when the entry point is a .js file. The sample includes JSX which CodeSandbox is happy with. Vite isn’t. It requires any code that includes JSX to have a .jsx extension. I needed some vite.config magic from Stack Overflow to force Vite to accept JSX in .js files.

Next Time

That’s more than enough time messing around with tooling. I need to get some more react-virtual-scroll features implemented and have a real reason to push out another release.