DEV Community

Cover image for Create a Component Library Fast🚀(using Vite's library mode)

Create a Component Library Fast🚀(using Vite's library mode)

Andreas Riedmüller on August 10, 2023

If you are managing multiple React applications and want consistency across your user interfaces, sooner or later you'll find that you need a compo...
Collapse
 
merri profile image
Vesa Piittinen

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.

Collapse
 
receter profile image
Andreas Riedmüller • Edited

Update
Importing CSS from external npm packages works since Next.js 13.4:
github.com/vercel/next.js/discussi...


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 the package.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:

Should the file be consumed as Global CSS or CSS Modules?

As Global CSS.

If Global, in what order does the file need to be injected?

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?

Collapse
 
merri profile image
Vesa Piittinen • Edited

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' or injectStyles 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.

Thread Thread
 
receter profile image
Andreas Riedmüller • Edited

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:

  • Your libraries CSS is imported before any other local js or css
  • You never assign a CSS class imported from a js library to a component
  • Only one component library is allowed to influence global styles
Thread Thread
 
receter profile image
Andreas Riedmüller

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.

Do you have more info like a github issue on this?

So you just have to know not to ever write property overrides when using CSS Modules.

Can you ellaborate on this?

Thread Thread
 
merri profile image
Vesa Piittinen

I guess the issue answers the latter question: github.com/vitejs/vite/issues/7504

Thread Thread
 
receter profile image
Andreas Riedmüller

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.

Thread Thread
 
receter profile image
Andreas Riedmüller

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…

Thread Thread
 
merri profile image
Vesa Piittinen

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.

Thread Thread
 
receter profile image
Andreas Riedmüller

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.

Collapse
 
receter profile image
Andreas Riedmüller

Looks like it works since Next.js 13.4:

github.com/vercel/next.js/discussi...

Following up on this: the App Router is now stable with 13.4!
CSS files can now be imported inside any layout or page, including styles from external npm packages.
nextjs.org/blog/next-13-4

github.com/vercel/next.js/discussi...

Collapse
 
receter profile image
Andreas Riedmüller • Edited

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:

+module.exports = {
+  transpilePackages: ['awesome_module'],
+};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jamestbaker profile image
James Baker

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:

  1. Followed instructions here (in a directory inside packages).
  2. Installed Next 13.5.5 (in a directory inside apps).
  3. Added a very simple component from the custom library to Next's page.tsx. This file doesn't contain the 'use client' directive, so I believe it's SSR.
  4. Tested locally only — didn't deploy.

Results:

  • The admittedly simplistic component renders as expected.
  • No messages in the Terminal or the browser console.
  • (Storybook set up in the component library also renders the component as expected.)

Question:
Are there cases in which we would consume the build files and still encounter this issue?

Collapse
 
receter profile image
Andreas Riedmüller

I added a faq section about using Storybook: dev.to/receter/how-to-create-a-rea...

Collapse
 
receter profile image
Andreas Riedmüller
Collapse
 
merri profile image
Vesa Piittinen

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.

Collapse
 
coderollercoaster profile image
Felix Turner

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.

Collapse
 
j4v4scr1pt profile image
Js4 • Edited

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.. :/

Collapse
 
codalf profile image
Peter Kullmann

I was successful with this:
glob.sync('lib/**/*.{ts,tsx}', { ignore: 'lib/**/*.stories.tsx'})

Collapse
 
j4v4scr1pt profile image
Js4

Thx man!

That worked great 🤩!

Thread Thread
 
receter profile image
Andreas Riedmüller

I have added a FAQ section about using Storybook: dev.to/receter/how-to-create-a-rea...

Collapse
 
receter profile image
Andreas Riedmüller

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.

Collapse
 
waldronmatt profile image
Matthew Waldron • Edited

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 and cjs bundles in addition to subpath exports setup for explicit import references for anyone interested. You can find it here.

Collapse
 
receter profile image
Andreas Riedmüller

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.

Collapse
 
waldronmatt profile image
Matthew Waldron

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.

Collapse
 
federbeije profile image
FedericoReghiniBeije

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

import { bau } from 'mylib/assets
import {wow} from 'mylib/hooks
Enter fullscreen mode Exit fullscreen mode

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

Collapse
 
receter profile image
Andreas Riedmüller • Edited

Hi Federico,

You should be able to do that with subpath exports.
First create a file assets.ts alongside main.ts and export what you want:

// lib/assets.ts
export const bau = ":-)";
Enter fullscreen mode Exit fullscreen mode

Then in package.json you can define the exports like so:

{
…
-  "main": "dist/main.js",
-  "types": "dist/main.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/main.d.ts",
+      "default": "./dist/main.js",
+    },
+    "./assets": {
+      "types": "./dist/assets.d.ts",
+      "default": "./dist/assets.js",
+    },
…
}
Enter fullscreen mode Exit fullscreen mode

The "exports" provides a modern alternative to "main" allowing multiple entry points to be defined, conditional entry resolution support between environments, and preventing any other entry points besides those defined in "exports". This encapsulation allows module authors to clearly define the public interface for their package.
⚠️ When the "exports" field is defined, all subpaths of the package are encapsulated and no longer available to importers. For example, require('pkg/subpath.js') throws an ERR_PACKAGE_PATH_NOT_EXPORTED error.

You can read more about this here: nodejs.org/api/packages.html

Let me know if this works for you.

Collapse
 
dislersd profile image
Dylan Dislers

For multiple entry points like this, do we need to update vite config lib.entry as well as the rollupOptions.output?

Thread Thread
 
receter profile image
Andreas Riedmüller

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.

Collapse
 
federbeije profile image
FedericoReghiniBeije

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

Thread Thread
 
receter profile image
Andreas Riedmüller

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.

Thread Thread
 
federbeije profile image
FedericoReghiniBeije • Edited

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

Thread Thread
 
receter profile image
Andreas Riedmüller

👍 Let me know when it is ready!

Thread Thread
 
federbeije profile image
FedericoReghiniBeije

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

Thread Thread
 
federbeije profile image
FedericoReghiniBeije

@receter Sorry for the ping but maybe you didn't get a notification

Thread Thread
 
receter profile image
Andreas Riedmüller

I did, will do that today.

Thread Thread
 
receter profile image
Andreas Riedmüller

Just finished writing a guide to automatically publish packages if your are interested. And I will now read the code in your repo 😉

Thread Thread
 
receter profile image
Andreas Riedmüller

@federbeije I opened an issue in your repo, we can continue to discuss there.

Collapse
 
bnl profile image
Ben • Edited

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.

Collapse
 
receter profile image
Andreas Riedmüller

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!

Collapse
 
tomassadone profile image
Tomas Sadone • Edited

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:
Image description

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

Collapse
 
tomassadone profile image
Tomas Sadone

It was due to me importing files from outside lib

Collapse
 
receter profile image
Andreas Riedmüller

good to know, glad you found the issue

Collapse
 
pascalp profile image
Pascal

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 :)

