Sharing reusable Vue.js components with Lerna, Storybook, and npm or GitHub Package Registries
Creating reusable components is extremely important these days. We can get a lot of benefits from following the Component-Driven Development (CDD) methodology, it will result in code with more quality and fewer bugs, simply because while developing we can concentrate all our focus on a single component at a time, a single specific task.
It becomes easier to create new applications and even modify them, because we change the component once, and every place where it is being used will reflect that change. However, maintaining, sharing and reusing components can be tricky without the proper tools and that’s what I try to tackle in this article with Lerna, Storybook, npm and GitHub Package Registry.
What is the end goal of this article?
Create a GitHub repository where some Vue.js components that can be used across projects will be developed. It should be possible to visualize and publish these components to the npm or GitHub package registries. All these components must be scoped inside our username (eg. jsilva-pt).
What tools will we be using?
Lerna
Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.
Storybook
Storybook is a user interface development environment and playground for UI components. The tool enables developers to create components independently and showcase components interactively in an isolated development environment.
npm
npm is the world’s largest software registry.
GitHub Package Registry
GitHub Package Registry is a software package hosting service, similar to npmjs.org, rubygems.org, or hub.docker.com, that allows you to host your packages and code in one place. You can host software packages privately or publicly and use them as dependencies in your projects.
How these tools can work together?
Lerna will be used to keep all our components inside the same repository (monorepos) with independent versions, Storybook to create and visualize components in isolation and npm/GitHub package registry to distribute them to any user.
What will be our steps?
Setting up the project
- Create a new project using Lerna;
- Install Storybook;
- Adjust some configurations;
Create the first component
- Create a JTableRow component and its stories;
- Publish to npm;
Create the second component
- Create a JTable component;
- Add JTableRow as a dependency;
- Create its stories;
- Publish to npm;
Improve the JTable component
- Add optional header;
- Publish a new version to npm;
Moving from npm to GitHub package registry
- Configure Lerna to use GitHub as a package registry;
- Authenticate on GitHub package registry;
- Publish to GitHub package registry;
So, let's start!
Setting up the project
Let’s start by installing Lerna globally and creating a new project:
npm install --global lernamkdir medium-reusable-vue-components && cd $_
lerna init --independent
Here we are creating a project called medium-reusable-vue-components
and initializing a Lerna project in independent mode, which is perfect for us, as we can see in their documentation:
“Independent mode Lerna projects allows maintainers to increment package versions independently of each other. Each time you publish, you will get a prompt for each package that has changed to specify if it’s a patch, minor, major or custom change.
Independent mode allows you to more specifically update versions for each package and makes sense for a group of components.”
Then, we install Storybook for Vue …
npx -p @storybook/cli sb init --type vue
… as well as the necessary dependencies
npm install vue --save
npm install vue-loader vue-template-compiler @babel/core babel-loader babel-preset-vue --save-dev
In the package.json
file we can now find some commands that will allow us to.
- Compile and hot-reload for development (npm run storybook);
- Compile and minify for production (npm run build-storybook).
{
...
"scripts": {
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
}
}
By default, Lerna creates a packages
folder where every package must be located. However, we are creating UI components so it would be preferable to have a folder called components
. Lerna allows us to do it by replacing the configuration in the lerna.json
file.
- Rename
packages
folder tocomponents
; - Rename
"packages/*"
to"components/*"
inlerna.json
{
"packages": [
"components/*"
],
"version": "independent"
}
To finish the configuration let’s just create a .gitignore
file to ignore some files and folders that we don’t want to commit to our repository.
node_modules
storybook-static
npm-debug.log*
lerna-debug.log*
And it's done! The project configuration is done! 💪
Too easy, right? :P
At this point, our project structure must look like this:
├── .storybook
│ ├── addons.js
│ └── config.js
├── components
├── stories
│ └── ...
├── .gitignore
├── lerna.json
├── package.json
└── package-lock.json
Let's start creating some components! 😃
Creating our first component: JTableRow
To start a new component we could just run lerna add <package>
, however, it would generate some folders and files that we really don’t want, so let's do it manually.
- Create a
JTableRow
folder inside thecomponents
folder; - Create a
JTableRow.vue
file inside theJTableRow
folder with the following content:
This component is simply a row of a table that receives and prints the values sent to the prop values
.
I like to keep my components, stories, and tests as close as possible to each other, so let's create the JTableRow.stories.js
file next to the component:
When we install Storybook, by default, it will search for a stories
folder in our root project. However, as we are going to keep the stories next to the components, let's remove the stories
folder and change this configuration in .storybook/config.js
file.
import { configure } from '@storybook/vue';// automatically import all files ending in *.stories.js
const req = require.context(
'../components',
true,
/\.stories\.js$/
);function loadStories() {
req.keys().forEach(filename => req(filename));
}configure(loadStories, module);
Right now, our folder structure must be:
├── .storybook
│ ├── addons.js
│ └── config.js
├── components
│ └── JTableRow
│ ├── JTableRow.stories.js
│ ├── JTableRow.vue
│ └── package.json
├── .gitignore
├── lerna.json
├── package.json
└── package-lock.json
And we should be able to see our first component by running the command npm run storybook
, that will give us this page:
We are not done yet! We need to make this component available to be used by other applications/projects/components. In order to do that, let’s create a package.json
file inside the JTableRow
folder, with the following content:
{
"name": "@jsilva-pt/j-table-row",
"version": "0.0.0",
"publishConfig": {
"access": "public"
}
}
As described in our goal, our components must be scoped. When we create an account on npm, our username can be used as scope name, so we only need to:
- Prefix the component name with the scope name (@jsilva-pt/j-table-row);
- Set the access to the component as public.
Regarding versioning, when publishing a new version of the component, it will be automatically incremented, so we set its version to 0.0.0.
Our final steps are:
- Create a repository and push our code.
git add .
git commit -m "JTableRow component"
git remote add origin git@github.com:jsilva-pt/medium-reusable-vue-components.git
git push -u origin master
- Login in our npm account:
npm login
; - Run
lerna publish
:
And we are done! Our first reusable component is published and available to be used by everyone thought npm registry.
This component, by itself, is not very useful, so let's create another one that will use it as a dependency.
Creating our second component: JTable
- Create a
JTable
folder inside thecomponents
folder; - Create its
package.json
;
{
"name": "@jsilva-pt/j-table",
"version": "0.0.0",
"publishConfig": {
"access": "public"
}
}
- Add
JTableRow
as its dependency, which will update ourJTable package.json
and generate anode_modules
folder inside theJTable
;
lerna add @jsilva-pt/j-table-row --scope=@jsilva-pt/j-table
- Create a
JTable.vue
file inside theJTable
folder;
- Create a
JTable.stories.js
file inside theJTable
folder;
- Check Storybook to make sure everything is ok;
- Push our modifications to GitHub;
git add .
git commit -m "JTable component"
git push
- And publish the component:
lerna publish
;
Our final structure is:
├── .storybook
│ ├── addons.js
│ └── config.js
├── components
│ ├── JTable
│ │ ├── JTable.stories.js
│ │ ├── JTable.vue
│ │ └── package.json
│ └── JTableRow
│ ├── JTableRow.stories.js
│ ├── JTableRow.vue
│ └── package.json
├── .gitignore
├── lerna.json
├── package.json
└── package-lock.json
Improve JTable
Our JTable
is already useful but it would be nice to have a header like every normal table has, so let's do it.
- Add a
prop header
that accepts an array of values, iterate and displays them;
- Add a new story to the
JTable.stories.js
file that demonstrates this new option;
- Check Storybook;
- Push our modifications to GitHub;
git add .
git commit -m "Added header prop to JTable"
git push
- Publish the improvement made:
lerna publish
;
And we are done! A new version is available.
Moving from npm to GitHub Package Registry
Our project is ready to publish packages to npm but GitHub Package Registry just arrived and we decided we want to keep our packages close to the code. What do we need to do?
- Specify on the
package.json
of each component the repository where our code lives, adding the following configuration:
{
...
"repository" : {
"type" : "git",
"url": "ssh://git@github.com:jsilva-pt/medium-reusable-vue-components.git"
}
}
- Specify on
lerna.json
the registry where we want to publish the components, adding the following configuration:
{
...
"command": {
"publish": {
"registry": "https://npm.pkg.github.com"
}
}
}
- Create a personal token on GitHub with the permissions write:packages and read:packages;
- Authenticate on GitHub Package Registry using the generated token as a password:
$ npm login --registry=https://npm.pkg.github.com
> Username: USERNAME
> Password: TOKEN
> Email: PUBLIC EMAIL ADDRESS
- Once we didn’t change any code on our components, Lerna won’t publish a new version if we run
lerna publish
. To enforce that, let’s use the command:lerna publish from-package
, which will bump the components’ version, and publish them.
How can we use these components in other projects?
Independently of using npm or GitHub as a package registry, just install them like any other dependency, through npm or yarn.
# install
npm install @jsilva-pt/j-table# importing JTable automatically will bring JTableRow
import JTable from '@jsilva-pt/j-table/JTable'
Final Notes:
- We didn’t cover tests to our components, however, it is essential to have them.
- The source code can be found at GitHub.
master
branch publishes to npm whilegithub-registry
branch publishes to GitHub. - The code is deployed on Netlify.
Conclusion
With Lerna, we can manage several components in the same repository instead of creating one for each, which would become really difficult to maintain.
Storybook, for me, is essential in every project, even when components are not being created in a separated repository.
npm don’t really need a presentation, it is the “world’s largest software registry” that allow us to easily share components between projects.
GitHub Package Registry is a new registry that will allow us to keep the packages close to the code, which in my opinion is a big advantage.
Currently, it is very easy to create reusable components. Tools like Lerna, Storybook, npm and GitHub really help to isolate our components from the application, resulting in more focus, quality and development speed.