Streamlining Component Library Publishing with Vite and tsup
Emily Parker
Product Engineer · Leapcell

Introduction
In the rapidly evolving world of front-end development, creating reusable and shareable UI components is paramount for efficient development and maintaining consistent user experiences. Component libraries serve as the backbone for such modularity, allowing developers to encapsulate functionality and styling into digestible units. However, the true power of a component library is unleashed only when it's easily consumable by others, which necessitates robust packaging and effortless publication to a registry like npm. Traditionally, setting up a build pipeline for a component library could be a complex and time-consuming endeavor, often involving intricate Webpack configurations. This article explores how modern tools like Vite and tsup have revolutionized this process, making it significantly simpler and faster to build and publish your React or Vue component libraries to npm. We'll delve into their core principles, practical implementation, and the tangible benefits they bring to front-end developers.
The Modern Component Library Workflow
Before diving into the specifics of Vite and tsup, let's establish a common understanding of the key concepts involved in building and publishing a component library.
Component Library: A collection of pre-built UI components, often written in frameworks like React or Vue, designed for reuse across different projects. Packaging (Bundling): The process of taking your source code (e.g., TSX, Vue SFCs, CSS) and transforming it into optimized, browser-compatible files (e.g., JavaScript, CSS) that can be easily consumed by other projects. This typically involves transpilation, minification, and tree-shaking. Module Formats: Standards for organizing and loading JavaScript modules. The most common in modern web development are: * CommonJS (CJS): Primarily used in Node.js environments. * ECMAScript Modules (ESM): The official standard for JavaScript modules, suitable for both browsers and Node.js. Type Definitions (d.ts files): Files that provide type information for JavaScript modules, crucial for TypeScript support and improved developer experience in consuming projects. npm (Node Package Manager): The world's largest software registry, where developers publish and share JavaScript packages, including component libraries.
Historically, Webpack was the dominant bundler, offering immense flexibility but often at the cost of configuration complexity and slower build times. The rise of "no-config" or "low-config" bundlers has dramatically simplified the packaging process. Vite and tsup exemplify this new generation, focusing on developer experience and performance.
Vite for Component Library Packaging (React Example)
Vite, primarily known for its incredibly fast development server, also excels as a build tool, especially for modern JavaScript frameworks. Its configuration for library mode is remarkably straightforward.
Let's imagine we have a simple React component library structure:
my-react-library/
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ └── index.ts
│ │ └── Card/
│ │ ├── Card.tsx
│ │ └── index.ts
│ └── index.ts // Main entry point
├── package.json
├── tsconfig.json
├── vite.config.ts
Our src/index.ts
would re-export components:
// src/index.ts export * from './components/Button'; export * from './components/Card';
Now, let's configure vite.config.ts
for library mode:
// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { resolve } from 'path'; export default defineConfig({ plugins: [react()], build: { lib: { // Multiple entry points are also supported entry: resolve(__dirname, 'src/index.ts'), name: 'MyReactLibrary', // Global variable name for UMD build fileName: (format) => `my-react-library.${format}.js`, }, rollupOptions: { // Make sure to externalize deps that shouldn't be bundled // into your library external: ['react', 'react-dom'], output: { // Provide global variables to use in the UMD build // for externalized deps globals: { react: 'React', 'react-dom': 'ReactDOM', }, }, }, // Generate type definitions using tsc // enable for TypeScript projects // dts: true, // This generates .d.ts files }, });
To build, add a script to package.json
:
// package.json { "name": "my-react-library", "version": "1.0.0", "main": "./dist/my-react-library.umd.cjs", "module": "./dist/my-react-library.es.js", "types": "./dist/index.d.ts", // Important for TypeScript "files": ["dist"], "scripts": { "build": "vite build && tsc --emitDeclarationOnly --declaration --outDir dist" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "devDependencies": { "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@vitejs/plugin-react": "^4.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "typescript": "^5.0.0", "vite": "^4.0.0" } }
Notice the tsc --emitDeclarationOnly --declaration --outDir dist
part. While Vite has dts: true
, it might not always offer the full flexibility for complex type exports. Running tsc
separately like this ensures robust type definition generation. The main
, module
, and types
fields in package.json
are crucial for consumers to correctly pick up the correct module format and type definitions. files
specifies what should be included when publishing to npm. peerDependencies
declares dependencies your library expects to be provided by the consuming application, rather than bundling them.
Running npm run build
will generate output in the dist
folder, typically including ES module, CommonJS, and UMD formats, along with type definitions.
tsup for Component Library Packaging (Vue Example)
tsup is another excellent tool that simplifies bundling for libraries, particularly for TypeScript projects. It's built on Esbuild, making it incredibly fast, and often requires minimal configuration.
Consider a Vue component library:
my-vue-library/
├── src/
│ ├── components/
│ │ ├── MyButton.vue
│ │ ├── MyCard.vue
│ │ └── index.ts // Re-exports components
│ └── index.ts // Main entry point
├── package.json
├── tsconfig.json
├── tsup.config.ts
Our src/components/index.ts
might look like:
// src/components/index.ts import MyButton from './MyButton.vue'; import MyCard from './MyCard.vue'; export { MyButton, MyCard };
And src/index.ts
just re-exports:
// src/index.ts export * from './components';
Now, let's configure tsup.config.ts
:
// tsup.config.ts import { defineConfig } from 'tsup'; import vue from '@vitejs/plugin-vue'; // Or other Vue plugins if needed export default defineConfig({ entry: ['src/index.ts'], format: ['cjs', 'esm'], // CommonJS and ES Modules dts: true, // Generate type definitions clean: true, // Clean dist folder before build splitting: false, // For single-file component libraries, usually false sourcemap: true, external: ['vue'], // Vue is a peer dependency esbuildPlugins: [ // Use Vite's Vue plugin to handle .vue files if directly importing them // This is more common in Vite setups, but demonstrates how to handle Vue files // If you only export compiled JS/TS, this might not be strictly needed for tsup itself // but beneficial for building the source. // For tsup, it's often easier to convert .vue files to .js/.ts first or ensure they're handled higher up. // A simpler approach for tsup might be to use a preceding step to compile .vue files // or ensure your entry points are pure .ts files that import compiled Vue components. ], // Example for handling .vue files: Pre-compile them to JS modules. // Or, if tsup is directly processing TS that imports .vue, you'd need an esbuild plugin extension. // A common approach for Vue component libraries is using `vue-tsc` to generate .d.ts for SFCs. // tsup is excellent for TS/JS only; for SFCs, you might combine it with Vite or a custom build step. });
A more practical tsup.config.ts
for Vue component libraries might look simpler if you compile .vue
files separately, or if tsup
only consumes .ts
files that export already-compiled JS modules. If you want tsup to directly handle .vue
files, you'd need an esbuild plugin, which isn't as straightforward as in Vite. For simplicity and robustness, often a Vite build (vite build --lib
) is preferred for Vue SFCs, or you use vue-tsc
for types and then tsup
for bundling the compiled JS/TS.
Assuming we use a mix or have our Vue components already transpiled to JS/TS exports for tsup:
// package.json { "name": "my-vue-library", "version": "1.0.0", "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "files": ["dist"], "scripts": { "build": "tsup", "dev": "tsup --watch" }, "peerDependencies": { "vue": ">=3.0.0" }, "devDependencies": { "@types/node": "^20.0.0", "tsup": "^8.0.0", "typescript": "^5.0.0", "vue": "^3.0.0" } }
Running npm run build
will generate dist/index.cjs
, dist/index.mjs
, and dist/index.d.ts
. tsup
handles much of the boilerplate automatically, from type generation to multiple output formats.
Publishing to npm
Once your library is built, publishing it to npm is a straightforward process.
-
Ensure
package.json
is correct:name
: Unique name for your package.version
: Semantic versioning is crucial (e.g.,1.0.0
).description
: A brief summary of your library.keywords
: Relevant search terms.author
/license
: Important metadata.main
,module
,types
: Point to your built output files.files
: Specify included files, typically just["dist"]
.peerDependencies
: Correctly list external dependencies.
-
Login to npm:
npm login
Follow the prompts to enter your npm username, password, and email.
-
Publish:
npm publish
If you're publishing a scoped package (e.g.,
@your-scope/my-library
), you might need:npm publish --access public
This command uploads your package to the npm registry.
-
Update (if necessary): For subsequent updates, increment the
version
inpackage.json
(e.g., usingnpm version patch
,npm version minor
, ornpm version major
) then runnpm publish
again.
Advanced Considerations
- CSS/SCSS Handling: Both Vite and tsup can handle CSS imports. For component libraries, you might want to export raw CSS files (e.g., in a
dist/css
folder) or compile them directly into your JavaScript bundles, depending on how consumers prefer to import styles. Vite's library mode automatically extracts CSS when appropriate. For tsup, you might use an esbuild plugin or a separate build step (e.g.,postcss
to compile.css
orsass
to compile.scss
). - Storybook/Documentation: Building a component library often goes hand-in-hand with documenting it. Tools like Storybook allow you to develop, test, and document components in isolation, ultimately leading to better maintainability and adoption.
- Testing: Implement unit and integration tests for your components to ensure their reliability. Tools like Vitest (for Vite projects) or Jest are excellent choices.
- Monorepos: If you have multiple related packages (e.g., a shared utility library and a component library), consider using a monorepo setup with tools like Lerna or Turborepo.
Using Vite or tsup significantly simplifies the packaging process, allowing developers to focus on component logic and design rather than complex build configurations. Their speed and ease of use make them ideal choices for modern front-end library development.
Conclusion
Packaging and publishing component libraries to npm is a critical step in fostering reusability and accelerating front-end development. Modern build tools like Vite and tsup have abstracted away much of the underlying complexity, offering lightning-fast build times and intuitive configurations, particularly for React and Vue projects. By leveraging these tools, developers can efficiently transform their source code into optimized, consumable packages, complete with type definitions and various module formats, and share them effortlessly with the broader development community via npm. These tools empower developers to build and distribute high-quality, performant component libraries with unprecedented ease.