Collapse
 
receter profile image
Andreas Riedmüller

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!

Collapse
 
tombohub profile image
tombohub

Thank you, that really helped me ❤️

Collapse
 
codalf profile image
Peter Kullmann • Edited

Really great tutorial! Thanks!
I'd like to add StoryBook to the component library. Is this straight-forward or is there anything special I have to consider?

I used npx storybook@latest init to install Storybook, which seemed to work fine. I can start Storybook in dev mode with npm run storybook, however npm run build-storybook fails with an error:

=> Failed to build the preview
TypeError: Cannot convert undefined or null to object
at Function.values (<anonymous>)
at configResolved (file:///C:/prj/react/cp-web-react-components/node_modules/vite-plugin-dts/dist/index.mjs:582:100)
`
I looks like it has to do with the modified vite setup...

Collapse
 
receter profile image
Andreas Riedmüller • Edited

Hi Peter,

The issue comes from using the plugin libInjectCss with Storybook. You should not need this plugin to build Storybook. You can remove it from the config when building Storybook.

Add this to .storybook/main.ts add this:

+  import { withoutVitePlugins } from "@storybook/builder-vite";

const config: StorybookConfig = {
  …
+  viteFinal: async (config) => {
+    return {
+      ...config,
+      plugins: await withoutVitePlugins(config.plugins, [
+        "vite:lib-inject-css",
+      ]),
+    };
+  },
};

Enter fullscreen mode Exit fullscreen mode
Collapse
 
codalf profile image
Peter Kullmann

Hi Andreas,
that did it! Thanks a lot!
This is really a nice setup for our company component library!

Collapse
 
receter profile image
Andreas Riedmüller

Here is the branch with Storybook: github.com/receter/my-component-li...

Collapse
 
receter profile image
Andreas Riedmüller

Hi Peter, thanks for your comment. I will try to find some time over the weekend and create a branch with storybook.

Collapse
 
evertzner profile image
evertzner

Thanks for this article, I followed each step and works perfectly!!
Is there a possibility to make this work for sveltekit? I am trying to adapt this to it, but it is not working as spected.

Collapse
 
receter profile image
Andreas Riedmüller

I would like to try Svelte… What did not work as expected?

Collapse
 
benpleri profile image
Benny

This was super helpful!
I'm still deciding between emotion or styled-components or regular sass but this had everything I needed to set up my vite component library
Thank you Andreas!

Collapse
 
j4v4scr1pt profile image
Js4 • Edited

I have problem that the generated main.js has imports to 3rd party libs.

Generated main.js:
import { Button as i } from "./components/Button/Button.js";
import "react/jsx-runtime";
import "tailwind-variants";
import "@nextui-org/react";
export {
i as Button
};

But my main.ts looks like this:
export {Button} from './components/Button/Button'

I have all three of them in the exclude block:
external: ['react','react/jsx-runtime',"tailwind-variants","framer-motion",RegExp("^(@nextui-org/).+")],

Why do Vite add them to the final build?!

Collapse
 
j4v4scr1pt profile image
Js4

I had removed this plugin in the Vite.config:

import {libInjectCss} from 'vite-plugin-lib-inject-css'

Because I dont use css files I use Tailwind classes.

But when I add this lib back the imports are not added.. 🤯

I have no idea why.. but at least it works....

Collapse
 
receter profile image
Andreas Riedmüller

Interesting, I will investigate this. Do you have a public repo to reproduce?

Thread Thread
 
j4v4scr1pt profile image
Js4 • Edited

Hi man!

I do not have a repo, this package is for internal use only.
But I created a sandbox for you to test with :). I tested and when you remove the libInjectCss from vite.config main.js gets the imports.. you run pnpm build-sb to build it and pnpm sb to start the project.

Sandbox Project

Thread Thread
 
shayankhp profile image
Shayan Khaleghparast

could u guys fix this issue?

Collapse
 
astarosa profile image
Astarosa • Edited

Tailwind needs Postcss using Vite so add it like so to your vite.config.ts
Put it in the external and output of rollup based on your needs.

import tailwindcss from "tailwindcss";

  css: {
    postcss: {
      plugins: [tailwindcss],
    },
  },
  build: {
    lib: {
      entry: resolve(__dirname, "src/index.ts"),
      formats: ["es"],
    },
    rollupOptions: {
      external: ["vue", "tailwindcss"],
      output: {
        globals: {
          vue: "Vue",
          tailwindcss: "tailwindcss"
        },
      },
    },
    sourcemap: true,
    emptyOutDir: true,
  },
Enter fullscreen mode Exit fullscreen mode
Collapse
 
above_average_joe profile image
Joe

how do exclude .test / .spec files from the output?

Collapse
 
above_average_joe profile image
Joe • Edited

Figured it out. You can add this to dts:

plugins: [
    react(),
    libInjectCss(),
    dts({ include: ['lib/**/!(*.spec|*.test).{ts,tsx}'] }),
  ],
Enter fullscreen mode Exit fullscreen mode

and this to the rollupOptions:

rollupOptions: {
      external: ['react', 'react/jsx-runtime'],
      input: Object.fromEntries(
        glob.sync('lib/**/!(*.spec|*.test).{ts,tsx}').map((file) => [
        ...
Enter fullscreen mode Exit fullscreen mode
Collapse
 
above_average_joe profile image
Joe

When I import a component in another project, I get the this error with vitest when running tests:

TypeError: Unknown file extension ".css" for C:\Users\joe\OneDrive\Documents\code\my-project\node_modules\@portfolijo\react-comp-lib\dist\assets\button.css
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bica profile image
Bica

So 3 questions.

  1. How do I handle global types that multiple components might share?
  2. So do I need to export ALL components in lib from the main.ts?
  3. I'm using SASS, so trying to find a way in which a compiled stylesheet can become a part of the dist.
Collapse
 
receter profile image
Andreas Riedmüller

Hi Bica,

Thanks for you comment!

  1. Per default ´vite-plugin-dts´ does not copy any d.ts files but only generates declaration files from source files. But you can set copyDtsFiles and all your d.ts files will get copied to the output.
  plugins: [
    react(),
    libInjectCss(),
-    dts({ include: ['lib'] })
+    dts({ include: ['lib'], copyDtsFiles: true })
  ],
Enter fullscreen mode Exit fullscreen mode

I also noticed that for each d.ts file an empty d.js file is created. This is not wanted and you can fix it by changing the glob to lib/**/!(*.d).{ts,tsx}:

    rollupOptions: {
      external: ['react', 'react/jsx-runtime'],
      input: Object.fromEntries(
-        glob.sync('lib/**/.{ts,tsx}').map(file => [
+        glob.sync('lib/**/!(*.d).{ts,tsx}').map(file => [
Enter fullscreen mode Exit fullscreen mode

Here is a branch that uses global types: github.com/receter/my-component-li...

  1. You need to export all components that you want to expose to the outside. If you have a component that is only used internally you don't have to include it in main.ts.
    But not all components will end up in you final application bundle, only the components that you import will.

  2. To use SASS you need to first install it

npm install -D sass
Enter fullscreen mode Exit fullscreen mode

And then you can just rename files to for example styles.module.sass and everything should compile fine.

If you need a global stylesheet you can create a lib/global.sass and import it in main.ts:

+import './global.sass'
export { Button } from './components/Button'

Enter fullscreen mode Exit fullscreen mode

Here is a branch that uses global sass for the Button and a global stylesheet: github.com/receter/my-component-li...

Does this answer your questions sufficiently?

Collapse
 
bica profile image
Bica • Edited

I had noticed my index.d.ts becoming converted to index.d.js and being empty.

On the export question, our setup differs a bit from the standard react component folder structure....i.e how I'm doing it is basically..

Components 
- input
- - toggleWhatever.tsx
- - someOtherOne.tsx
- - toggleWhatever.scss 
Enter fullscreen mode Exit fullscreen mode

So in main.ts I guess I have to do

export (ToggleWhatever as default) from 'compoments/input/togglewhatever';
Enter fullscreen mode Exit fullscreen mode

Yes? I get an error if I don't specify as default because I don't have my components as index.tsx files.

Then we are using Bootstrap. They haven't updated their sass to support @use and @forward, they still use @import so I have to be careful when pulling it in due to increased file size and duplication. What I was doing was running the sass command on build so a css was compiling and added to dist so the end developer could pull it in if they chose (from a theme perspective). However I want to play around with your solution inside the main.ts

Per the above I have to be careful adding a sass import to the top of individual components because if an input is used multiple times per page, I fear the reference to the styles will be repeated....I am not sure if that is valid in react. But if that file has to reference our bootstrap sass for their utility methods (i.e. color or fontsize) I think it will try to pull in bootstrap again due to the import issue I mentioned.

I want to have a playground locally to test the package but I don't want my local testing to be a part of our commit, just the package library.

Thread Thread
 
receter profile image
Andreas Riedmüller

For exporting your components you should be able to:

export { ToggleWhatever } from 'compoments/input/toggleWhatever';
Enter fullscreen mode Exit fullscreen mode

But inside of toggleWhatever.tsx you need to have a named export like:

export function ToggleWhatever() {}
// or
export const ToggleWhatever  = () => {}
// instead of
export default function ToggleWhatever() {}
Enter fullscreen mode Exit fullscreen mode

If you for some reason need to export the components as default you can also do it like this which is easier to read in my opinion:

import ToggleWhatever from 'compoments/input/toggleWhatever';
import SomeOtherOne from 'compoments/input/someOtherOne';

export {
  ToggleWhatever,
  SomeOtherOne
}
Enter fullscreen mode Exit fullscreen mode

Not 100% sure what you mean with the SASS, but if you import a button multiple times the styles will be included just once.

CSS duplication issues can also be handled downstream by your bundler.

If you use a global CSS library you should be fine by just adding an import in main.tsx In this case the CSS library will end up in your application as soon as one component of the library is used. You could also add an import for the library to every component that needs the library, so the CSS library is only bundled if one of these components is imported.

Collapse
 
bica profile image
Bica

Yes it does! I'll check out your fixes and if they will work with my changes.

Collapse
 
headwinds profile image
brandon flowers • Edited

This is a fantastic article and gave me the confidence to rebuild my personal component library, cross-country, with vite and upgrade to storybook 7!

I also wanted to consume the library in a NextJS app but instead of separating the CSS and JS, I wanted to compile them into a single JS file which also eliminates the global import css error and in theory one wouldn't have to worry importing any third-party css into the _app file.


NextJS Update
I'm now using this recommended rollup-plugin-css-only approach, and have decided that I'm ok with including simple instructions to simply import the single bundled css file into the NextJS _app.js file since you only have to do this once.


Instead of vite-plugin-lib-inject-css, I'm experimenting with vite-plugin-css-injected-by-js which seems to be what I want but isn't quite working yet. While I have no css errors I still don't see the css being applied when I consume the published library components. They appear unstyled which definitely isn't good. This is the recommended setup:

When I try vite-plugin-lib-inject-css with my library, publish it, and then consume it in a non-nextjs app like the Vite react stackblitz example, it works! I can import my components and their css style is maintained.

This is the ChatGPT explanation:

The vite-plugin-css-injected-by-js and vite-plugin-lib-inject-css are both Vite plugins that help manage CSS in your JavaScript bundles, but they serve slightly different purposes and work in different ways.

The vite-plugin-css-injected-by-js takes all the CSS generated by the build process and adds it via JavaScript. This results in a single JavaScript file, as the CSS file is not generated separately. This plugin is useful for those who want a single JavaScript file and can be configured to execute the CSS injection before or after your app code

On the other hand, vite-plugin-lib-inject-css is designed to work specifically in library mode. It injects CSS at the top of each chunk file using an import statement. This plugin supports multi-entries build and is especially helpful when building component libraries

Collapse
 
receter profile image
Andreas Riedmüller

Hm, I think injecting css has it's own problems like no control over the order of appearance. Also no further CSS processing is possible downstream... But this might be no problem in you use case.

What do you mean with "you don't have to worry importing any third-party css into the _app file."?

Collapse
 
lahirutech profile image
Lanka Rathanyaka

Thank you very much for this tutorial. I am using this to build a production app in my workplace, The issue I am going through is something like this.
Think that a button components is using types which are coming from icon component also.
So in the button component there will be a import like
import iconProps from "../icons/types"
but after building the project all the t.ds files are in root level but still the button.js build file has a import import iconProps from "../icons/types" which is not resolving since there is no icon folder inside the dist folder now. Seems like even I managed to add all the d.ts files in root level the js files using them are not resolving the path properly.
I am using multiple entry points for dts plugin as well.
So I managed to add d.ts files of some components on the relevant folder without ading them to root, which is not the ideal way I guess

    dts({
      include: [
        "src/components/**/*.{ts,tsx}",
        "src/icons/**/*.{ts,tsx}",
      ],
      exclude: ["**/*.stories.tsx"],

      beforeWriteFile: (filePath, content) => {
        if (
          filePath.includes("dist-storybook") ||
          filePath.includes(".stories.d.ts") ||
          filePath.includes("icons")
        ) {
          return { filePath, content };
        }
        const [projectPath, chunkPath] = filePath.split("/dist/");
        const chunk = chunkPath?.split("/").pop();
        return { filePath: `${projectPath}/dist/${chunk}`, content };
      },
    }),
  ],
Enter fullscreen mode Exit fullscreen mode
Collapse
 
receter profile image
Andreas Riedmüller

Hi Lanka,

Thanks, glad you like the article. Could you create a repo with a minimal example to reproduce this? Can you ellaborate on why you need this config for dts? Why don't you exclude the story files in the rollup config?

And here is a branch on how I would recommend to setup storybook: github.com/receter/my-component-li...

Collapse
 
xuhdev profile image
Hong Xu

Are you sure it is correct to use peerDependencies for react here? Based on the npm doc, the package is expected to not import a peerDependency.

Collapse
 
bnl profile image
Ben • Edited

peerDependencies should be read as: "dependencies if this project is a plugin". And a component plugs in to another project--it's not a project itself. So it relies on its host's plugins. peerDependencies says "as a plugin I want my host to have these deps" (which is why it's important to test with a variety of versions and be very lenient with the sem-version control).

Collapse
 
receter profile image
Andreas Riedmüller

Yes, using peerDependencies for React is appropriate here. peerDependencies should be used for packages that the consumer is expected to provide. This ensures that React is not automatically installed by the library, preventing multiple versions of React from existing in the same project, which is essential for React to work.

Collapse
 
u4ea profile image
U4EA • Edited

Excellent article and I have some points I would like your opinion on.

The use of tsc in the build command

"build": "tsc --p ./tsconfig-build.json && vite build"

I believe this is redundant as you are using vite-plugin-dts in your vite.config.js, which is building the types. I have forked the repo and run it with and without the inclusion of the tsc command and this appears to make no difference to the output. As the sole purpose of tsconfig-build.json appears to be the exclusion of the src directory during building I believe the tsconfig-build.json file itself is redundant as, if what I wrote above is correct, a build-specific tsconfig is not required at all.

Placement of vite-env.d.ts inside the lib directory

This also appears to be redundant. vite does indeed need to reference it's own types but it's clutter and code duplication to have separate vite-env-d.ts files.

I believe the best option here to solve these issues is: -

  1. Remove tsc from the build command, meaning tsconfig-build.json is now redundant and can be deleted.
  2. Delete vite-env.d.ts from the lib directory.
  3. In tsconfig.json, either reference the vite-env.d.ts file in the src directory in the compilerOptions "types": ["./src/vite-env.d.ts"] or simply delete all instances of the vite-env.d.ts file and reference the types directory "types": ["vite/client"].

I've tried the latter and have been able to both run the dev server and compile the library without issues.

Alternatively, if you want to keep the vite-env.d.ts file in in the lib directory, you can do modify the glob command in vite.config.ts to exclude it's output (as an empty file) in dist: -

glob.sync("lib/**/*.{ts,tsx}", {
ignore: ["lib/**/*.d.ts"],
})

Collapse
 
andreiisakov1 profile image
Andrei

Hello!
Thanks for the tutorial! It is great! Unfortunately I was not able fully finish it. After I added glob config to my vite.config.ts, the "npm run build" started to return an error:

failed to load config from ..../vite.config.ts
error during build:
file:///..../vite.config.ts.timestamp-1694619764411-22e8f4327550d.mjs:7
import glob from "file:///..../node_modules/glob/dist/mjs/index.js";
^^^^
SyntaxError: The requested module 'file:///..../node_modules/glob/dist/mjs/index.js' does not provide an export named 'default'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:122:21)
at async ModuleJob.run (node:internal/modules/esm/module_job:188:5)
at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)
at async loadConfigFromBundledFile (file:///..../node_modules/vite/dist/node/chunks/dep-df561101.js:66235:21)
at async loadConfigFromFile (file:///..../node_modules/vite/dist/node/chunks/dep-df561101.js:66086:28)
at async resolveConfig (file:///..../node_modules/vite/dist/node/chunks/dep-df561101.js:65682:28)
at async build (file:///..../node_modules/vite/dist/node/chunks/dep-df561101.js:47852:20)
at async CAC. (file:///..../node_modules/vite/dist/node/cli.js:822:9)

I use npm version 9.8.0, nvm version 20.5.1, vite version

Best Regards,
Andrei

Collapse
 
receter profile image
Andreas Riedmüller • Edited

Hi Andrei,

thanks for your comment, I am glad you like the tutorial!

I guess you need to do a named import:

import { glob } from 'glob'
Enter fullscreen mode Exit fullscreen mode

It was a "typo" in the article, I have updated it accordingly, thanks for your feedback!

Collapse
 
andreiisakov1 profile image
Andrei • Edited

Hello Andreas! Thanks a lot for the update! now everything works just perfectly!

Collapse
 
mediabuff profile image
mediabuff

thanks for excellent article and sample code. The import CSS does not get transpiled to JS script. Thus I get run time error (when I use the library in another project).

For eg. Button/index.js has the following:
import "../../assets/index4.css";
Obvisiouly, this will not work in the brower. Should'nt this get transpiled as CSS Module ?

Collapse
 
receter profile image
Andreas Riedmüller • Edited

If the consuming environment does not have a bundler setup that supports CSS imports please see this answer: dev.to/receter/comment/2a198

You can disable these CSS imports and generate a CSS file that you can import sepparately.

Collapse
 
mediabuff profile image
mediabuff

thank you. Got it!

Collapse
 
alexandrebenkendorf profile image
Alexandre Benkendorf

I tried installing the vite-plugin-lib-inject-css on my Mac M1 (Sonoma 14.4.1) but got error @ast-grep/napi-darwin-x64@npm:0.21.4: No candidates found

For those with the same error, I've found this one that worked for me: npmjs.com/package/vite-plugin-css-...

Great post, thx

Collapse
 
kneumei profile image
Kyle Neumeier • Edited

One question I have:

If my component depends on another npm module (say lodash), when I run vite...the dependency would be bundled into my index.js file....

So do I have lodash listed as a dependency in package.json? If so, wouldn't that mean consumers would transitively install lodash unnecessarily?

So seems like I should either:

  1. install lodash as a normal dependency, but then also put lodash in external
  2. move lodash to devDependency
Collapse
 
receter profile image
Andreas Riedmüller

Hi Kyle, sorry for my late reply. Here is an answer to a similar question if you are still interested: reddit.com/r/reactjs/comments/15o4...

My recommendation for your case would be to add it to dependencies and define it as external.

I made a branch to show how I would do it here:
github.com/receter/my-component-li...

Collapse
 
brunowbbs profile image
Wesley Bruno

Hello, after adding tailwind to my project, the imports are not working when the developed lib is used in another project. It is generating a different structure in the "dist" directory

Image description

Collapse
 
tomaszplewka profile image
Tomasz Plewka

Wow, @receter ! Absolutely awesome stuff! That's exactly what I needed. Thank you! Can I buy you a coffee? :D

I have my library as a part of nx monorepo with Vite as bundler and my consuming app is in Next.js v13.5.6 - works flawlessly! I used plain old sass with BEM and had css code split up per component and loaded per component - Next.js deals with that easily - no css / font 'jumps'.

Collapse
 
receter profile image
Andreas Riedmüller

Cool, glad my article did help you! If you ever make it to Austria/Graz you can buy me a real coffee 😉

Collapse
 
jonnjjordan profile image
Jonn Jordan

Hi Andreas, great article!

I have an issue with the global css import, I have imported global styles in my main.ts file and when the library is compiled I can see those clases added into my /assets/main.css file that is imported into the compiled main.js.

The problem is when I import the library in a create-react-app project, it ignores everything that is inside main.css, It only works if I directly import it into the project like: import "mylibrary/dist/assets/main.css".

Do you know how to solve this issue?

Collapse
 
receter profile image
Andreas Riedmüller

Hi Jonn,

do you import anything in the create-react-app project or did you just install the plugin? The import will only work if the main.js file is somehow included in the build. For example when you import anything from main.js.

Do you have an example project on GitHub in which I can replicate the issue?

Collapse
 
c0mc00l profile image
Dennis Thrane

I have a interface i export in a component. When i run build i get a error that it can not import the interface in the main.ts. Why is that?

Collapse
 
receter profile image
Andreas Riedmüller

Hi, can you provide a repo to reproduce this? What is the error message?

Collapse
 
c0mc00l profile image
Dennis Thrane • Edited

Have a component in lib/components/date-input/

It has a date-input.component.tsx file that includes a interface:

export interface IDateInputProps extends IValidatableField, ITooltip {
  value?: string;
  ...
}
Enter fullscreen mode Exit fullscreen mode

It also has a index.ts file with the following code:

export { IDateInputProps, DateInput, FormDateInput } from "./date-input.component"; 
Enter fullscreen mode Exit fullscreen mode

When I run build I get this error:

vite v5.0.11 building for production...
✓ 1283 modules transformed.
"IDateInputProps" is not exported by "lib/components/form/date-input/date-input.component.tsx", imported by "lib/components/form/date-input/index.ts".
file: /Volumes/Data/Repos/Ecom.UI.Npm/lib/components/form/date-input/index.ts:1:9
1: export { IDateInputProps, DateInput, FormDateInput } from "./date-input.component";
            ^
error during build:
RollupError: "IDateInputProps" is not exported by "lib/components/form/date-input/date-input.component.tsx", imported by "lib/components/form/date-input/index.ts".
    at error (file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/node_modules/rollup/dist/es/shared/parseAst.js:337:30)
    at Module.error (file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/node_modules/rollup/dist/es/shared/node-entry.js:12759:16)
    at Module.getVariableForExportName (file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/node_modules/rollup/dist/es/shared/node-entry.js:12923:29)
    at Module.includeAllExports (file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/node_modules/rollup/dist/es/shared/node-entry.js:12999:37)
    at Graph.includeStatements (file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/node_modules/rollup/dist/es/shared/node-entry.js:18928:36)
    at Graph.build (file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/node_modules/rollup/dist/es/shared/node-entry.js:18865:14)
    at async file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/node_modules/rollup/dist/es/shared/node-entry.js:19502:13
    at async catchUnfinishedHookActions (file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/node_modules/rollup/dist/es/shared/node-entry.js:19014:16)
    at async rollupInternal (file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/node_modules/rollup/dist/es/shared/node-entry.js:19497:5)
    at async build (file:///Volumes/Data/Repos/Ecom.UI.Npm/node_modules/vite/dist/node/chunks/dep-V3BH7oO1.js:67172:18)
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
receter profile image
Andreas Riedmüller

Is it maybe related to this issue? github.com/rollup/plugins/issues/71

If you can share a repo to reproduce the issue, I can have a look.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
receter profile image
Andreas Riedmüller

I would need more detailed information to give you an helpful answer. Do you have a repo were this happens? Does it also happen when you install my demo npm library? (link at the bottom of the article) What bundler setup are you using?

Collapse
 
pcharliee profile image
Carlos Delgado

Hey! Amazing tutorial thanks for sharing.

Im having kind of an issue where the build is creating an additional file and Im not sure what could be happening.

Any ideas? Not sure where i should look

Image description

Collapse
 
chriscosentino profile image
ChrisCosentino

Great post I've been looking for something like this for a while now...is it possible to test this by using npm link with another react app locally before deploying to npm?

Collapse
 
receter profile image
Andreas Riedmüller

Hi, yes you can totally do that. There are some peculiarities when using npm link. Here is an older article of mine, the section about npm link might help you: dev.to/receter/the-minimal-setup-t...

In most cases it might be better to just install the package via a file path, see stackoverflow.com/questions/143818...

Like so:


  "dependencies": {
    "my-library": "file:../my-library"
 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ryanneil profile image
Ryan Neil

Thank you for this article its been extremely helpful! Quick question, how much refactor would it be to substitute css modules with tailwind? Do you have any resources you could point me too?

Collapse
 
receter profile image
Andreas Riedmüller

I think that should be fairly easy. I never worked with tailwind but always wanted to try using it. If I have time today, I will give it a try and push a branch with tailwind instead of CSS Modules.

Collapse
 
jamestbaker profile image
James Baker

@receter , thanks so much for this. Fantastic walkthrough!

Collapse
 
sakobume profile image
Sarkis Melkonian

Any ideas on why I would get an error TS2307: Cannot find module './styles.module.css' or its corresponding type declarations?

Collapse
 
u4ea profile image
U4EA

It's not finding the vite environment types. In the repo, this is performed by referencing the vite-env.d.ts file. In the comment I just added above I have suggested another, cleaner way to reference the types: -

dev.to/u4ea/comment/2eo2i

Collapse
 
kevinah95 profile image
Kevin Hernández Rostrán

Thank you

Collapse
 
avoguga profile image
Gustavo Henrique

Hey Andreas! Nice work dude, u really help me! Is really a great article.

You have something about make a lib with more performance? My lib is getting bigger and bigger.

Collapse
 
receter profile image
Andreas Riedmüller

With performance you mean build performance?

Collapse
 
lamimershed profile image
lamimershed

how to write styles using tailwind
it would be nice if you include that to

Collapse
 
receter profile image
Andreas Riedmüller

I think you can just use tailwind classes an make sure tailwind is set up in the consuming application and your test environment.

Collapse
 
asurakev profile image
AsuraKev

I heard Vite library doesnt do tree shake, how do you know your import is being tree shaked?

Collapse
 
receter profile image
Andreas Riedmüller

Thanks for your comment. I will investigate this.

Here is a SO question I found regarding this topic: stackoverflow.com/questions/743626...

And this linked issue in particular: github.com/vitejs/vite/issues/5174

It depends on the bundler I guess, seems that when using webpack you might need to do additional work.

To find out if tree shaking works, take a look at the final bundle.

Using some unique strings in your code can help you to quickly find if something made it to the bundle.

I will do some tests to find out what works and will share the repos in f I do so.

If you have a public repo to reproduce tree shaking issues I am also happy to have a look at it.