DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • React, Angular, and Vue.js: What’s the Technical Difference?
  • In-Depth Guide to Using useMemo() Hook in React
  • Top React Libraries for Data-Driven Dashboard App Development
  • The Cypress Edge: Next-Level Testing Strategies for React Developers

Trending

  • Blue Skies Ahead: An AI Case Study on LLM Use for a Graph Theory Related Application
  • How to Practice TDD With Kotlin
  • Immutable Secrets Management: A Zero-Trust Approach to Sensitive Data in Containers
  • A Modern Stack for Building Scalable Systems
  1. DZone
  2. Coding
  3. JavaScript
  4. Component Library With Lerna Monorepo, Vite, and Storybook

Component Library With Lerna Monorepo, Vite, and Storybook

Learn how to organize frontend packages in monorepo, track changes across all projects, reuse shared libraries, and build packages with a modern build system.

By 
Anton Kalik user avatar
Anton Kalik
·
Sep. 05, 23 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
9.1K Views

Join the DZone community and get the full member experience.

Join For Free

Building components and reusing them across different packages led me to conclude that it is necessary to organize the correct approach for the content of these projects in a single structure. Building tools should be the same, including testing environment, lint rules, and efficient resource allocation for component libraries.

I was looking for tools that could bring me efficient and effective ways to build robust, powerful combinations. As a result, a formidable trio emerged. In this article, we will create several packages with all those tools.

Tools

Before we start, let’s examine what each of these tools does.

  • Lerna: Manages JavaScript projects with multiple packages; It optimizes the workflow around managing multipackage repositories with Git and NPM.
  • Vite: Build tool providing rapid hot module replacement, out-of-the-box ES Module support, extensive feature, and plugin support for React
  • Storybook: An open-source tool for developing and organizing UI components in isolation, which also serves as a platform for visual testing and creating interactive documentation

Lerna Initial Setup

The first step will be to set up the Lerna project. Create a folder with lerna_vite_monorepo and inside that folder, run through the terminal npx lerna init — this will create an essential for the Lerna project. It generates two files — lerna.json, package.json — and empty folder packages.

lerna.json — This file enables Lerna to streamline your monorepo configuration, providing directives on how to link dependencies, locate packages, implement versioning strategies, and execute additional tasks.

Vite Initial Setup

Once the installation is complete, a packages folder will be available. Our next step involves creating several additional folders inside packages the folder:

  • vite-common
  • footer-components
  • body-components
  • footer-components

To create those projects, we have to run npm init vite with the project name. Choose React as a framework and Typescript as a variant. Those projects will use the same lint rules, build process, and React version.

This process in each package will generate a bunch of files and folders:

├── .eslintrc.cjs
├── .gitignore
├── index.html
├── package.json
├── public
│   └── vite.svg
├── src
│   ├── App.css
│   ├── App.tsx
│   ├── assets
│   │   └── react.svg
│   ├── index.css
│   ├── main.tsx
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts


Storybook Initial Setup

Time to set up a Storybook for each of our packages. Go to one of the package folders and run there npx storybook@latest init for Storybook installation. For the question about eslint-plugin-storybook — input Y for installation. After that, the process of installing dependencies will be launched.

This will generate .storybook folder with configs and stories in src. Let’s remove the stories folder because we will build our own components.

Now, run the installation npx sb init --builder @storybook/builder-vite — it will help you build your stories with Vite for fast startup and HMR.

Assume that for each folder, we have the same configurations. If those installation has been accomplished, then you can run yarn storybook inside the package folder and run the Storybook.

Initial Configurations

The idea is to reuse common settings for all of our packages. Let’s remove some files that we don’t need in each repository. Ultimately, each folder you have should contain the following set of folders and files:

├── package.json
├── src
│   └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts


Now, let’s take all devDependencies and cut them from package.json in one of our package folders and put them all to devDependenices in the root package.json.

Run in root npx storybook@latest init and fix in main.js property:

stories: [
  "../packages/*/src/**/*..mdx",
  "../packages/*/src/**/*.stories.@(js|jsx|ts|tsx)"
],


And remove from the root in package.json two scripts:

"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"


Add components folder with index.tsx file to each package folder:

├── package.json
├── src
│   ├── components
│   │   └── index.tsx
│   ├── index.tsx
│   └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts


