How we host a React component library on GitHub Packages

Rares Capilnar
nPlan
Published in
7 min readNov 29, 2021

Why do we need a component library?

Most software start-ups will find themselves going through many iterations of their product. At nPlan our journey was no different. Over the first couple of years the face of our product underwent many changes and strategy shifts. Having a centralized way to control the look, feel and functionality of UI elements and easily-shared logic (think authentication or feature-flagging), as well as the ability to roll out a fresh test product by mostly putting building blocks together proved invaluable.

What we tried

When we first identified this need and opportunity we looked towards more hands-off, fully hosted solutions. For a while, we used a service with its own registry, tooling and methodology. The website had some useful features for examples and documentation, but ultimately the tooling was slightly awkward and onboarding new developers (and consumers of our library) was cumbersome and often hit bugs. As a result, we started looking for an alternative.

Why GitHub Packages

When looking for a new place to host our library, two obvious players came up: npm and GitHub (who had launched their Packages product just a few months before).

As our main concerns for this choice were:

  1. The ability to host an unlimited number of private packages
  2. Onboarding new consumers should be as easy as possible (ideally close to our existing infrastructure),

GitHub pulled ahead for us. As we were already using GitHub Enterprise we could use Packages at no additional cost. It also meant that all of our developers would already be enrolled and would only need minimal additional configuration to get started. Even better, GitHub Actions (which we were already using for frontend CI/CD) traffic does not count towards the Packages traffic quota — win!

Setting up and hosting a React component library on GitHub Packages

Let’s get our hands dirty and set up a repository to serve as our component library, create a small React component and publish it as a stand-alone package.

For our setup, we are using Rollup to produce the modules, alongside some yarn scripts to make it all quick and easy! We won’t be covering typescript support at this time, as this wasn’t part of our initial setup, but it shouldn’t be too hard to work it in.

Starting your library

For the purpose of this example, I am using node@16.13.0 with npm@8.1.0 but any reasonable version should work similarly.

Setting up the library project

In a new directory, run npm init and configure it to your desire. I went with the defaults for now. I’ve also set up my directory as a git repository, to host the source files.

For reference, I essentially ran:

mkdir my-component-library && cd my-component-library
npm init
git init
git remote add origin git@github.com:stephixone/my-component-library

Installing dependencies

Next, we install react and react-dom. If you don’t have yarn globally installed, or to ensure other contributors have it in the library project and to ensure version consistency, you will want to to install it here as well.

npm i react react-dom yarn

The following dev dependencies are also needed for our setup:

npm i -D @babel/core @babel/preset-react @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-url babel-loader rimraf rollup rollup-plugin-peer-deps-external rollup-plugin-postcss

Depending on your component code, you will likely also need other babel plugins in your dev dependencies, such as @babel/plugin-proposal-nullish-coalescing-operator. Don’t worry if you don’t know what you might need ahead of time, babel will complain at build time and let you know what plugins you need to install, and I will show you where these get specified further on.

Creating a component

To test our setup, let’s go ahead and create a new component that we can later on publish. I like my components to live under a src directory, so I have created that. You can put them wherever you wish, just make sure to update paths accordingly in the next steps.

Under src, I have made a SimpleButton directory with a SimpleButton.js and an index.js exporting it. My test component is just a very thin button wrapper:

./src/SimpleButton/SimpleButton.js
./src/SimpleButton/index.js

Lastly, each of our components needs its own package.json, to define the deployed package name, its dependencies and a few other necessary things. For my SimpleButton component, it looks like this:

./src/SimpleButton/package.json

The name of the package must be namespaced under the GitHub account or organisation you intend to publish it under. The version should start at 0.0.0 and never be tinkered with manually—we’ll let npm handle it later on.

Configuring rollup to build our modules

In the root of our library, let’s create a rollup.config.js file.

Import our plugins and some node bits we’ll need:

./rollup.config.js

Next, let’s create a helper function that gets us all directories under a specified path and use it to get all of our modules:

