DEV Community 👩‍💻👨‍💻

Sam Robbins
Sam Robbins

Posted on • Updated on

Making an NPM package for a React component library with Tailwind CSS

First you need to make an npm package, this can be done with npm init provided you have npm installed on your computer. For the name if you want a scoped package, e.g. @samrobbins/package, ensure that the package name follows that structure, otherwise, you can just go with package. Remember that these have to be unique, so check npm to ensure you're not overlapping. Also ensure that your main key is output.js, or if you want it to be something different, then substitute your different name when I use output.js further down in this file

The first thing we need is a JavaScript bundler, for this I've chosen rollup, but you could do this with any of them. To install rollup, run

npm i rollup
Enter fullscreen mode Exit fullscreen mode

The config file for rollup is rollup.config.js, so create that file, and we'll start simple with this

export default {
  input: "./index.js",
  output: {
    file: "./output.js",
    format: "esm",
  },
}
Enter fullscreen mode Exit fullscreen mode

This takes the file index.js and creates a file output.js, with the format of ES Modules (esm).

At the time of writing, the postcss plugin we'll use later is only compatible with postcss 7, so we'll install everything for the compatibility version of Tailwind CSS

npm install tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
Enter fullscreen mode Exit fullscreen mode

and create a simple postcss.config.js

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}
Enter fullscreen mode Exit fullscreen mode

Then we can initialise Tailwind CSS

npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

This will create a tailwind.config.js file, and we can add to purge whichever folder we're going to put our components in by adding a purge key like this

module.exports = {
  purge: ["./components/**/*.js"],
  //...
};
Enter fullscreen mode Exit fullscreen mode

Create a styles folder with tailwind.css inside, with the following text

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

This allows you to use things like @layers in the future if you need to.

Now Tailwind is set up, we want to go back to rollup so it understands what to do with it

For this we want to use the rollup-plugin-postcss plugin, which can be installed like this

npm install rollup-plugin-postcss
Enter fullscreen mode Exit fullscreen mode

You can then use this in your rollup.config.js file by adding this at the top

import postcss from "rollup-plugin-postcss";
Enter fullscreen mode Exit fullscreen mode

Then going into the main object, add a key called plugins, which is a list of functions, and you can add postcss like this

plugins: [
    postcss({
      config: {
        path: "./postcss.config.js",
      },
      extensions: [".css"],
      minimize: true,
      inject: {
        insertAt: "top",
      },
    }),
  ],
Enter fullscreen mode Exit fullscreen mode

Here we're giving it the path of our postcss path under config, telling it which files to run postcss on with extensions and minimizing the output with minimise. An important key here is inject, this determines where in the head of your page the CSS will be inserted. This is very important with Tailwind CSS as it has an order of priority, allowing for patterns like block md:flex and it will use display block less than the md viewport width, then flex after that. However, if there is a definition for block after the definition for md:flex, then this pattern will not work as expected. So in order for the CSS to work as you would expect, you want it at the top, and the inject key used as shown does this.

As these are React components, we expect React to be included in the application we're using these, so we want to add react and react-dom as peer dependencies. So add a peerDependencies key in your package.json and add the latest versions of those packages, at the time of writing, looking like this

"peerDependencies": {
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
},
Enter fullscreen mode Exit fullscreen mode

You can then specify the same kind of thing in rollup.config.js by adding these under the external key like this

external: ["react", "react-dom"],
Enter fullscreen mode Exit fullscreen mode

Next we want to generate the index.js file we referenced earlier. How specifically you export from your component files may change this, but for my example, I'm doing export default from all my component files. So for each component I have, I want to add a line that looks like this

export { default as Answer } from "./components/answer.js";
Enter fullscreen mode Exit fullscreen mode

This will reexport the default export as Answer from this file.

If you run rollup -c (-c specifying that you have a custom config) you should see that it builds to an output.js file. However if you look in here, you will see that the CSS is huge as Tailwind doesn't know if you're running locally or in production, and so assumes local and includes all the styles. You can quickly get around this by running

NODE_ENV=production rollup -c
Enter fullscreen mode Exit fullscreen mode

but any way to set the environment variable NODE_ENV to production will work

We also want to add babel to this project, which allows for using newer JavaScript features on older browsers.

To do this, we need to install a few packages to get all the features we need

npm install @babel/core @babel/preset-env @babel-preset-react @rollup/plugin-babel babel-loader
Enter fullscreen mode Exit fullscreen mode