We can establish common configurations that apply to all packages. This includes settings for Vite, Storybook, Jest, Babel, and Prettier, which can be universally configured.

The root folder has to have the following files:

├── .eslintrc.cjs
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc.json
├── .storybook
│   ├── main.ts
│   ├── preview-head.html
│   └── preview.ts
├── README.md
├── babel.config.json
├── jest.config.ts
├── lerna.json
├── package.json
├── packages
│   ├── vite-body
│   ├── vite-common
│   ├── vite-footer
│   └── vite-header
├── test.setup.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts


We won’t be considering the settings of Babel, Jest, and Prettier in this instance.

Lerna Configuration

First, let’s examine the Lerna configuration file that helps manage our monorepo project with multiple packages.

JSON
 
{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "useWorkspaces": true,
  "packages": ["packages/*"],
  "version": "independent"
}


First of all, "$schema" provides structure and validation for the Lerna configuration.

When "useWorkspaces" is true, Lerna will use yarn workspaces for better linkage and management of dependencies across packages. If false, Lerna manages interpackage dependencies in monorepo.

"packages" defines where Lerna can find the packages in the project.

"version" when set to "independent", Lerna allows each package within the monorepo to have its own version number, providing flexibility in releasing updates for individual packages.

Common Vite Configuration

Now, let’s examine the necessary elements within the vite.config.ts file.

TypeScript
 
import path from "path";
import { defineConfig } from "vite";
import pluginReact from "@vitejs/plugin-react";

const isExternal = (id: string) => !id.startsWith(".") && !path.isAbsolute(id);

export const getBaseConfig = ({ plugins = [], lib }) =>
  defineConfig({
    plugins: [pluginReact(), ...plugins],
    build: {
      lib,
      rollupOptions: {
        external: isExternal,
        output: {
          globals: {
            react: "React",
          },
        },
      },
    },
  });


This file will export the common configs for Vite with extra plugins and libraries which we will reuse in each package. defineConfig serves as a utility function in Vite’s configuration file. While it doesn’t directly execute any logic or alter the passed configuration object, its primary role is to enhance type inference and facilitate autocompletion in specific code editors.

rollupOptions allows you to specify custom Rollup options. Rollup is the module bundler that Vite uses under the hood for its build process. By providing options directly to Rollup, developers can have more fine-grained control over the build process. The external option within rollupOptions is used to specify which modules should be treated as external dependencies.

In general, usage of the external option can help reduce the size of your bundle by excluding dependencies already present in the environment where your code will be run.

The output option with globals: { react: "React" } in Rollup's configuration means that in your generated bundle, any import statements for react will be replaced with the global variable React. Essentially, it's assuming that React is already present in the user's environment and should be accessed as a global variable rather than included in the bundle.

JSON
 
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


The tsconfig.node.json file is used to specifically control how TypeScript transpiles with vite.config.ts file, ensuring it's compatible with Node.js. Vite, which serves and builds frontend assets, runs in a Node.js environment. This separation is needed because the Vite configuration file may require different TypeScript settings than your frontend code, which is intended to run in a browser.

JSON
 
{
  "compilerOptions": {
    // ...
    "types": ["vite/client", "jest", "@testing-library/jest-dom"],
    // ...
  },
  "references": [{ "path": "./tsconfig.node.json" }]
}


By including "types": ["vite/client"] in tsconfig.json, is necessary because Vite provides some additional properties on the import.meta object that is not part of the standard JavaScript or TypeScript libraries, such as import.meta.env and import.meta.glob.

Common Storybook Configuration

The .storybook directory defines Storybook's configuration, add-ons, and decorators. It's essential for customizing and configuring how Storybook behaves.

├── main.ts
└── preview.ts


For the general configs, here are two files. Let’s check them all.

main.ts is the main configuration file for Storybook and allows you to control the behavior of Storybook. As you can see, we’re just exporting common configs, which we’re gonna reuse in each package.

TypeScript
 
import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
  addons: [
    {
      name: "@storybook/preset-scss",
      options: {
        cssLoaderOptions: {
          importLoaders: 1,
          modules: {
            mode: "local",
            auto: true,
            localIdentName: "[name]__[local]___[hash:base64:5]",
            exportGlobals: true,
          },
        },
      },
    },
    {
      name: "@storybook/addon-styling",
      options: {
        postCss: {
          implementation: require("postcss"),
        },
      },
    },
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
    "storybook-addon-mock",
  ],
  framework: {
    name: "@storybook/react-vite",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
};