./rollup.config.js

Now, let’s create a configuration builder function, that accepts a module directory and returns a rollup configuration object for it:

./rollup.config.js

We set the input property to the path to the file exporting our module:

./rollup.config.js

And the output property to define what our build outputs should be. In our case, I am publishing both CommonJS and ECMAScript modules. If you strongly feel you will only ever need one of these formats, you can probably leave the other one out:

./rollup.config.js

We don’t want rollup to bundle react and react-dom with each of our packages, which it won’t by default, but in order to suppress some warnings we explicitly tell it to treat them as external and only expect them at runtime:

./rollup.config.js

You might find yourself needing to add more dependencies to this array, for things such as dayjs, lodash and similar.

Finally, for our configuration object, we set up plugins:

./rollup.config.js

I’ve included one of our optional babel plugins, to demonstrate where it would go. This is where you would include any other babel plugins your codebase might need, for language features or things like @emotion/babel-plugin.

Lastly, we export a helper function to resolve a package name from command line args into its config object:

./rollup.config.js

In the end, your rollup config file should look something like this:

./rollup.config.js

Adding yarn scripts to build and publish

To make our lives a lot easier, we use yarn scripts to build and publish our packages. We define these in the root package.json‘s scripts block.

First, let’s add a script that uses rollup to build the package specified in the command line:

"build": "run() { rollup -c --package=$1; }; run",

and a helper to clear existing build artifacts:

"clean": "run() { cd src/$1 && rimraf build; }; run",

Next, let’s add our main script for versioning and publishing a package:

"version-and-publish": "run() { yarn clean $1 && yarn build $1 && version=$(cd src/$1 && npm version --no-git-tag-version $2) && (cd src/$1 && npm publish --registry=https://npm.pkg.github.com/) && git add src/$1 && git commit -m \"$1 - $version\" && git push; }; run",

This is a long one, with a good bit of automation, but it boils down to these steps:

  • Clean the existing build dir, to ensure we don’t have any obsolete build artifacts
  • Build the specified package
  • Generate a new semantic version based on the specified versioning bump and update it in the corresponding package.json, saving it in a variable for later use in this script
  • Publish the package
  • Add contents of package’s directory to the git index
  • Make a commit with these changes, using the package name and the new version we saved above
  • Push the source changes to git

Lastly, we define a few helper scripts, which are the ones we will actually run from the command line:

"patch": "run() { yarn version-and-publish $1 patch; }; run","minor": "run() { yarn version-and-publish $1 minor; }; run","major": "run() { yarn version-and-publish $1 major; }; run"

And with that, all the scripts we need for now should be set up!

Publishing to GitHub Packages

Now that all our files and configurations are in order, let’s publish our package. The last hurdle before we can publish our component is setting up npm to be able to publish to GitHub’s registry.

First, let’s create a .npmrc file in the root of our library, linking the package namespace to the registry:

Then, navigate to GitHub’s Developer Personal Access Tokens page and generate a token with at least read:packages and write:packages permissions.

Once you have your token created and copied, run

npm login --registry=https://npm.pkg.github.com

and log in with your GitHub account, using the token as password.

You should now be set! Let’s publish our SimpleButton component by running one of the helper scripts we’ve set up earlier:

yarn patch SimpleButton

You can, of course, choose to run yarn minor or yarn major instead, but I’ve chosen to publish my package @0.0.1 for now. If you’d like to know more about versioning best practices, the semver docs are a great place to start.

Once the script has finished, I can now see the published package under the Packages listing in my repository:

To install this package in a different project, please refer to the GitHub documentation on installing. You’ll need to ensure you’re authenticated and have set up your package manager to work with the GitHub registry.

Conclusion

That’s it! These are the basic building blocks for publishing a React component library with individual packages to the GitHub npm registry. You can play around with adding more automation to your yarn scripts, with advanced rollup configurations to suit the needs of your components as well as with all sorts of cool “side effects” with GitHub Actions!

Complete source code for this article is available here.

--

--