Building a JavaScript application with Vite is straightforward. Publishing a reusable JavaScript library, however, requires a few additional decisions. You need to configure the entry point, generate the right output formats, manage dependencies, and make sure other developers can import your package without running into compatibility problems.

For example, you might build a shared date utility, a collection of reusable UI components, or a small validation library. Instead of copying the code into every project, you can package it and publish it to npm.

Vite provides a library mode designed for this purpose. It uses Vite's build pipeline to produce distributable files for JavaScript libraries, while allowing you to control how consumers import and use the package.

In this article, you will learn how to create a reusable library with Vite, configure its build output, manage dependencies, test the generated package, and prepare it for publishing.

What Is Vite Library Mode?

Vite is a frontend build tool that provides development features and a production build process. Its library mode configures the production build for distributing reusable code rather than producing a complete website.

A normal Vite application build typically generates assets for a browser application. A library build generates files that other projects can import.

For example, a utility library might expose this function:

JavaScript
export function formatCurrency(amount) {
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD"
    }).format(amount);
}

After building the library, another project can install the package and import the function:

JavaScript
import { formatCurrency } from "@example/format-utils";

console.log(formatCurrency(1250));

The package name is illustrative. A real package needs a unique name and a correctly configured package.json.

Vite library mode is useful when you want to reuse code across applications, distribute a component library, or publish a JavaScript package without manually configuring every part of the bundling process.

Step 1: Create a Vite Project

Start by creating a new project directory:

Bash
mkdir format-utils
cd format-utils
npm init -y
npm install --save-dev vite

These commands create a package and install Vite as a development dependency.

Create the following structure:

Code
format-utils/
├── src/
│   └── index.js
├── package.json
└── vite.config.js

The src/index.js file will serve as the library's entry point.

Add the following code:

JavaScript
export function formatCurrency(amount, currency = "USD") {
    if (!Number.isFinite(amount)) {
        throw new TypeError("Amount must be a finite number.");
    }

    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency
    }).format(amount);
}

export function formatPercentage(value) {
    if (!Number.isFinite(value)) {
        throw new TypeError("Value must be a finite number.");
    }

    return `${value}%`;
}

This module exports two functions. The first formats a number as currency, while the second formats a number as a percentage string.

The example keeps the library small so that you can focus on the packaging process. A production library should also document its supported inputs, expected outputs, error behavior, and compatibility requirements.

Step 2: Configure Vite Library Mode

Create a vite.config.js file:

JavaScript
import { defineConfig } from "vite";

export default defineConfig({
    build: {
        lib: {
            entry: "src/index.js",
            name: "FormatUtils",
            formats: ["es", "cjs"],
            fileName: (format) => {
                return format === "es"
                    ? "format-utils.js"
                    : "format-utils.cjs";
            }
        }
    }
});

This configuration tells Vite to build the library from src/index.js.

The important options are:

  • entry: identifies the main source file.

  • name: defines a library name used by applicable output formats, such as UMD or IIFE.

  • formats: specifies the module formats to generate.

  • fileName: controls the generated filenames.

In this example, the build produces an ES module and a CommonJS file.

ES modules use import and export. CommonJS uses require() and module.exports.

The name property is not required for every format. It becomes relevant for formats such as UMD and IIFE that expose a global variable in environments where native module imports are not used.

Step 3: Add Build Scripts and Package Metadata

Update package.json to describe the package and its generated files:

JSON
{
    "name": "@example/format-utils",
    "version": "1.0.0",
    "description": "Reusable JavaScript formatting utilities",
    "type": "module",
    "main": "./dist/format-utils.cjs",
    "module": "./dist/format-utils.js",
    "exports": {
        ".": {
            "import": "./dist/format-utils.js",
            "require": "./dist/format-utils.cjs"
        }
    },
    "files": [
        "dist"
    ],
    "scripts": {
        "build": "vite build"
    },
    "devDependencies": {
        "vite": "YOUR_INSTALLED_VERSION"
    }
}

The version placeholder is illustrative. Keep the actual Vite version installed by your package manager.

Several fields are important when publishing a library.

Field

Purpose

name

Identifies the package

version

Defines the published package version

main

Provides a CommonJS entry point for tools that use this field

module

Provides an ES module entry point for tools that recognize this convention

exports

Defines the public package entry points

files

Controls which files are included in the published package

scripts.build

Defines the command used to generate the distributable files

The exports field is particularly important because it gives consumers a defined public entry point.

Because this example uses "type": "module", the .js output is interpreted as an ES module by Node.js. The .cjs extension explicitly identifies the CommonJS output.

The main and module fields are included for compatibility with tools that recognize them. Modern package resolution should generally rely on a correctly configured exports field.

The package name @example/format-utils is a placeholder. Replace it with a package name you are authorized to publish.

Step 4: Build the Library

Run the build command:

Bash
npm run build

Vite generates the configured output files in the dist directory.

The resulting structure should look similar to this:

Code
format-utils/
├── dist/
│   ├── format-utils.js
│   └── format-utils.cjs
├── src/
│   └── index.js
├── package.json
└── vite.config.js

The exact output can vary depending on your Vite version and build configuration.

The generated ES module can be consumed by modern JavaScript applications. The CommonJS output can be used by compatible Node.js tools and projects.

Before publishing, inspect the generated files and verify that they export the functions you intended to make public.

Step 5: Understand ES Modules, CommonJS, and UMD

A library may need to support different environments. Choosing output formats depends on the consumers you want to support.

Format

Common use

Considerations

ES modules (ESM)

Modern JavaScript applications and tools

Supports static imports and tree-shaking workflows

CommonJS (CJS)

Node.js projects and tools using require()

Requires compatible CommonJS output