export default config;


File preview.ts allows us to wrap stories with decorators, which we can use to provide context or set styles across our stories globally. We can also use this file to configure global parameters. Also, it will export that general configuration for package usage.

TypeScript
 
import type { Preview } from "@storybook/react";

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    options: {
      storySort: (a, b) => {
        return a.title === b.title
            ? 0
            : a.id.localeCompare(b.id, { numeric: true });
      },
    },
    layout: "fullscreen",
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
};

export default preview;


Root package.json

In a Lerna monorepo project, the package.json serves a similar role as in any other JavaScript or TypeScript project. However, some aspects are unique to monorepos.

JSON
 
{
  "name": "root",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "start:vite-common": "lerna run --scope vite-common storybook --stream",
    "build:vite-common": "lerna run --scope vite-common build --stream",
    "test:vite-common": "lerna run --scope vite-common test --stream",
    "start:vite-body": "lerna run --scope vite-body storybook --stream",
    "build": "lerna run build --stream",
    "test": "NODE_ENV=test jest --coverage"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.22.1",
    "@babel/preset-env": "^7.22.2",
    "@babel/preset-react": "^7.22.3",
    "@babel/preset-typescript": "^7.21.5",
    "@storybook/addon-actions": "^7.0.18",
    "@storybook/addon-essentials": "^7.0.18",
    "@storybook/addon-interactions": "^7.0.18",
    "@storybook/addon-links": "^7.0.18",
    "@storybook/addon-styling": "^1.0.8",
    "@storybook/blocks": "^7.0.18",
    "@storybook/builder-vite": "^7.0.18",
    "@storybook/preset-scss": "^1.0.3",
    "@storybook/react": "^7.0.18",
    "@storybook/react-vite": "^7.0.18",
    "@storybook/testing-library": "^0.1.0",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^14.0.0",
    "@types/jest": "^29.5.1",
    "@types/react": "^18.0.28",
    "@types/react-dom": "^18.0.11",
    "@typescript-eslint/eslint-plugin": "^5.57.1",
    "@typescript-eslint/parser": "^5.57.1",
    "@vitejs/plugin-react": "^4.0.0",
    "babel-jest": "^29.5.0",
    "babel-loader": "^8.3.0",
    "eslint": "^8.41.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.3.4",
    "eslint-plugin-storybook": "^0.6.12",
    "jest": "^29.5.0",
    "jest-environment-jsdom": "^29.5.0",
    "lerna": "^6.5.1",
    "path": "^0.12.7",
    "prettier": "^2.8.8",
    "prop-types": "^15.8.1",
    "sass": "^1.62.1",
    "storybook": "^7.0.18",
    "storybook-addon-mock": "^4.0.0",
    "ts-jest": "^29.1.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.0.2",
    "vite": "^4.3.2"
  }
}


Scripts will manage the monorepo. Running tests across all packages or building all packages. This package.json also include development dependencies that are shared across multiple packages in the monorepo, such as testing libraries or build tools. The private field is usually set to true in this package.json to prevent it from being accidentally published.

Scripts, of course, can be extended with other packages for testing, building, and so on, like:

"start:vite-footer": "lerna run --scope vite-footer storybook --stream",


Package Level Configuration

As far as we exported all configs from the root for reusing those configs, let’s apply them at our package level.

Vite configuration will use root vite configuration where we just import getBaseConfig function and provide there lib. This configuration is used to build our component package as a standalone library. It specifies our package's entry point, library name, and output file name. With this configuration, Vite will generate a compiled file that exposes our component package under the specified library name, allowing it to be used in other projects or distributed separately.

TypeScript
 
import * as path from "path";
import { getBaseConfig } from "../../vite.config";

export default getBaseConfig({
  lib: {
    entry: path.resolve(__dirname, "src/index.ts"),
    name: "ViteFooter",
    fileName: "vite-footer",
  },
});


For the .storybook, we use the same approach. We just import the commonConfigs.

TypeScript
 
import commonConfigs from "../../../.storybook/main";

