
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:
-
Running
npm run buildat the root should build all your components in CJS and ESM formats in adistfolder.
-
Running
npm run storybookshould start your development kitchen sink. -
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:
- 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.
- “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 publishThis 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.0If 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 packagesCongrats! Your library has been published.

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/phoenixAnd 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-buttonAnd 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
- GitHub repository with all code: https://github.com/davixyz/phoenix
- Demo Storybook: https://davixyz.github.io/phoenix
- GitHub: https://github.com/davixyz
- Twitter: https://twitter.com/carloscastrodev
