Skip to content

Building component library with Vite + React + TS + Code splitting

Published: at 02:04 PM

The following guide based on this article

In this guide we will find out how to create your own React component library with SCSS, CSS modules, with code splitting by files for JS and for CSS. Learn how to manage your assets as separate files such as fonts, pictures and SVGs

Table of contents

Open Table of contents

TLDR

Introduction

Vite is simple and a fast way to start develop your web application. In comparison with webpack with its complicated and low-level configuration, Vite allows you just install Vite and React plugin, but the rest stuff like SCSS, CSS modules and Typescript are available out of the box

Vite is extremely simple and easy to work when you are making your web application, but when you want to make a component library you face the issues:

Vite has a special mode for making component libraries called library mode, but there is no built-in support of these basic features!

Features of my build

Installing dependencies

Dev dependencies

npm install --save-dev \
vite \
typescript \
sass \
glob \
@types/react

Vite plugins for build

npm install --save-dev \
vite-plugin-dts \
vite-plugin-lib-inject-css \
vite-plugin-static-copy \
vite-tsconfig-paths \
@vitejs/plugin-react \
@laynezh/vite-plugin-lib-assets \

Peer dependencies

npm i --save-peer react react-dom

Vite Configuration

vite.config.js

import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
import { libInjectCss } from "vite-plugin-lib-inject-css";
import { extname, relative, resolve } from "path";
import { fileURLToPath } from "node:url";
import { glob } from "glob";
import libAssetsPlugin from "@laynezh/vite-plugin-lib-assets";

export default defineConfig({
  resolve: {
    // Aliases for SCSS and CSS
    alias: {
      $fonts: resolve("src/fonts"),
      // These styles are in public directory, so styles from this directory
      // can be used int the library and in the consuming app
      $shared: resolve("public/scss"),
    },
  },
  plugins: [
    react(),
    // Generate d.ts for lib
    dts({
      include: ["src"],
    }),
    // Paths from tsconfig.json
    tsconfigPaths(),
    // Add imports for css in generated js files
    // This the essence of the CSS code splitting
    libInjectCss(),
    // Extracts assets to specific directory. Without this plugin
    // Vite injects assets such fonts into CSS as base64
    libAssetsPlugin({
      include: /\.(eot|woff2?|ttf)(\?.*)?(#.*)?$/,
      name: "fonts/[name].[ext]",
    }),
    libAssetsPlugin({
      include: /\.(svg)(\?.*)?(#.*)?$/,
      name: "svg/[name].[ext]",
    }),
    libAssetsPlugin({
      include: /\.(png|jpeg|jpg|gif|webp)(\?.*)?(#.*)?$/,
      name: "images/[name].[ext]",
    }),
  ],
  build: {
    // Copy everything from public directory to dist
    // This is why our mixins from /public/scss are available
    // in the consuming app
    copyPublicDir: true,
    // We don't need minification for library in order to debug it
    minify: false,
    // This switches Vite ot the Vite Library mode
    lib: {
      // Lib entry point
      entry: resolve(__dirname, "src/main.ts"),
      formats: ["es"],
    },
    rollupOptions: {
      external: ["react", "react/jsx-runtime"],
      input: Object.fromEntries(
        glob
          .sync(["src/**/*.{ts,tsx}"], {
            ignore: ["src/**/*.d.ts", "src/**/*.d"],
          })
          .map(file => {
            return [
              // The name of the entry point
              // lib/nested/foo.ts becomes nested/foo
              relative(
                "src",
                file.slice(0, file.length - extname(file).length)
              ),
              // The absolute path to the entry file
              // lib/nested/foo.ts becomes /project/lib/nested/foo.ts
              fileURLToPath(new URL(file, import.meta.url)),
            ];
          })
      ),
      output: {
        // This is path for CSS assets
        assetFileNames: "assets/css/[name][extname]",
        entryFileNames: "[name].js",
      },
    },
  },
});

Example project

For this article, I provide a minimalistic project for demonstration purposes. Let’s briefly describe the file structure. You can clone the project here or here

package.json
tsconfig.json
vite.config.js

# main.ts is the entry point for the library
src/main.ts
# Global styles
src/main.scss
# All necessary declarations for TS (CSS modules, images and etc.)
src/declaration.d.ts

# This component imports an image
src/components/Content/Content.tsx
src/components/Content/Content.module.scss
src/components/Content/image.png

# This component imports a SVG
src/components/Header/Header.tsx
src/components/Header/Header.module.scss
src/components/Header/close.svg

# Both our React components import fonts using SCSS mixins from mixins.scss
public/scss/mixins.scss
src/fonts/Roboto-Light.ttf
src/fonts/Roboto-Regular.ttf

The reason why /scss/mixins.scss is in public directory, all public directory contents copy into the final build. This is why we can use /scss/mixins.scss in the consuming app

Example project after the build

The structure after the build will look like this

dist/assets/svg/close.svg               0.32 kB │ gzip: 0.21 kB
dist/assets/images/image.png            0.96 kB

dist/assets/fonts/Roboto-Light.ttf    167.00 kB
dist/assets/fonts/Roboto-Regular.ttf  168.26 kB

dist/assets/css/Content.css             0.16 kB │ gzip: 0.14 kB
dist/assets/css/Header.css              0.19 kB │ gzip: 0.16 kB
dist/assets/css/main.css                0.33 kB │ gzip: 0.16 kB

dist/components/Content/Content.js      0.43 kB │ gzip: 0.27 kB
dist/components/Header/Header.js        0.48 kB │ gzip: 0.28 kB
dist/main.js                            0.18 kB │ gzip: 0.13 kB

dist/scss/mixins.**scss**

let’s look at some JS files

main.js

import "./assets/css/main.css";
import { Header } from "./components/Header/Header.js";
import { Content } from "./components/Content/Content.js";
export { Content, Header };

Header.js

import "../../assets/css/Header.css";
import { jsxs, jsx } from "react/jsx-runtime";
import closeSvgPath from "../../assets/svg/close.svg";
const header = "_header_1hika_1";
const close = "_close_1hika_10";
const s = {
  header,
  close,
};
const Header = ({ children }) => {
  return /* @__PURE__ */ jsxs("header", {
    className: s.header,
    children: [
      children,
      /* @__PURE__ */ jsx("img", {
        src: closeSvgPath,
        alt: "close",
        className: s.close,
      }),
    ],
  });
};
export { Header };

Here we can assets imported from the bundled JS code. But keep in mind: to be able to use the library built like that, you may have to configure the consumer’s app bundler to be able to import CSS and other assets