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:
Running
npm run build
at the root should make a build of all your components withcjs
andesm
formats output in adist folder.
Running
npm run storybook
should start your dev kitchen sync.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
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
- 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
Top comments (2)
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?
Thanks!
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.