UMD

Environments that need a universal module wrapper

Can support several loading styles but is less central to modern package workflows

IIFE

Browser scripts loaded directly through a script tag

Typically exposes functionality through a global variable

You do not need to generate every format.

For a modern npm package, ESM may be enough. If you need to support consumers that rely on CommonJS, include that format and test it explicitly.

UMD and IIFE outputs are more relevant when consumers need to load a bundled script directly in a browser or use older module-loading patterns.

Avoid generating formats that your consumers do not need. Every additional format creates another output to test and maintain.

Step 6: Manage Dependencies Correctly

Libraries often depend on other packages.

Suppose your library uses a third-party date library. You must decide whether that dependency should be bundled into your output or remain an external dependency.

Vite library mode supports externalizing dependencies through Rollup-compatible build configuration.

For example:

JavaScript
import { defineConfig } from "vite";

export default defineConfig({
    build: {
        lib: {
            entry: "src/index.js",
            formats: ["es", "cjs"],
            fileName: (format) =>
                format === "es"
                    ? "format-utils.js"
                    : "format-utils.cjs"
        },
        rollupOptions: {
            external: ["date-fns"]
        }
    }
});

This example marks date-fns as external. The generated library will expect the consuming environment to resolve that dependency rather than including its implementation in the bundle.

If a dependency is externalized, declare it appropriately in the package metadata. Depending on its role, it may belong in dependencies or peerDependencies.

For example:

  • Use dependencies when the package needs to install a runtime dependency for its own operation.

  • Use peerDependencies when the consuming application is expected to provide a compatible dependency, which is common for framework integrations and plugins.

Do not externalize a package without ensuring that consumers can resolve it. An incorrectly configured external dependency can produce a library that builds successfully but fails when imported.

Step 7: Test the Package Before Publishing

A successful build does not prove that the generated package works in a consumer project.

One useful approach is to create a temporary package archive:

Bash
npm pack

This creates a package archive containing the files selected for publication.

Inspect the archive to confirm that it includes the generated output and required metadata. You can then install the archive into a separate test project.

For example:

Bash
npm install ../format-utils/example-format-utils-1.0.0.tgz

The archive filename depends on the actual package name and version, so adjust the path accordingly.

Test the ESM entry point:

JavaScript
import { formatCurrency } from "@example/format-utils";

console.log(formatCurrency(1250));

If CommonJS support is part of your package contract, test it separately:

JavaScript
const { formatCurrency } = require("@example/format-utils");

console.log(formatCurrency(1250));

These examples assume the package has been installed into the test project and its metadata matches the configured output.

Testing the packaged artifact catches problems that source-level tests may miss, such as incorrect export paths, missing files, and module-format mismatches.

Step 8: Publish the Package to npm

Before publishing, verify the package name, version, README, license, and generated output.

If you are publishing a public package, authenticate with npm using your organization's approved process.

Then build and publish:

Bash
npm run build
npm publish --access public

The --access public option is relevant when publishing a scoped package that should be publicly accessible. For an unscoped package or a package with different access requirements, follow the appropriate npm publishing workflow.

Do not publish a package until you have tested the packed artifact and confirmed that the intended files will be included.

Also consider using a continuous integration pipeline to build, test, and publish releases after the relevant checks and approvals have passed.

Common Mistakes to Avoid

Mistake

Why it causes problems

How to avoid it

Incorrect exports paths

Consumers cannot import the package

Verify paths against the actual build output

Publishing before building

The archive may not contain the expected files

Run the build before packing or publishing

Mixing module formats incorrectly

import or require() may fail

Align file extensions, package metadata, and output formats

Externalizing undeclared dependencies

Consumers cannot resolve required packages

Declare dependencies in the appropriate metadata field

Including unnecessary source files

Increases package size and exposes internal files unnecessarily

Review the files field and packed archive

Generating unused formats

Adds maintenance and testing work

Support only the formats required by consumers

Testing only inside the original repository

Workspace configuration may hide packaging problems

Install and test the packed archive separately

Best Practices for Production Libraries

  • Keep the public API small and clearly documented.

  • Use explicit entry points and package exports.

  • Generate only the module formats your consumers need.

  • Externalize dependencies deliberately.

  • Include the required runtime dependencies in package metadata.

  • Test the generated package outside the source repository.

  • Review the contents of npm pack before publishing.

  • Use semantic versioning to communicate breaking changes.

  • Include a README with installation instructions and examples.

  • Add automated tests for supported import styles.

  • Publish through a controlled release process.

  • Keep the Vite version and build configuration consistent across releases.

Advantages and Limitations of Vite Library Mode

Advantages

Limitations

Provides a straightforward library build configuration

Package metadata still requires careful setup

Supports multiple output formats

Every supported format needs testing

Integrates with Vite's build pipeline

Does not replace package publishing and release management

Supports dependency externalization

External dependencies must be declared and resolved correctly

Works well for reusable JavaScript utilities and components

Complex libraries may need additional type declaration and compatibility configuration

If your library is written in TypeScript, you should also plan how declaration files will be generated and included. Vite's standard JavaScript build does not automatically provide a complete TypeScript declaration-generation workflow in every configuration. A separate declaration-generation step or a suitable plugin may be needed.

Summary

Vite library mode helps developers turn reusable JavaScript code into a distributable package. By configuring an entry point, selecting output formats, defining package exports, and managing dependencies, you can create a library that other applications can import.

The build itself is only one part of the process. Package metadata, module compatibility, dependency handling, and testing the packed artifact are equally important.

Start with the formats your consumers actually need, verify the generated files, test the package in a separate project, and publish only after the release checks pass. This approach makes JavaScript libraries easier to maintain and more reliable for other developers to use.