Making an NPM package for a React component library with Tailwind CSS
Dec 04, 2020
6 minutes
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
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",
},
};
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
and create a simple postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Then we can initialise Tailwind CSS
npx tailwindcss init
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"],
//...
};
Create a styles
folder with tailwind.css
inside, with the following text
@tailwind base;
@tailwind components;
@tailwind utilities;
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
You can then use this in your rollup.config.js
file by adding this at the top
import postcss from "rollup-plugin-postcss";
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",
},
}),
],
Here we’re giving it the path of our postcss path under config
, telling it which files to run postcss on with extensions
and minimising 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 defintion for block
after the defintition 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"
},
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"],
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";
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
but any way to set the environment variable NODE_ENV
to production will work
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
Then to publish from the command line you can do
npm publish --access public
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.
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
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]
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
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
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'
Then we want to install all our packages, and run the build command
- run: npm install
- run: npm run-script build
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 }}
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 }}
And that’s it! Whenever you create a release, it’ll be published