const config = {
  ...commonConfigs,
  stories: ["../src/**/*..mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
};

export default config;


And preview it as well.

TypeScript
 
import preview from "../../../.storybook/preview";

export default preview;


For the last one from the .storybook folder, we need to add preview-head.html.

HTML
 
<script>
  window.global = window;
</script>


And the best part is that we have a pretty clean package.json without dependencies, we all use them for all packages from the root.

JSON
 
{
  "name": "vite-footer",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  },
  "dependencies": {
    "vite-common": "^2.0.0"
  }
}


The only difference is vite-common, which is the dependency we’re using in the Footer component.

Components

By organizing our component packages in this manner, we can easily manage and publish each package independently while sharing common dependencies and infrastructure provided by our monorepo.

Let’s look at the folder src of the Footer component. The other components will be identical, but the configuration only makes the difference.

├── assets
│   └── flow.svg
├── components
│   ├── Footer
│   │   ├── Footer.stories.tsx
│   │   └── index.tsx
│   └── index.ts
├── index.ts
└── vite-env.d.ts


The vite-env.d.ts file in the src folder helps TypeScript understand and provide accurate type checking for Vite-related code in our project. It ensures that TypeScript can recognize and validate Vite-specific properties, functions, and features.

Embedded Javascript
 
/// <reference types="vite/client" />


In the src folder, index.ts has:

TypeScript
 
export * from "./components";


And the component that consumes vite-common components look like this:

TypeScript-JSX
 
import { Button, Links } from "vite-common";

export interface FooterProps {
  links: {
    label: string;
    href: string;
  }[];
}

export const Footer = ({ links }: FooterProps) => {
  return (
    <footer>
      <Links links={links} />
      <Button label="Click Button" backgroundColor="green" />
    </footer>
  );
};

export default Footer;


Here’s what stories looks like for the component:

TypeScript-JSX
 
import { StoryFn, Meta } from "@storybook/react";
import { Footer } from ".";

export default {
  title: "Example/Footer",
  component: Footer,
  parameters: {
    layout: "fullscreen",
  },
} as Meta<typeof Footer>;

const mockedLinks = [
  { label: "Home", href: "/" },
  { label: "About", href: "/about" },
  { label: "Contact", href: "/contact" },
];

const Template: StoryFn<typeof Footer> = (args) => <Footer {...args} />;

export const FooterWithLinks = Template.bind({});
FooterWithLinks.args = {
  links: mockedLinks,
};

export const FooterWithOneLink = Template.bind({});
FooterWithOneLink.args = {
  links: [mockedLinks[0]],
};


We use four packages in this example, but the approach is the same. Once you create all the packages, you have to be able to build, run, and test them independently. Before all are in the root level, run yarn install then yarn build to build all packages, or build yarn build:vite-common and you can start using that package in your other packages.

Publish

To publish all the packages in our monorepo, we can use the npx lerna publish command. This command guides us through versioning and publishing each package based on the changes made.

lerna notice cli v6.6.2
lerna info versioning independent
lerna info Looking for changed packages since vite-body@1.0.0
? Select a new version for vite-body (currently 1.0.0) Major (2.0.0)
? Select a new version for vite-common (currently 2.0.0) Patch (2.0.1)
? Select a new version for vite-footer (currently 1.0.0) Minor (1.1.0)
? Select a new version for vite-header (currently 1.0.0) 
  Patch (1.0.1) 
❯ Minor (1.1.0) 
  Major (2.0.0) 
  Prepatch (1.0.1-alpha.0) 
  Preminor (1.1.0-alpha.0) 
  Premajor (2.0.0-alpha.0) 
  Custom Prerelease 
  Custom Version 


Lerna will ask us for each package version, and then you can publish it.

lerna info execute Skipping releases
lerna info git Pushing tags...
lerna info publish Publishing packages to npm...
lerna success All packages have already been published. 


Conclusion

I was looking for a solid architecture solution for our front-end components organization in the company I am working for. For each project, we have a powerful, efficient development environment with general rules that help us become independent. This combination gives me streamlined dependency management, isolated component testing, and simplified publishing.

References

  • Repository 
  • Vite with Storybook
JavaScript Library Monorepo React (JavaScript library)

Published at DZone with permission of Anton Kalik. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • React, Angular, and Vue.js: What’s the Technical Difference?
  • In-Depth Guide to Using useMemo() Hook in React
  • Top React Libraries for Data-Driven Dashboard App Development
  • The Cypress Edge: Next-Level Testing Strategies for React Developers

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!