DEV Community

Carlos Castro
Carlos Castro

Posted on


Writing your first React UI Library - Part 4: Ship It! (V1)

ship it

This is the fourth post on a series on how to make your own UI React Library.

What are we going to do?

  • Map our compiled Javascript for older and newer clients in our package.json.
  • Change a little bit the configuration of storybook and our builder to support compiling from the source.
  • Publishing with Lerna!

Shipping it!

By now you should have almost everything ready to ship it:

  1. Running npm run build at the root should make a build of all your components with cjs and esm formats output in a dist folder.
    file output

  2. Running npm run storybook should start your dev kitchen sync.

  3. CSS Modules are working in storybook and you can see the css on the compiled files as well.

Mapping our compiled files in package.json

We have two types of clients for our UI Library:

1) People that just want an it just works™ by just importing our components and forgetting about them; They will get our compiled components + css which won't clash with their styles for the most.

2) People deemed as power users which have their own bundling system and they want to generate their classes on their build process.

For this we'll modify the package.json in all our distributable packages to:


  "main": "dist/phoenix.cjs.js",
  "module": "dist/phoenix.esm.js",
  "src": "lib/phoenix.js",
Enter fullscreen mode Exit fullscreen mode


  "main": "dist/phoenix-button.cjs.js",
  "module": "dist/phoenix-button.esm.js",
  "src": "lib/phoenix-button.js",
Enter fullscreen mode Exit fullscreen mode


  "main": "dist/phoenix-text.cjs.js",
  "module": "dist/phoenix-text.esm.js",
  "src": "lib/phoenix-text.js",
Enter fullscreen mode Exit fullscreen mode

Modern bundlers like Webpack or Rollup will use module entry when using imports/exports on an ES6 environment and main when we are using require.

We want those to be resolved from the compiled version in case our clients don't have CSS Modules on their app and they just want to use our components.

Notice We added a src attribute, this is basically a pointer to the real source that we want our power users to use.

