Building Reusable JavaScript Packages with Vite Library Mode
Learn how to use Vite's library mode to build reusable ES module and UMD packages from a single configuration, with practical guidance on externals, package.json fields, and verification.
16 Aug 2026, 23:18 UTC

When you publish a JavaScript library, you typically need to support two consumption patterns: modern bundlers that prefer ES modules, and older environments that load a script tag and expect a global variable. Vite's library mode lets you produce both artifacts from one configuration file, using the same Rollup pipeline that powers Vite's application builds.
The useful takeaway is that you do not need a separate build tool for packaging. You set build.lib in vite.config.js, list the formats you want, and mark peer dependencies as external so they are not duplicated inside your bundle.
How Vite library mode works
In normal application mode, Vite expects an HTML entry point and injects your scripts into it. Library mode replaces that expectation: you provide a JavaScript entry point, and Vite hands the module graph to Rollup, which performs tree-shaking, code splitting decisions, and output generation. The result is a set of files in dist/ that you can publish directly.
Two concepts matter here. An ES module uses import and export statements and is understood by modern bundlers and browsers with type="module". A UMD bundle (Universal Module Definition) wraps your code in a factory that detects whether it is running in CommonJS, AMD, or a browser global environment, then exposes your API accordingly.
A worked configuration
The following vite.config.js builds an ES module and a UMD bundle from src/index.js, treats lodash as external, and maps it to the global _ variable in the UMD output:
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.js'),
name: 'MyLib',
formats: ['es', 'umd'],
fileName: (format) => `mylib.${format}.js`,
},
rollupOptions: {
external: ['lodash'],
output: {
globals: { lodash: '_' },
},
},
},
});
Run the build from your project root with npm run build or npx vite build. No special permissions are required beyond normal filesystem write access to dist/. The name field is required for UMD because it defines the global variable name; it is ignored for ES modules. The fileName callback receives the format string (es or umd) so you can generate distinct filenames.
After the build completes, inspect dist/. You should see mylib.es.js and mylib.umd.js, along with .map source-map files. Open mylib.es.js and look for export statements near the end of the file. Open mylib.umd.js and look for a wrapper that begins with (function (root, factory) { or similar, which is the UMD pattern. If you see lodash code inlined inside either file, the external array is not working as expected.
Limits of library mode
Library mode is deliberately narrower than application mode. It does not accept an HTML entry point, and it does not start a development server for the library itself. If you want hot-module replacement while developing the library, you need a separate demo or playground application inside the same repository that imports the library source.
Tree-shaking also behaves differently by format. Rollup can eliminate unused exports from the ES module because the import/export graph is static. The UMD bundle is a single closure assigned to a global, so a consumer loading it via <script> cannot tree-shake it. If your library has side effects, declare them accurately in package.json with the sideEffects field so downstream bundlers can safely drop unused code from the ES module.
Assets are another constraint. CSS, images, or other files imported by your library are processed by Vite's asset pipeline, but their URLs are rewritten relative to the built output. If you publish only the JavaScript files without the asset directory, consumers will encounter 404 errors. Either emit assets alongside the bundle or document that consumers must handle those imports themselves.
Common mistakes
Forgetting to mark dependencies as external
If you omit external: ['lodash'] from rollupOptions, Rollup bundles lodash into your output. This inflates the file size and, worse, can create duplicate copies if the consumer also depends on lodash. Every runtime dependency that the consumer is expected to provide should appear in external. For UMD, also map it in output.globals so the factory knows which global variable to reference.
Missing package.json entry points
Building the files is only half the job. Your package.json must tell consumers which file to load. A typical mapping looks like this:
{
"main": "./dist/mylib.umd.js",
"module": "./dist/mylib.es.js",
"exports": {
".": {
"import": "./dist/mylib.es.js",
"require": "./dist/mylib.umd.js"
}
}
}
The main field points to the CommonJS/UMD build for older tools; module points to the ES build for modern bundlers. The exports map is increasingly important because it takes precedence in Node.js and modern bundlers and lets you provide conditional entry points.
Relative paths and aliases
If your source uses relative imports like import { helper } from '../utils/helper', those paths are resolved at build time and usually work fine. Problems arise when you rely on non-standard resolution, such as bare imports that assume a specific node_modules layout or custom aliases that are not mirrored in resolve.alias. When the package is installed by a consumer, those assumptions break. Keep the entry graph relative to the entry file, or configure resolve.alias explicitly.
UMD global name collisions
The name field becomes a property on window in browser environments. If you choose a generic name like Utils and another script on the page already defines window.Utils, one will overwrite the other. Pick a unique, namespaced global name.
Verifying the result
A practical check is to consume the built package in a separate test project. For the ES module, run npm install /path/to/your/library in the test project, then add import { foo } from 'your-library' and confirm the API resolves. For the UMD bundle, copy mylib.umd.js into an HTML file, add a <script> tag, and verify that window.MyLib exposes the expected methods. Finally, open the generated .map files in your browser's developer tools and confirm that stack traces point to your original src/ files rather than the minified bundle.
Library mode is not a replacement for a full application build, but for packaging a reusable module it removes the need to hand-write Rollup configuration. The main discipline is keeping your dependency boundaries explicit: declare what is external, map the globals, and point package.json at the right files.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.