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.
(In the future I want to extend this tutorial to use vanilla-extract instead.)
😎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
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
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.
Top comments (140)
Please note that this will not work with Next.js. You cannot provide CSS import in JS for SSR.
This is the issue you will face: nextjs.org/docs/messages/css-npm
For Next.js the solution would be to import the CSS on Next's side so that it can be loaded in an appropriate location. So you would need to make boilerplate file for every entry in a Next app so that the CSS would be only loaded when the component is going to be used to get the benefit of only loading what is needed.
Hi Vesa,
this is very interesting, thank you! I did not yet try to use this approach within a Next.js project.
You can also remove the inject-css plugin (and the
sideEffects
from thepackage.json
)Having done that you should be able to just import the generated css file (
dist/assets/style.css
) inside your Next.js project. But of course you will not have the css treeshaking advantages with this approach.I published a branch with this approach here: github.com/receter/my-component-li...
Generally there are answers for the quetions raised in the linked issue:
As Global CSS.
The order of the individual files is determined by the order they are imported inside the libraries main file.
And the order in the consuming application should not matter.Maybe it would be possible to write a Next.js plugin enabling this.
What do you think?
You are incorrect about the order in consuming application not mattering. Because people tend to write overrides. Or they may use things from multiple sources and mix their use together. And then loading order, or order of the styles as they exist in the document, does matter.
For an example, in my past experience Styled Components was notorious for becoming a true troublemaker with style issues. Because if you had multiple different components from different libraries + your own local Styled Components + SC was primarily designed for the React SPA world, but we entered the "SSR is a better idea" era, and then you would just end up having awfully hard to debug style issues. CJS to ESM transition also caused it's own share of issues.
And Styled Components is all about style injection. I think they've now got it somewhat in order with their v6 release, but we're of course on our way away from Styled Components as runtime CSS-in-JS is fundamentally bad for performance.
Then there are even issues with CSS Modules in Vite. Vite does not keep the modules on their own but instead mashes
composes
into the consuming output file. So this means the same class name definition can be loaded again later, and if you do any property overrides then BOOM now you have issues that flicker depending on which order the CSS is loaded. So you just have to know not to ever write property overrides when using CSS Modules.Anyway, I think doing any sort of
import './style.css'
orinjectStyles
will always be fundamentally wrong. You must be able to control the loading order of the CSS and the only thing that can reliably do that is the consuming app / framework.Yes you are absolutely right about overrides. If you assign classes to the exported components it makes a difference if these classes come before or after the classes provided by the component library.
In this case the component library styles have to be imported before to have a lower specificity. And this is only guaranteed if the library is imported before any other styles.
As you correctly mention, the consuming application is responsible for ensuring the correct order of css.
You should be able to ensure this if:
Do you have more info like a github issue on this?
Can you ellaborate on this?
I guess the issue answers the latter question: github.com/vitejs/vite/issues/7504
Thanks for the link!
The reason I am working on this topic and wrote the article is that I am trying to find the best solution to build a component library. I don't like CSS in JS that much and I am convinced that a stylesheet based approach is the way I want to go. I will think about the style ordering and might publish another article on this soon.
I wrote you on LinkedIn, if you are interested in having a discussion about this topic I would be more then happy to speak/write to you.
The advantage of handling the style imports inside the library is (obviously) that you don't need to manually import styles. This is not a big issue if it is just one stylesheet for a library. But if you only want to import the styles for components you actually use I see no other really satisfying solution.
I do have some rough ideas though…
Yeah, the more you want to provide benefits to the user (= well splitting code, tree shaking, only styles you need) the nastier the management comes for the component library consumer.
We are in transition to CSS Modules based component library at work and the design of that is basically a bunch of
createXYZ()
functions with the sole role of passing in the CSS Modules as loaded by the consuming app. It could be a little better by instead just creating the components and making them use a hook that would provide the styles from context, and then have a context provider. Although I guess then you'd end up with the issue that all the CSS would be loaded at once.One reason for doing things like that is we also use some of the very same CSS Modules directly. It does provide benefits as you can choose to work without a component just using the styles to classes, use composes to extend in a local module, get unified breakpoints for both CSS and JS, or use the convenient components when you don't need that much power (although our main layout component and text component are both very versatile).
The problem I have is that only our team really needs the full intelligent splitting. Other teams work on what are more like in-house SPAs, so for them there is a friction in moving from Styled Components to CSS Modules. They don't like the boilerplate and they don't need to consider app boot times. So this is one reason I found your article.
I have the feeling that a fundamental problem is that the order of CSS is defined by the order it is imported in javascript. Which is kind of by chance because sometimes a component is imported earlier and sometimes later.
There are also not so easy to fix problems with dynamic importing github.com/vitejs/vite/issues/3924
I created a branch with a very simple example that demonstrates the order issue if anyone is interested in an example: github.com/receter/my-component-li...
What I did not expect: When importing ANY component from the library, all other imported CSS, even for components imported later on, will be at the same position.
I just created a new nextjs project and it seems to work quite fine.
Here is the my repo: github.com/receter/my-nextjs-compo...
What version of nextjs were you using? I would like to reproduce.
Also can you try if this works for you:
In
next.config.js
:Looks like it works since Next.js 13.4:
github.com/vercel/next.js/discussi...
github.com/vercel/next.js/discussi...
Hi, Vesa,
I'm not having this problem with Next. The doc you provided says that this issue occurs when the source files are consumed instead of the build files. Are there other circumstances that produce this problem that we should watch out for?
My steps, in a monorepo with NPM workspaces:
packages
).apps
).page.tsx
. This file doesn't contain the 'use client' directive, so I believe it's SSR.Results:
Question:
Are there cases in which we would consume the build files and still encounter this issue?
I added a faq section about using Storybook: dev.to/receter/how-to-create-a-rea...
dev.to/receter/comment/2ag7m
I don't have experience of having a library as a part of a monorepo setup, but I would guess it will be handled more like a part of the local project, not as an external dependency.
The problem exists when you release the built library as a npm package and then try to import it, so it will be within
node_modules
.The guide discusses essential technical details, such as TypeScript integration, CSS modules, tree shaking, and peer dependencies. It covers these aspects while keeping the content concise and understandable.
Great article really helpfull! 🥳
Keep up the awesome work 💪!
But my .stories files gets included in the bundle.
I tried to exluded them by extending the glob.sync:
glob.sync('stories/**/*{!(*.stories).ts,!(*.stories).tsx,!(*.stories).js,!(*.stories).jsx}')
But it did not work.. 🤔
Any idea why this is happening?
Thx!
Edit:
Fixed it, I was stupid and included the * in the begining:
glob.sync('stories/**/{!(*.stories).ts,!(*.stories).tsx,!(*.stories).js,!(*.stories).jsx}')
But it still includes the stories.d.ts... 😞
Edit 2:
Sorry I'm asking to fast 😅, working glob that exclude stories files:
'stories/**/{!(*.stories|*.stories.d).ts,!(*.stories).tsx,!(*.stories).js,!(*.stories).jsx}'
Edit 3:
I initially thought it worked. But stories.d.ts still included.. :/
I was successful with this:
glob.sync('lib/**/*.{ts,tsx}', { ignore: 'lib/**/*.stories.tsx'})
Thx man!
That worked great 🤩!
I have added a FAQ section about using Storybook: dev.to/receter/how-to-create-a-rea...
Hi Js4,
why do you have your stories in the library? I think it would be better to place them outside the
lib
folder. Then you shouldn't have to exclude them.Andreas, thank you for sharing such a well thought out and detailed explanation of how to set up a component library. This subject isn't easy, but you did a great job explaining everything. I took inspiration from your setup and expanded it to include support for
esm
andcjs
bundles in addition to subpath exports setup for explicit import references for anyone interested. You can find it here.Awesome, thanks for sharing!
Not sure why you want a cjs version that imports css files. Do you have a use case where a bundler can import css files but not esm? If not it wouldn't make sense to me.
Good question, this was more about compatibility between our legacy and newer codebases. The goal was to implement a bundler config that would operate for both situations so that when we do migrate to a fully esm system, the conversion would be painless. Probably not necessary in most cases considering cjs doesn’t gain any tree shaking benefits typically, but an activity I enjoyed doing to see if I could get both esm and cjs bundle outputs working.
Hy thank you so much for the guide. I do have a question though.
if in my library i'd like to achieve an import like: d
how can i do it.
do i have to pass an array of entries like?
my structors is
lib/assest
lib/hooks
etc
So sorry for the dumb question
Hi Federico,
You should be able to do that with subpath exports.
First create a file
assets.ts
alongsidemain.ts
and export what you want:Then in
package.json
you can define the exports like so:You can read more about this here: nodejs.org/api/packages.html
Let me know if this works for you.
For multiple entry points like this, do we need to update vite config lib.entry as well as the rollupOptions.output?
Yes, either that or you could also do multiple build steps. Like a different vite config file for each export and then run
vite --config ./vite-build-main.config.ts build && vite --config ./vite-build-assets.config.ts build
for example.Thank you so much @receter. I do have another questions. I'm using this set(more or less) for my RN lib.
I having trouble with react-native-reanimated library. I created my animated Comp in the library and set reanimated as a external lib and peer, i can build without problems my lib but then when i try to import my comp the app crash. I don't get the problem to be honest. Do you have any suggest? thank you in advance
Hi Frederico,
do you get any kind of error message? If you can provide a repo where I can reproduce your issue, I am happy to help you find it.
That's my biggest problem, i don't get an error. the app just crash. i gonna create a simple repo and share it with you. Really appreciate your help
@receter i'll add here the link for the repo example
👍 Let me know when it is ready!
Hey, i added in the comment the link for the lib repo. do you need also a simple repo app example? Thank you so much
@receter Sorry for the ping but maybe you didn't get a notification
I did, will do that today.
Just finished writing a guide to automatically publish packages if your are interested. And I will now read the code in your repo 😉
@federbeije I opened an issue in your repo, we can continue to discuss there.
GREAT post. I was struggling with this. I especially overlooked
react/jsx-runtime
as an external dep as was getting frustrated why it was being included in the build. So you saved me a fair bit of time there.The CSS splitting I would have figured out.... eventually. But after a lot of frustration and maybe hours of searching. So, again, big thanks. Followed.
Hi Ben, very glad I saved you some time and thanks for letting me know. That was the main reason for me to write this article. It took me some time and frustration to get it all working and I wanted to share my learnings so that others have a better starting point.
If you find anything that can be improved or needs to be updated let me know!
Hi Andreas, thank you for this really useful post.
I wanted to comment that I am having an issue when compiling, on the dist folder, instead of getting "main.js", "main.d.ts", and "components", I am getting this structure:
Here, under "dist/components/Button" for example, I have the index.js with the compiled code. And under "dist/lib/components/Button" I have the index.d.ts with the declarations
So I have to do nested imports to get to the correct main.d.ts file and that isn't ideal.
Do you have any thoughts on what may be causing this problem? thanks in advance
It was due to me importing files from outside lib
good to know, glad you found the issue
Hi Andreas,
thanks for excellent article and sample code! I followed your entire article and everything works perfectly then I tried npm link on the project root and ran npm link @username/my-component-library on a new project that uses the library. On this project I tried to debug the Button component (with devTools of Chrome) but I only see the compiled code (dist/components/Button index.js). Is it possible to view the source code in debug?
Thanks :)
You're welcome, I'm glad you like the article. First, if you run
npm run dev
you can start the dev server and debug your components locally. Further you can install something like react-cosmos or storybook locally to test and debug your components.If you really need/want to install your package with
npm link
to test it in another project you can try if building with a sourcemap solves your problem: vitejs.dev/config/build-options#bu...Let me know if that helps you, cheers!
Hello!
I found and issue with the tsconfig files in the latest vite version (5.4 at the time of this comment). While previous versions of create vite would generate a tsconfig.json and a tsconfig.node.json, the latest version creates an additional tsconfig.app.json making it no longer possible to make the steps in this tutorial work by extending any of these files in the tsconfig-build.json. What solved the issue for me was deleting all three files and copying tsconfig.json and tsconfig.node.json from an older vite project. Hope this helps!
I created a new
tsconfig.build.json
file and extended tsconfig.app.json, seems to work fine:{
"extends": "./tsconfig.app.json",
"include": ["lib"]
}
Also, with the new vite version with tsconfig.app.json, for building type definitions I had to add tsconfigPath:
dts({ include: ['lib'], rollupTypes: true, tsconfigPath: 'tsconfig.app.json' }),
I would suggest this:
Do not include the
lib
folder intsconfig.app.json
, instead create atsconfig.lib.json
like so:Then add
tsconfig.lib.json
to thereferences
array in yourtsconfig.json
.And set the build script to:
I made a branch with the latest vite version (5.4.4 at the time of writing): github.com/receter/my-component-li...
Here is a summary of what I had to change for the building the types:
update to vite-plugin-dts@4
add tsconfig path to dts config
This outputs types to a
lib
subfolder in dist. So you need to update your package.json to point to the correct type entry: "./dist/lib/main.d.ts" (or you could use rollupTypes: true)Also I manually had to install
ajv
because of an issue: stackoverflow.com/questions/787352...In this branch I also use the new package.json
exports
feature and "self reference" the library in App.tsx.I will try to find time to update my article accordingly, but for now this branch might help you.
Thank you, that really helped me ❤️