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:
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:
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:
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:
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:
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:
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:
{
"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:
npm run build
Vite generates the configured output files in the dist directory.
The resulting structure should look similar to this:
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:
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:
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:
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:
import { formatCurrency } from "@example/format-utils";
console.log(formatCurrency(1250));
If CommonJS support is part of your package contract, test it separately:
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:
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.
Join the conversation! Your thoughts help the community grow.