Before we can proceed, we also need to add the dist folder to the files we are publishing to NPM; This can be done by adding the folder name into the files array in each package.json. For example this is the modification in the phoenix package.


  "files": [
Enter fullscreen mode Exit fullscreen mode

Do the same for phoenix-button and phoenix-text packages.

Fix Storybook setup

The problem now, is that when running storybook it will grab the code pointing to module since this is the default webpack configuration behavior.

See here:

We don't want that since our kitchen sink should always point to latest src so we can try new things without having to run build on every change;

Let's change that:


module.exports = {
  stories: ['../packages/**/*.stories.js'],
  addons: ['@storybook/addon-actions', '@storybook/addon-links'],
  webpackFinal: async (config) => {
    // remove default css rule from storybook
    config.module.rules = config.module.rules.filter((f) => f.test.toString() !== '/\\.css$/');

    // push our custom easy one
      test: /\.css$/,
      use: [
          loader: 'css-loader',
          options: {
            // Key config
            modules: true,
    // This is where we change the order of resolution of main fields
    config.resolve.mainFields = ['src', 'module', 'main'];

    // Return the altered config
    return config;
Enter fullscreen mode Exit fullscreen mode

With the above we are telling the storybook webpack to first grab src and if it doesn't find it, then fallback to the other options. This is the same configuration we are going to ask our power users to use when compiling the components on their own.

Fix the builder setup

We also need to modify our phoenix-builder to grab the code from src instead of main as we had before.


#!/usr/bin/env node
const rollup = require('rollup');
const path = require('path');
const resolve = require('@rollup/plugin-node-resolve').default;
const babel = require('@rollup/plugin-babel').default;
const postcss = require('rollup-plugin-postcss');

const currentWorkingPath = process.cwd();
// Little refactor from where we get the code
const { src, name } = require(path.join(currentWorkingPath, 'package.json'));

// build input path using the src
const inputPath = path.join(currentWorkingPath, src);

// Little hack to just get the file name
const fileName = name.replace('@cddev/', '');

// see below for details on the options
const inputOptions = {
  input: inputPath,
  external: ['react'],
  plugins: [
      // Key configuration
      modules: true,
      presets: ['@babel/preset-env', '@babel/preset-react'],
      babelHelpers: 'bundled',
      exclude: 'node_modules/**',
const outputOptions = [
    file: `dist/${fileName}.cjs.js`,
    format: 'cjs',
    file: `dist/${fileName}.esm.js`,
    format: 'esm',

async function build() {
  // create bundle
  const bundle = await rollup.rollup(inputOptions);
  // loop through the options and write individual bundles
  outputOptions.forEach(async (options) => {
    await bundle.write(options);

Enter fullscreen mode Exit fullscreen mode

We are now ready to publish


lerna publish
Enter fullscreen mode Exit fullscreen mode

This will open a prompt in your terminal to select the version you want to publish.

We started in version 0.0.0 and since this is our first release, let's select Major. This will present a message on what it's going to happen:

 - @cddev/phoenix-builder: 0.0.0 => 1.0.0
 - @cddev/phoenix-button: 0.0.0 => 1.0.0
 - @cddev/phoenix-text: 0.0.0 => 1.0.0
 - @cddev/phoenix: 0.0.0 => 1.0.0
Enter fullscreen mode Exit fullscreen mode

Run it!

If everything goes well, you should see:

Successfully published:
 - @cddev/phoenix-builder@1.0.0
 - @cddev/phoenix-button@1.0.0
 - @cddev/phoenix-text@1.0.0
 - @cddev/phoenix@1.0.0
lerna success published 4 packages
Enter fullscreen mode Exit fullscreen mode

Congrats! Your library has been published

How can your clients consume it?

The beauty of this setup is that your clients can either consume the main package phoenix which will get them all the components or each component separately. Here are some examples:

Consuming as a whole

npm i --save-dev @cddev/phoenix
Enter fullscreen mode Exit fullscreen mode

And then later in your JS

import { Button, Text } from '@cddev/phoenix';

render() {
  return (
Enter fullscreen mode Exit fullscreen mode

Consuming one package only

npm i --save-dev @cddev/phoenix-button
Enter fullscreen mode Exit fullscreen mode

And then later in your JS

import { Button } from '@cddev/phoenix-button';

render() {
  return (
Enter fullscreen mode Exit fullscreen mode


With this setup you should be able to add more packages, release them independently and hopefully have a small pipeline in terms of UI Development.

In future parts we will explore adding tooling such as eslint, stylelint, prettier to have a consistent codebase and prevent small bugs; We are also going to put in place a testing infrastructure using jest and react testing library.

For now I leave you with a phrase so you can keep learning on your own: "In case of doubt, push on just a little further and then keep on pushing."


Top comments (2)

jasongsheldon profile image

thank you for a wonderful guide to packaging. I have one issue which I hope can be resolved. After packing when I use my package I get the following error

ERROR in ./node_modules/@jason-core/Button/dist/Button.esm.js
Module not found: Error: Can't resolve './checkPropTypes' in '/Volumes/Untitled/test-web/node_modules/@jason-core/Button/dist'
@ ./node_modules/@jason-core/Button/dist/Button.esm.js 432:21-48
@ ./src/pages/home/Home.jsx

Basically I took the button example and just added some things which my usual components have and in this case the problem item seems to be

import PropTypes from 'prop-types'

Button.defaultProps = {
customStyle: null
Button.propTypes = {
children: PropTypes.node.isRequired,
onClick: PropTypes.func,
customStyle: PropTypes.string,

Do you have any ideas how to resolve this?


tiagossaurus profile image
Tiago Caetano • Edited

The solution to this would be to install prop-types on the package that is using it, in your case @jason-core/Button (Make sure you install it as a dependency and not a devDependency)

from the root dir run:
lerna add prop-types --scope=@jason-core/Button

then rebuild, commit and publish.

Visualizing Promises and Async/Await 🤓

async await

☝️ Check out this all-time classic DEV post on visualizing Promises and Async/Await 🤓