In our rollup.config.js we need to import the rollup plugin we just installed by adding this at the top

import babel from "@rollup/plugin-babel";
Enter fullscreen mode Exit fullscreen mode

and then this new entry in our plugins array:

babel({
      babelHelpers: "bundled",
      exclude: "node_modules/**",
    }),
Enter fullscreen mode Exit fullscreen mode

and finally to tell babel what we want it to do, create a .babelrc file in your root directory with the following code

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}
Enter fullscreen mode Exit fullscreen mode

NPM publishing

Now we want to publish this package to npm, so make sure you have an npm account, then login with npm login, and add the flag --scope with your username, so I do:

npm login --scope=@samrobbins
Enter fullscreen mode Exit fullscreen mode

Then to publish from the command line you can do

npm publish --access public
Enter fullscreen mode Exit fullscreen mode

and this will publish it to npm. You need the --access public flag if you have a free account as scoped packages default to restricted but restricted packages are paid on npm.

GitHub action

Now we have a published package, but it's a bit of a pain to have to do this manually every time, so you can go further by creating a GitHub action to do it automatically

You can do this by creating a file insider .github/workflows of the yml format, for example, I created publish.yml

We'll go through this step by step, but if you want the whole file I'll put it at the bottom

First we want a name for our workflow, so we can see from the UI what is running if we have multiple actions, so set

name: Node.js package
Enter fullscreen mode Exit fullscreen mode

or whatever you want it called.

Next we want a trigger for this, I've chosen to have it when I create a GitHub release so that GitHub releases and NPM are in sync, but you can change the trigger if you want.

on:
  release:
    types: [created]
Enter fullscreen mode Exit fullscreen mode

Then we want to determine what is actually running. We don't need any operating specific features, so ubuntu is the best choice for the operating system to run it on.

jobs:
  build:
    runs-on: ubuntu-latest
Enter fullscreen mode Exit fullscreen mode

The rest of these steps sit underneath the build: key just like runs-on

First we want to get the code from our repository, this can be done with the actions/checkout action

- uses: actions/checkout@v2
Enter fullscreen mode Exit fullscreen mode

Then we want to set up our Node.js environment. Using the latest version of Node.js is suggested as some packages will use more modern Node.js features, for example I had Tailwind fail on Node.js 10. And we want to use the official npm registry as that's the one everyone is used to, but if you want to use something like the GitHub package repository, you could change that here.

- uses: actions/setup-node@v1
    with:
        node-version: '12.x'
        registry-url: 'https://registry.npmjs.org'
Enter fullscreen mode Exit fullscreen mode

Then we want to install all our packages, and run the build command

- run: npm install
- run: npm run-script build
Enter fullscreen mode Exit fullscreen mode

And finally we want to publish. Instead of using npm login like we did locally, here we want to instead use a token. This can be found on the npm website, and make sure you get a publish token. Then add this as NPM_TOKEN in the repository you will be running the action in.

This will allow the final statement to work

- run: npm publish --access public
      env:
        NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

So in total, the file should look like this

name: Node.js Package
on:
  release:
    types: [created]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@v1
      with:
        node-version: '12.x'
        registry-url: 'https://registry.npmjs.org'
    - run: npm install
    - run: npm run-script build
    - run: npm publish --access public
      env:
        NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

And that's it! Whenever you create a release, it'll be published

Top comments (5)

Collapse
 
craigdanj profile image
Craig • Edited on

There were no steps to set up the transpilation of React components....like JSX etc. Those aren't needed?

Collapse
 
samrobbins85 profile image
Sam Robbins

Thanks very much @abbanfahim for catching this and sorry Craig for taking so long to get round to this. I've updated this blog with instructions for adding babel.

Collapse
 
vernerkeel profile image
Verner Keel

Hi Sam. Thanks for your guide it was helpful. However, my output.js results in 100kb+ size just for three components even when running using NODE_ENV=production rollup -c like you suggested. Here's my repo: github.com/vernerkeel/examples-for...
Any suggestions how to get the output smaller?

Collapse
 
samrobbins85 profile image
Sam Robbins

Hmm sorry not sure about this. I'll have a look at your repo when I've got time

We are hiring! Do you want to be our Senior Platform Engineer? We're hiring for a Senior Platform Engineer and would love for you to apply.

Head here to learn more about who we're looking for.