loading...

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

davixyz profile image Carlos Castro ・5 min read

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:

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 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.

phoenix/package.json

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

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: https://webpack.js.org/configuration/resolve/#resolvemainfields

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:

.storybook/main.js

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
    config.module.rules.push({
      test: /\.css$/,
      use: [
        'style-loader',
        {
          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;
  },
};

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.

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();
// 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: [
    resolve(),
    postcss({
      // Key configuration
      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() {
  // create bundle
  const bundle = await rollup.rollup(inputOptions);
  // loop through the options and write individual bundles
  outputOptions.forEach(async (options) => {
    await bundle.write(options);
  });
}

build();

We are now ready to publish

Run

lerna publish

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:

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

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

Congrats! Your library has been published
Carlton

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

And then later in your JS

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 later in your JS

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 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."

Resources

Discussion

pic
Editor guide