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:
- JS code splitting by components without bundling it to the gigantic index.js file
- CSS code splitting by components without bundling it into one index.css file
- Asset management: fonts, pictures, SVGs as separate files
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
- JS code splitting by components without bundling it to the gigantic index.js file
- CSS code splitting by components without bundling it into the one index.css file
- Asset management: fonts, pictures, SVGs as separate files. No SVG in JSX, no base64 encoded assets
- Typescript with aliases from
tsconfig.json
(@
,@components
,@utils
and etc.) - SCSS and CSS modules with aliases (
$fonts
,$shared
and etc.) - Ability to ship some files as it is. For example: SCSS files with mixins and functions, so you are able to import it directly from your project and use it
- Tree shacking friendly: thanks to code, styles and assets splitting
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