If you are managing multiple React applications and want consistency across your user interfaces, sooner or later you'll find that you need a component library.
When I first wanted to create a React component library, it took me a lot of time to find a setup that met all my requirements and wasn't too complicated.
A guide like this would've spared me a great amount of energy wrestling with this stuff myself. I hope it can help you as much as it would have helped me.
This post covers setting up and publishing a React component library, including configuring your build process and publishing your package to npm so you and/or others can use it.
I've done my best to keep all configurations simple and concise, using default settings whenever possible.
When you are done, you can install your library like any other npm package:
npm install @username/my-component-library
And use it like:
import { Button } from `@username/my-component-library`;
function MyComponent() {
return <Button>Click me!</Button>
}
Before we start
Before we dig into the implementation details, I would like to elaborate on some technical details regarding the setup of the library.
🌳 Fully tree shakeable
For me it was particularly important that only necessary code ends up in the final application. When you import a component, it only includes the necessary JS and CSS styles. Pretty cool, right?
🦑 Compiled CSS modules
The components are styled with CSS modules. When building the library, these styles will get transformed to normal CSS style sheets. This means that the consuming application will not even be required to support CSS modules.
As a bonus compiling the CSS modules avoids a compatibility issue and the package can be consumed in both, environments that support named imports for CSS modules, and environments that don't.
🧁 If you are interested in using vanilla-extract instead of CSS modules you can find a branch with vanilla extract at the bottom of the article.
😎 TypeScript
While the library is written in TypeScript, it can be consumed in any "normal" JavaScript project as well. If you never used TypeScript before, give it a try. It not only forces you to write cleaner code, but also helps your AI coding assistant make better suggestions 😉
OK enough reading, now let's have some fun!
1. Setup a new Vite project
If you have never worked with Vite, think of it as a replacement for Create React App. Just a few commands and you are ready to go.
npm create vite@latest
? Project name: › my-component-library
? Select a framework: › React
? Select a variant: › TypeScript
cd my-component-library
npm i
That's it, your new Vite/React project is ready to go.
Here are two things I recommend you to do right after installing Vite.
2. Basic build setup
You can now run npm run dev
and browse to the url provided by Vite. While working on your library, this is a place where you can easily import your library and actually see your components. Think of all code inside the src
folder as your demo page.
The actual library code will reside in another folder. Let's create this folder and name it lib
. You could also name it differently, but lib
is a solid choice.
The main entry point of your library will be a file named main.ts
inside of lib
. When installing the library you can import everything that is exported from this file.
📂my-component-library
+┣ 📂lib
+┃ ┗ 📜main.ts
┣ 📂public
┣ 📂src
…
Vite Library Mode
At this time, if you build the project with npm run build
Vite will transpile the code inside src
to the dist
folder. This is default Vite behavior.
For now you will use the demo page for development purposes only. So there is no need to transpile this part of the project yet. Instead you want to transpile and ship the code inside of lib
.
This is where Vite's Library Mode comes into play. It was designed specifically for building/transpiling libraries. To activate this mode, simply specify your library entry point in vite.config.ts.
Like so:
import { defineConfig } from 'vite'
+ import { resolve } from 'path'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
+ build: {
+ lib: {
+ entry: resolve(__dirname, 'lib/main.ts'),
+ formats: ['es']
+ }
}
})
💡 The default formats are
'es'
and'umd'
. For your component library 'es' is all you need. This also removes the necessity for adding thename
property.💡 If your TypeScript linter complains about
'path'
and__dirname
just install the types for node:npm i @types/node -D
TypeScript and library mode
The tsconfig.json
created by Vite only includes the folder src
. To enable TypeScript for your newly created lib
folder as well you need to add it to the TypeScript configuration file like this:
- "include": ["src"],
+ "include": ["src", "lib"],
Although TypeScript needs to be enabled for both the src
and lib
folders, it is better to not include src
when building the library.
To ensure only the lib
directory is included during the build process you can create a separate TypeScript configuration file specifically for building.
💡 Implementing this separate configuration helps avoid TypeScript errors when you import components directly from the
dist
folder on the demo page and those components haven't been built yet.⚠️ For Vite 5 please read my comment on the new Typscript config structure.
📂my-component-library
┣ …
┣ 📜tsconfig.json
+┣ 📜tsconfig-build.json
…
The only difference is that the build config includes only the lib
directory, whereas the default configuration includes both lib
and src
📜tsconfig-build.json
{
"extends": "./tsconfig.json",
"include": ["lib"]
}
To use tsconfig-build.json
for building you need to pass the configuration file to tsc
in the build script in your package.json:
"scripts": {
…
- "build": "tsc && vite build",
+ "build": "tsc --p ./tsconfig-build.json && vite build",
Finally you will also need to copy the file vite-env.d.ts
from src
to lib
. Without this file Typescript will miss some types definitions provided by Vite when building (because we don't include src
anymore).
You can now execute npm run build
once more and this is what you will see in your dist folder:
📂dist
┣ 📜my-component-library.js
┗ 📜vite.svg
💡 The name of the output file is identical with the
name
property in your package.json per default. This can be changed in the Vite config (build.lib.fileName
) but we will do something else about this later.
The file vite.svg
is in your dist
folder because Vite copies all files from the public
directory to the output folder. Let's disable this behavior:
build: {
+ copyPublicDir: false,
…
}
You can read a more detailed explanation here: Why is the file vite.svg in the dist folder?
Building the types
As this is a Typescript library you also want to ship type definitions with your package. Fortunately there is a Vite plugin that does exactly this: vite-plugin-dts
npm i vite-plugin-dts -D
Per default dts
will generate types for both src
and lib
because both folders are included in the project's .tsconfig
. This is why we need to pass one configuration parameter: include: ['lib']
.
// vite.config.ts
+import dts from 'vite-plugin-dts'
…
plugins: [
react(),
+ dts({ include: ['lib'] })
],
…
💡 It would also work to
exclude: ['src']
or use a different Typescript config file for building.
To test things out, let's add some actual code to your library. Open lib/main.ts
and export something, for example:
lib/main.ts
export function helloAnything(thing: string): string {
return `Hello ${thing}!`
}
Then run npm run build
to transpile your code. If the content of your dist
folder looks like below you should be all set 🥳:
📂dist
┣ 📜main.d.ts
┗ 📜my-component-library.js
💡 Don't be shy, open the files and see what the program did for you!
3. What is a React component library without components?
We didn't do all of this just to export a helloAnything
function. So let's add some meat 🍖 (or tofu 🌱 or both) to our library.
Let's go with three very common basic components: A button, a label, and a text input.
📂my-component-library
┣ 📂lib
+┃ ┣ 📂components
+┃ ┃ ┣ 📂Button
+┃ ┃ ┃ ┗ 📜index.tsx
+┃ ┃ ┣ 📂Input
+┃ ┃ ┃ ┗ 📜index.tsx
+┃ ┃ ┗ 📂Label
+┃ ┃ ┗ 📜index.tsx
┃ ┗ 📜main.ts
…
And a very basic implementation for these components:
// lib/components/Button/index.tsx
export function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
return <button {...props} />
}
// lib/components/Input/index.tsx
export function Input(props: React.InputHTMLAttributes<HTMLInputElement>) {
return <input {...props} />
}
// lib/components/Label/index.tsx
export function Label(props: React.LabelHTMLAttributes<HTMLLabelElement>) {
return <label {...props} />
}
Finally export the components from the library's main file:
// lib/main.ts
export { Button } from './components/Button'
export { Input } from './components/Input'
export { Label } from './components/Label'
If you npm run build
again you will notice that the transpiled file my-component-library.js
now has 78kb 😮
The implementation of the components above contains React JSX code and therefore react
(and react/jsx-runtime
) gets bundled as well.
As this library will be used in projects that have React installed anyways, you can externalize this dependencies to remove the code from bundle:
//vite.config.ts
build: {
…
+ rollupOptions: {
+ external: ['react', 'react/jsx-runtime'],
+ }
}
4. Add some styles
As mentioned in the beginning, this library will use CSS modules to style the components.
CSS modules are supported by Vite per default. All you have to do is to create CSS files that end with .module.css
.
📂my-component-library
┣ 📂lib
┃ ┣ 📂components
┃ ┃ ┣ 📂Button
┃ ┃ ┃ ┣ 📜index.tsx
+ ┃ ┃ ┃ ┗ 📜styles.module.css
┃ ┃ ┣ 📂Input
┃ ┃ ┃ ┣ 📜index.tsx
+ ┃ ┃ ┃ ┗ 📜styles.module.css
┃ ┃ ┗ 📂Label
┃ ┃ ┣ 📜index.tsx
+ ┃ ┃ ┗ 📜styles.module.css
┃ ┗ 📜main.ts
…
And add some basic CSS classes:
/* lib/components/Button/styles.module.css */
.button {
padding: 1rem;
}
/* lib/components/Input/styles.module.css */
.input {
padding: 1rem;
}
/* lib/components/Label/styles.module.css */
.label {
font-weight: bold;
}
And import/use them inside your components eg:
import styles from './styles.module.css'
export function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
const { className, ...restProps } = props
return <button className={`${className} ${styles.button}`} {...restProps} />
}
⛴️ Ship your style
After transpiling your library you will notice that there is a new file in your distribution folder:
📂dist
┣ …
┣ 📜my-component-library.js
+ ┗ 📜style.css
But there are two issues with this file:
- You need to manually import the file in the consuming application.
- It is one file that contains all styles for all components.
Import the CSS
CSS files just can't easily be imported in JavaScript. Therefore, the CSS file is generated separately, allowing the library user to decide how to handle the file.
But what if we were to assume that the application using the library has a bundler configuration that can handle CSS imports?
For this to work, the transpiled JavaScript bundle must contain an import statement for the CSS file. We are going to use yet another Vite plugin (vite-plugin-lib-inject-css) that does exactly what we need with zero configuration.
npm i vite-plugin-lib-inject-css -D
// vite.config.ts
+import { libInjectCss } from 'vite-plugin-lib-inject-css'
…
plugins: [
react(),
+ libInjectCss(),
dts({ include: ['lib'] })
],
…
Build the library and take a look at the top of your bundled JavaScript file (dist/my-component-library.js
):
// dist/my-component-library.js
import "./main.css";
…
💡 You may notice that the CSS filename has changed from style.css to main.css. This change occurs because the plugin generates a separate CSS file for each chunk, and in this case the name of the chunk comes from the filename of the entry file.
Split up the CSS
But there's still the second problem: when you import something from your library, main.css
is also imported and all the CSS styles end up in your application bundle. Even if you only import the button.
The libInjectCSS
plugin generates a separate CSS file for each chunk and includes an import statement at the beginning of each chunk's output file.
So if you split up the JavaScript code, you end up having separate CSS files that only get imported when the according JavaScript files are imported.
One way of doing this would be to turn every file into an Rollup entry point. And, it couldn't be better, there is a recommended way of doing this right in the Rollup documentation:
📘 If you want to convert a set of files to another format while maintaining the file structure and export signatures, the recommended way—instead of using output.preserveModules that may tree-shake exports as well as emit virtual files created by plugins—is to turn every file into an entry point.
So let's add this to your configuration.
First install glob
as it will be required:
npm i glob -D
Then change your Vite config to this:
// vite.config.ts
-import { resolve } from 'path'
+import { extname, relative, resolve } from 'path'
+import { fileURLToPath } from 'node:url'
+import { glob } from 'glob'
…
rollupOptions: {
external: ['react', 'react/jsx-runtime'],
+ input: Object.fromEntries(
+ glob.sync('lib/**/*.{ts,tsx}', {
+ ignore: ["lib/**/*.d.ts"],
+ }).map(file => [
+ // The name of the entry point
+ // lib/nested/foo.ts becomes nested/foo
+ relative(
+ 'lib',
+ 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))
+ ])
+ )
}
…
💡 The glob library helps you to specify a set of filenames. In this case it selects all files ending with
.ts
or.tsx
and ignores*.d.ts
files Glob Wikipedia
Now you end up with a bunch of JavaScript and CSS files in the root of your dist
folder. It works, but it doesn't look particularly pretty, does it?
// vite.config.ts
rollupOptions: {
…
+ output: {
+ assetFileNames: 'assets/[name][extname]',
+ entryFileNames: '[name].js',
+ }
}
…
Transpile the library again and all JavaScript files should now be in the same organized folder structure you have created in lib
alongside with their type definitions. And the CSS files are inside a new folder called assets.
Transpile the library again and all JavaScript files should now be in the same organized folder structure that you created in lib
along with their types. And the CSS files are in a new folder called "assets". 🙌
Notice that the name of the main file has changed from "my-component-library.js" to "main.js". That's great!
4. A few last steps before you can publish the package
Your build setup is now ready, there are just a few things to consider before releasing your package.
The package.json
file will get published along with your package files. And you need to make sure it contains all important information about the package.
Main file
Every npm package has a primary entry point, per default this file is index.js
in the root of the package.
Your library's primary entry point is now located at dist/main.js
, so this needs to be set in your package.json
. The same applies to the type's entry point: dist/main.d.ts
// package.json
{
"name": "my-component-library",
"private": true,
"version": "0.0.0",
"type": "module",
+ "main": "dist/main.js",
+ "types": "dist/main.d.ts",
…
Define the files to publish
You should also define which files should be packed into your distributed package.
// package.json
…
"main": "dist/main.js",
"types": "dist/main.d.ts",
+ "files": [
+ "dist"
+ ],
…
💡 Certain files like
package.json
orREADME
are always included, regardless of settings: Read the docs
Dependencies
Now take a look at your dependencies
: right now there should be only two react
and react-dom
and a couple of devDependencies
.
You can move those two to the devDepedencies
as well. And additionally add them as peerDependencies
so the consuming application is aware that it must have React installed to use this package.
// package.json
- "dependencies": {
+ "peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
…
}
💡 See this StackOverflow answer to learn more about the different types of dependencies: Link
Side effects
To prevent the CSS files from being accidentally removed by the consumer's tree-shaking efforts, you should also specify the generated CSS as side effects:
// package.json
+ "sideEffects": [
+ "**/*.css"
+ ],
You can read more about sideEffects
in the webpack docs. (Originally from Webpack, this field has developed into a common pattern that is now also supported by other bundlers)
Ensure that the package is built
You can use the special lifecycle script prepublishOnly
to guarantee that your changes are always built before the package is published:
// package.json
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
…
+ "prepublishOnly": "npm run build"
},
5. Demo page and deployment
To just play around with your components on the demo page, you can simply import the components directly from the root of your project. This works because your package.json
points to the transpiled main file dist/main.ts
.
src/App.tsx
…
import { Button, Label, Input } from '../';
…
To publish your package, you just need to run npm publish
. If you want to release your package to the public, you have to set private: false
in your package.json
.
You can read more about publishing your package, including installing it in a local project (without publishing) in these articles of mine:
FAQs
Can I use vanilla extract instead of CSS Modules?
Here is a branch that uses vanilla extract: https://github.com/receter/my-component-library/tree/vanilla-extract
To be able to still test your library with npm run dev
it was necessary to add an ignore
for "lib/**/*.css.ts"
in vite.config.ts
to avoid that the vanillaExtractPlugin()
acts on the compiled files.
I have an issues with the latest version of create vite
I did not yet update this article, following this guide you might run into some issues with the latest version of create vite
. I did however create a branch with some modifications to work with vite@5.4.4:
https://github.com/receter/my-component-library/tree/revision-1
Can I remove the CSS imports from the output?
Yes, you can easily remove the vite-plugin-lib-inject-css
plugin (and subsequential the sideEffects
from your package.json
)
Having done that you will get one compiled stylesheet containing all required classes in dist/assets/style.css
. Import/use this stylesheet in your application and you should be good to go.
You will of course loose the CSS treeshaking feature which is made possible by importing only the required CSS inside each component.
I published a branch demonstrating this change here: https://github.com/receter/my-component-library/tree/no-css-injection
Does this work with Next.js?
Importing CSS from external npm packages works since Next.js 13.4:
https://github.com/vercel/next.js/discussions/27953#discussioncomment-5831478
If you use an older version of Next.js you can install next-transpile-modules
Here is a Next.js demo repo: https://github.com/receter/my-nextjs-component-library-consumer
Error: Cannot find module 'ajv/dist/core'
This error will happen if you are using vite-plugin-dts@4
in combination with --legacy-peer-deps
. The solution is to install ajv@8
manually or stop using --legacy-peer-deps
.
npm i ajv@8
https://github.com/qmhc/vite-plugin-dts/issues/388
How to use Storybook for my library?
To install Storybook run npx storybook@latest init
and start adding your stories.
If you add stories inside the lib
folder you also need to make sure to exclude all .stories.tsx
files from the glob pattern so the stories don't end up in your bundle.
glob.sync('lib/**/*.{ts,tsx}', { ignore: 'lib/**/*.stories.tsx'})
I have published a branch with Storybook here: https://github.com/receter/my-component-library/tree/storybook
To be able to build Storybook you need to disable the libInjectCss
plugin. Otherwise you will run into an TypeError: Cannot convert undefined or null to object
error when running npm run build-storybook
(Thanks @codalf for figuring that out!)
Update 26.03.2024: This issue (#15) with vite-plugin-lib-inject-css
and has been fixed in version 2.0.0
and the fix is not needed anymore.
Thanks for reading!
If you did not follow along or something wasn't that clear, you can find the full source code with working examples on my GitHub Profile:
- https://github.com/receter/my-component-library
- https://github.com/receter/my-component-library/tree/revision-1 (Revision that works with latest Vite version)
- https://github.com/receter/my-component-library-consumer
- https://www.npmjs.com/package/@receter/my-component-library
Fingers crossed you found it helpful, and I'm all ears for any thoughts you'd like to share.
Latest comments (163)
Thanks bro! You've just saved my life. I got a task at work to implement UI library and struggled for a couple of days until encountered your solution. Liked and subscribed!
I'm glad that helped you, let me know how it goes! I am currently working on a library to help creating component libraries, but it is very much experimental and work in progress. If you want to take a look for inspiration, you can find it here: github.com/receter/sys42/
hi,I find this article extremely useful. I've translated it into Chinese to help fellow developers better understand it in a localized context.
如果你正在管理多个 React 应用,并希望在ui保持一致,迟早你会发现需要一个组件库。
当我第一次想要创建一个 React 组件库时,花了很多时间 才找到一个满足我所有要求 且不太复杂的设置。
本篇指南可以为我节省大量与这些东西搏斗的精力,我也希望它能帮助到你。
此文章涵盖了 React 组件库 的编写和发布,包括配置构建过程和将包发布到 npm!。
我已经尽力保持所有配置简单明了,尽可能使用默认设置。
完成后,你可以像安装任何其他 npm 包一样安装你的库:
并像这样使用它:
开始之前
在深入实现细节之前,我想详细说明一些关于库设置的技术细节。
🌳 Tree shaking
对我来说,特别重要的一点是:“最终的应用程序里只包含真正必要的代码”。当你导入一个组件时,它只包含必要的 JS 和 CSS 样式,干净利落,很酷吧?
🦑 Css Modules
组件使用 CSS Modules 来写样式。在打包成库时,它们会被编译成普通 CSS 文件,因此使用方完全不需要支持 CSS Modules。
额外的好处是:把 CSS Modules 提前编译后,就绕过了兼容性问题,既可直接引入这个包,零配置。
🧁 如果你想用 vanilla-extract 而不是 CSS Modules,在文章底部有对应的示例,可以参考。
😎 TypeScript
虽然这个库是用 TypeScript 写的,但普通 JavaScript 项目也能无缝使用。
如果你还没试过 TypeScript,不妨尝试下Ts:它不仅能逼你写更干净的代码,还能让你的 AI 小助手给出更好的建议 😉
好了,读够了,现在让我们开始享受乐趣吧!
1.新建 Vite 项目
如果你从未用过 Vite,可以将其视为 Create React App 的替代品。只需几个命令,你就可以开始了。
就是这样,你的新 Vite/React 项目已经准备就绪。
这里有两件我建议你在安装 Vite 后立即做的事情。
2. 基本构建设置
现在你可以运行 npm run dev 并访问 Vite 提供的 URL。
在开发库时,这个环境可以轻松导入你的库并实时查看组件效果(请将 src 内的内容均视为演示页面 example or demo)。
而咱们的库的代码将存放在另一个文件夹中,比如这里我们创建一个名为 lib 的文件夹(也可选用其它名称,但 lib 是个大家都常用的的选择)。
库的主要入口点将是
lib
内名为main.ts
的文件。安装库时,你可以导入从此文件导出的所有内容。Vite 库模式
当前,如果您运行
npm run build
,Vite 会默认将src
目录下的代码构建并输出到dist
文件夹。这是 Vite 的标准行为。不过,目前我们
src
目录中的内容仅用于开发环境下启动和演示,因此并没有被构建的必要。相反,lib
目录中的代码才是我们真正需要构建、编译并发布(到 npm) 的部分!这正是 Vite 库模式 发挥作用的地方。该模式专为构建库(Library)而设计。要激活此模式,只需在
vite.config.ts
配置文件中指定您的库入口点即可。📘 Library 模式文档
📘 lib 模式文档
TypeScript 和库模式
Vite 创建的
tsconfig.json
只包含src
文件夹。要为你新创建的lib
文件夹也启用 Ts,你需要将其添加到 Ts 配置文件中虽然需要为
src
和lib
文件夹都启用 Ts,但在构建库时最好不包含src
。为确保在构建过程中只包含
lib
目录,你可以专门为构建创建一个单独的 Ts 配置文件。唯一的区别是 📜tsconfig.lib.json 中的构建配置只包含
lib
目录,而默认配置包含lib
和src
要使用
tsconfig.lib.json
进行构建,你需要在 package.json 的构建脚本中将配置文件传递给tsc
:最后,你还需要将文件
vite-env.d.ts
从src
复制到lib
。没有这个文件,Ts 在构建时会错过 Vite 提供的一些类型定义(因为我们不再包含src
)。现在你可以再次执行
npm run build
,这是你将在 dist 文件夹中看到的内容:文件
vite.svg
在你的dist
文件夹中,因为 Vite 将public
目录中的所有文件复制到输出文件夹。让我们禁用此行为:构建类型
由于这是一个 Ts 库,你还希望随包一起发布类型定义。幸运的是,有一个 Vite 插件可以做到这一点:vite-plugin-dts
默认情况下,
dts
会为src
和lib
生成类型,因为两个文件夹都包含在项目的.tsconfig
中。这就是为什么我们需要传递一个配置参数:include: ['lib']
。为了测试,让我们向你的库添加一些实际代码。打开
lib/main.ts
并导出一些内容,例如:然后运行
npm run build
来转译你的代码。如果你的dist
文件夹的内容如下所示,你应该已经准备就绪 🥳:3. 没有组件的 React 组件库算什么?
我们做这一切可不仅仅只是为了导出一个
helloAnything
函数😔,所以让我们为我们的库添加一些有意义实质内容:让我们使用三个非常常见的基本组件--按钮、标签和输入框。以及这些组件的非常基本的实现:
最后从库的主文件中导出组件:
如果你再次运行
npm run build
,你会注意到编译后的文件my-component-library.js
现在有 78kb 😮上面组件的实现包含 React JSX 代码,因此
react
(和react/jsx-runtime
)也被打包了。不过由于这个库 将会在已经安装了 React 的项目中使用,你可以将这些依赖项外部化(即你的react项目已经包含了react运行时库),以从包中移除代码:
4. 添加一些样式
如开头所述,这个库将使用 Css Module 来为组件设置样式。
Vite 默认支持 Css Module,你所要做的就是创建以
.module.css
结尾的 CSS 文件即可。并添加一些基本的 CSS 类:
然后在你的组件中导入/使用它们,例如:
⛴️ 发布你的样式
转译库后,你会注意到分发文件夹中有一个新文件:
但这个文件有两个问题:
怎么办?请接着往下看 👇
导入 CSS
由于 CSS 文件无法直接在 JavaScript 中轻松导入,因此需要单独生成 CSS 文件,让库的使用者自行决定如何处理该文件。
但但如果我们假设:“使用该库的应用程序已经配置了能处理 CSS 导入的打包器配置”会怎样?
要实现这种处理方式,就需要要求我们编译后 Js 包必须包含 CSS 文件的导入语句,这里我们将使用另一个 Vite 插件 👉 vite-plugin-lib-inject-css 来达到这个效果,且零配置。
构建库 并 查看打包的 Js 文件(
dist/my-component-library.js
)的顶部,你会发现 OBJK成了😊!拆分 CSS
接下来我们解决第二个问题:当你从库中导入某些内容时,
main.css
也会被导入,所有 CSS 样式最终都会出现在你的应用程序中,即使你只导入Button
这个组件。好在
libInjectCSS 插件
会为每个组件代码块生成独立的 CSS 文件,并在每个代码块输出文件的开头添加对应的导入语句。因此,如果您对 Js 代码进行拆分,就将会得到独立的 CSS 文件——且这些样式文件只会在对应的 JavaScript 文件被导入时才会同步加载,这就是解决方案!
实现此功能的一种方法是将每个文件都转换为 Rollup 的入口点。更赞的是,Rollup 文档中正好推荐了这种实现方式:
所以让我们将此添加到你的配置中。
首先安装
glob
,因为它是必需的:然后将你的 Vite 配置更改为:
现在你的
dist
文件夹根目录中会有一堆 Js 和 CSS 文件。它可以工作,但看起来不是特别美观,不是吗?重新打包lib,你会发现所有 Js 文件现在应该与它们的类型定义一起位于你在
lib
中创建的相同有组织的文件夹结构中。CSS 文件位于名为 assets 的新文件夹内。🙌注意 主文件的名称已从"my-component-library.js"更改为"main.js",真棒👍!
4. 发布包之前的最后几个步骤
你的构建设置现在已经准备就绪,在发布包之前只需要考虑几件事。
package.json
文件将与你的包文件一起发布。你需要确保它包含有关包的所有重要信息。主文件
每个 npm 包都有一个主要入口点,默认情况下这个文件是包根目录中的
index.js
。你的库的主要入口点现在位于
dist/main.js
,因此需要在你的package.json
中设置。类型的入口点也是如此:dist/main.d.ts
定义要发布的文件
你还应该定义哪些文件应该打包到你的分发包中。
依赖项
现在看看你的
dependencies
:现在应该只有两个react
和react-dom
以及一些devDependencies
。你也可以将这两个移动到
devDependencies
中。并且另外将它们添加为peerDependencies
,以便使用应用程序知道它必须安装 React 才能使用此包。副作用
为了防止 CSS 文件被消费者的 tree-shaking 工作意外删除,你还应该将生成的 CSS 指定为副作用:
你可以在 webpack 文档中阅读有关
sideEffects
的更多信息。(最初来自 Webpack,此字段已发展为现在也受其他打包器支持的通用模式)确保包已构建
你可以使用特殊的生命周期脚本
prepublishOnly
来保证在发布包之前始终构建你的更改:5. 演示页面和部署
要在演示页面上使用你的组件,你可以简单地直接从项目根目录导入组件。这是可行的,因为你的
package.json
指向转译后的主文件dist/main.ts
。要发布你的包,你只需要运行
npm publish
。如果你想将包发布给公众,你必须在package.json
中设置private: false
。你可以在我的这些文章中阅读有关发布包的更多信息,包括在本地项目中安装它(不发布):
发布和安装你的包
利用 GitHub actions自动构建和发布
good!
Thank you so much for this article! I was trying to avoid CSS-in-JS since I use CSS modules heavily. The only tweaks I needed (and commented on below) are fixing the
dts
configuration:There is a branch "revision-1" where I did a similar thing. I still need to update the article based on this branch.
You can create a
tsconfig.lib.json
:tsconfig.lib.json
:Then remove the
include: ['lib'],
from the dts configuration and adapt also the build script to"build": "tsc -b ./tsconfig.lib.json && vite build",
This way the config is more concise.Why did you want/need to set
rollupTypes: true
?Thanks, it helped me lot.
The types are not getting exported correctly. Have created a stackoverflow issue. Any help is much appreciated. Thanks.
Do you have a link to the Stackoverflow issue?
Pls, write the new tutorial with vanilla-extract. I am looking for that one.
Here is a branch with vanilla-extract: github.com/receter/my-component-li...
I heard you :)
Creating and Publishing React Npm Packages simply using tsup medium.com/@sundargautam2022/creat...
Looks interesting, did you get any experience with TSUP and CSS code splitting? One of my requirements was that not all CSS is bundled, but only the CSS of components that are imported.
Thank for a great article, it's really helpful.
Have you successfully added vanilla extract to this project?
Here is a branch with vanilla-extract: github.com/receter/my-component-li...
I am stuck with CSS Modules. Still I like the idea of vanilla extract. I think it should just work, but yeah I should try it :)
After building my component library using your guide, it works perfectly. However, when running vitest I get an error saying the .css files are unable to be located. Is this expected? I have tried configuring my vitest settings, but I feel like the workaround I have produced is better solved for this library itself. (The components render and work fine in my project, but the tests that use anything from the component library have a .css file error)
I think a few others in this thread have experienced this issue
Hm, do you test the original files or the transpiled code?
If you have a public example repo I can take a look at your issue.