Back to all articles

Writing your first React UI Library · Part 4 of 4

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

Map compiled library outputs, finish the Storybook and builder setup, and publish with Lerna.

Shipping the React UI library

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

What are we going to do?

  • Map our compiled JavaScript for older and newer clients in package.json.
  • Change the Storybook and builder configuration to compile from source.
  • Publish with Lerna!

Shipping it!

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

  1. Running npm run build at the root should build all your components in CJS and ESM formats in a dist folder.

    Compiled package output
  2. Running npm run storybook should start your development kitchen sink.

  3. CSS Modules should work in Storybook and appear in the compiled files.

Mapping our compiled files in package.json

We have two types of clients for our UI Library:

  1. People who want an “it just works™” experience by importing components and forgetting about them. They get compiled components and CSS that should avoid most style clashes.
  2. “Power users” who have their own bundling system and want to generate classes during their own build process.

For this, modify the package.json in all distributable packages.

phoenix/package.json

"main": "dist/phoenix.cjs.js",
"module": "dist/phoenix.esm.js",
"src": "lib/phoenix.js",

phoenix-button/package.json

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

phoenix-text/package.json

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

Modern bundlers like Webpack or Rollup will use the module entry for ES module imports and main for require.

We want those to resolve from the compiled version in case clients do not have CSS Modules configured in their app and simply want to use our components.

Notice that we added a src attribute. This is a pointer to the real source that “power users” can compile themselves.

Before we proceed, add the dist folder to the files published to npm. For example, in the phoenix package:

"files": [
  "dist",
  "lib"
],

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

Fix Storybook setup

When running Storybook it will grab the code pointed to by module, since that is the default webpack behavior. We do not want that: our kitchen sink should always point to the latest source so we can try new things without building first.

.storybook/main.js

module.exports = {
  stories: ['../packages/**/*.stories.js'],
  addons: ['@storybook/addon-actions', '@storybook/addon-links'],
  webpackFinal: async (config) => {
    config.module.rules = config.module.rules.filter(
      (rule) => rule.test.toString() !== '/\\.css$/',
    );
 
    config.module.rules.push({
      test: /\.css$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            modules: true,
          },
        },
      ],
    });
 
    config.resolve.mainFields = ['src', 'module', 'main'];
    return config;
  },
};

This tells Storybook to grab source first and fall back to the other entries.

Fix the builder setup

We also need to modify phoenix-builder to grab code from src instead of main.

phoenix-builder/lib/phoenix-builder.js

#!/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();
const { src, name } = require(path.join(currentWorkingPath, 'package.json'));
const inputPath = path.join(currentWorkingPath, src);
const fileName = name.replace('@cddev/', '');
 
const inputOptions = {
  input: inputPath,
  external: ['react'],
  plugins: [
    resolve(),
    postcss({ modules: true }),
    babel({
      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() {
  const bundle = await rollup.rollup(inputOptions);
 
  for (const options of outputOptions) {
    await bundle.write(options);
  }
}
 
build();

We are now ready to publish

Run:

lerna publish

This opens a prompt to select the version. We started at 0.0.0; since this is the first release, select Major.

Changes:
 - @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

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

Congrats! Your library has been published.

Animated celebration reaction GIF

How can your clients consume it?

The beauty of this setup is that clients can either consume the main phoenix package, which gets all components, or each component separately.

Consuming as a whole

npm i --save-dev @cddev/phoenix

And then in JavaScript:

import { Button, Text } from '@cddev/phoenix';
 
render() {
  return (
    <>
      <Button>Woo</Button>
      <Text>Waa</Text>
    </>
  );
}

Consuming one package only

npm i --save-dev @cddev/phoenix-button

And then in JavaScript:

import { Button } from '@cddev/phoenix-button';
 
render() {
  return <Button>Woo</Button>;
}

Conclusion

With this setup you should be able to add more packages, release them independently, and have a small UI development pipeline.

In future parts we will explore tools such as ESLint, Stylelint, and Prettier for a consistent codebase, plus 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.”

Resources