DEV Community

Josip Ivancic
Josip Ivancic

Posted on • Originally published at jivancic.com

Build a typescript component library with Vite

Example repo at: https://github.com/josip2312/typescript-lib-vite

Why Vite

Vite ships with a pre-configured build command that has many performance optimizations out of the box. It uses Rollup under the hood and provides an abstraction over the default rollup configuration. This makes it way easier to set up the build step without knowing all the internals of rollup.

Components

For the sake of the example, I'm going to create a simple button component, but the build step remains the same for any number of components. In this example, I'm using the <script setup> syntax.

Component code:

<script setup lang="ts">
import { reactive } from "vue";

interface Props {
  primary?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  primary: false,
});

const { primary } = reactive(props);
</script>

<template>
  <button class="btn" :class="{ primary }">
    <slot />
  </button>
</template>

<style scoped>
.btn {
  padding: 0.5rem 1rem;
}
.btn.primary {
  background: hsl(239, 100%, 27%);
  color: #fff;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Next, I'm going to create the entry file for the library where all the components will be exported.

// src/index.ts
export { default as MyButton } from "./MyButton";
Enter fullscreen mode Exit fullscreen mode

Vite config

Vite is a fast new build tool that is intended for modern web projects. It uses native ES modules and provides a blazing-fast dev server and hot module replacement. Learn more about it on the official website.

Vite is framework agnostic which means you can use it with most frontend frameworks, and the build config is pretty much the same. There is a section on the official website which describes different build modes, our interest is in the library mode build.

So we need to add the following to our vite.config.ts (or .js).

// vite.config.ts
const path = require("path");
const { defineConfig } = require("vite");

module.exports = defineConfig({
  build: {
    lib: {
      entry: path.resolve(__dirname, "src/index.ts"),
      name: "MyComponentLib",
      fileName: (format) => `my-component-lib.${format}.js`,
    },
    rollupOptions: {
      // make sure to externalize deps that shouldn't be bundled
      // into your library
      external: ["vue"],
      output: {
        // Provide global variables to use in the UMD build
        // for externalized deps
        globals: {
          vue: "Vue",
        },
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

If you now run npm run build you should get the output in the dist folder.

If you're getting an error regarding __dirname or path you need to install node type declarations

npm i -D @types/node
Enter fullscreen mode Exit fullscreen mode

Package.json config

Before testing our build output we need to configure the package.json to point at correct built files. We are defining the entry point to the library.
You can learn more about what each option does by hovering the properties in VS Code.

// "my-component-lib" should match the "name" field in your package.json
{
  "files": ["dist"],
  "main": "./dist/my-component-lib.umd.js",
  "module": "./dist/my-component-lib.es.js",
  "exports": {
    ".": {
      "import": "./dist/my-component-lib.es.js",
      "require": "./dist/my-component-lib.umd.js"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Test build output

To test the library locally we can zip the dist file, before running the below change the name field in your package.json to your preferred library name.

Run the following:

npm pack
Enter fullscreen mode Exit fullscreen mode

This command provides us with a zipped file that we can map to the library dependency in the package.json. After running this add the following to the dependencies field:

 "dependencies": {
 "my-component-lib": "my-component-lib-0.0.0.tgz"
  },
Enter fullscreen mode Exit fullscreen mode

After importing the library it should now work.

If you run into dependency errors try deleting the zipped file and packing it again, or deleting the package.lock.json and node_modules and runningnpm install again.

<script setup lang="ts">
import { MyButton } from "my-component-lib";
import "/node_modules/my-component-lib/dist/style.css";
</script>
Enter fullscreen mode Exit fullscreen mode

However, you'll notice we get an error regarding missing declaration files, we will solve this in the next step.

Typescript declarations

To solve the typescript declarations error, we need to generate type declaration files for the components. We will do this using the vue-tsc package.

Make sure to update vue-tsc to a newer version since the create-vue CLI defaults to an older version.

You need to add the following to tsconfig.json:

{
  "outDir": "dist",
  "declaration": true
}
Enter fullscreen mode Exit fullscreen mode

Change the build script to the following:

{
  "build": "vite build && vue-tsc --emitDeclarationOnly && mv dist/src dist/types"
}
Enter fullscreen mode Exit fullscreen mode

Lastly, set the path for the type declarations inside package.json

{
  "types": "./dist/types/index.d.ts"
}
Enter fullscreen mode Exit fullscreen mode

The last command in the build script moves the types to a types directory, by default they would be in a src directory since that is our target inside the tsconfig.

If you now repeat the build and pack process, the type error should not be there.

If it's still there try deleting your node_modules and package.lock.json and installing the packages again.

Publish to npm

Everything is already set up for publishing, you can just authenticate with npm and publish.

npm login # authenticate

npm publish # publish
Enter fullscreen mode Exit fullscreen mode

Conclusion

Vite makes it incredibly easy to build a component library with a very slim config. Coupled with the amazing development experience it provides I can conclude it is the next big thing in frontend development.

Top comments (3)

Collapse
 
andrioid profile image
Andri

Do you know how we can use the vite build --watch and somehow get the type declaration files copied? I can use "concurrently", but that seems heavy handed.

Collapse
 
josip2312 profile image
Josip Ivancic

I would suggest just developing the components regularly without a watcher, once you are finished you can check the actual build output, but it shouldn't ever be different than the local one.

Collapse
 
ghiscoding profile image
Ghislain B. • Edited

I'm also having this problem after following this great article, I guess that the other option would be to run vite build --watch without the type declaration, which is a lot faster, and completely disregard the import error in your application. I added myself a separate script ("build:types": "vue-tsc --emitDeclarationOnly") for creating the type after the build watch started, so this way it removes the import warning and I can still develop, but like I said earlier even with the warnings you can still develop and once you're done with the component then run a full build components and you will get all types back. Another option could be to use vite-plugin-dts but I had issues with it, it is however much faster than vue-tsc

Also note that you probably don't need the mv command (which is not cross platform, at least it doesn't work on Windows), you can simply add this tsconfig "declarationDir": "dist"