10.1 · difficulty 2/4 · 18 min read
`build.lib` Configuration Reference
Vite library mode is not just a different entry. Learn build.lib, formats, external, exports, CSS output, peerDependencies, and consumer verification.
Library Mode Is About Being Imported
Application output is deployed to browsers:
index.html -> assets/index-HASH.js -> assets/index-HASH.cssLibrary output is published to npm and imported by another project:
import { Button } from "@myorg/ui"| Dimension | App mode | Library mode |
|---|---|---|
| Entry | HTML | JS/TS public API |
| Consumer | browser | another bundler/runtime |
| Filenames | hash-friendly | stable paths |
| Dependencies | often bundled | frameworks often peer |
| package.json | not browser entry | defines import surface |
| CSS | injected/extracted for app | delivery strategy required |
The question is not just "does it build?" It is "can consumers resolve the right JS and types?"
Minimal Config
import { resolve } from "node:path"
import { defineConfig } from "vite"
export default defineConfig({
build: {
lib: {
entry: resolve("src/index.ts"),
formats: ["es", "cjs"],
fileName: (format) => `index.${format === "es" ? "js" : "cjs"}`,
},
},
})src/index.ts should export only public API:
export { Button } from "./components/Button"
export { createTheme } from "./theme"
export type { ButtonProps, ThemeOptions } from "./types"If a deep path should be public, expose it explicitly through exports.
build.lib
entry
lib: {
entry: {
index: "src/index.ts",
react: "src/react.ts",
theme: "src/theme.ts",
},
}Multiple entries are useful when you intend to expose subpaths:
import { Button } from "@myorg/ui/react"
import { tokens } from "@myorg/ui/theme"formats
| Format | Use case |
|---|---|
es | modern bundlers and tree-shaking |
cjs | tools still using require() |
umd | CDN script global |
iife | rare direct browser global |
Modern packages usually start with ["es"] or ["es", "cjs"]. Add UMD/IIFE only when you really support globals.
name
name matters for umd and iife:
lib: {
name: "MyLibrary",
formats: ["es", "umd"],
}It is not central for pure es / cjs output.
fileName and cssFileName
lib: {
fileName: (format, entryName) => `${entryName}.${format === "es" ? "js" : "cjs"}`,
cssFileName: "style",
}Published package filenames should be stable because exports points to them.
External and Peer Dependencies
React libraries should not bundle React:
export default defineConfig({
build: {
lib: {
entry: "src/index.ts",
formats: ["es", "cjs"],
},
rolldownOptions: {
external: ["react", "react-dom", "react/jsx-runtime"],
},
},
}){
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"devDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}Externalizing peer dependencies avoids duplicate React copies, lets the app control versions, reduces package size, and prevents hooks/context breakage.
Not every dependency should be external. Small implementation dependencies can be bundled. Frameworks and singleton host dependencies should be peers.
exports Is the Public API
{
"name": "@myorg/ui",
"type": "module",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./theme": {
"types": "./dist/theme.d.ts",
"import": "./dist/theme.js",
"require": "./dist/theme.cjs"
},
"./style.css": "./dist/style.css"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts"
}Paths not listed in exports are not stable public imports. main and module are legacy compatibility fields, not replacements for exports.
Consumer code:
import { Button } from "@myorg/ui"
import { tokens } from "@myorg/ui/theme"
import "@myorg/ui/style.css"CSS Delivery
| Strategy | Usage | Fits |
|---|---|---|
| separate CSS | import "@myorg/ui/style.css" | classic component libraries |
| per-component side effect CSS | component entry imports CSS | small internal libraries |
| runtime styles | JS generates styles | highly dynamic themes |
If consumers import CSS explicitly:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./style.css": "./dist/style.css"
},
"sideEffects": ["**/*.css"]
}sideEffects prevents CSS imports from being incorrectly tree-shaken.
Type Declarations
Vite bundles JS/CSS/assets. It does not solve complex declaration bundling by itself. Pair library mode with tsc --emitDeclarationOnly, vue-tsc, tsdown, rolldown-plugin-dts, or API Extractor.
Verify with a Consumer
pnpm build
pnpm packThen install the tarball in a temporary Vite app:
pnpm create vite@latest consumer --template react-ts
cd consumer
pnpm add ../myorg-ui-1.0.0.tgz
pnpm dev
pnpm buildVerify import types, CSS loading, production build, no duplicate React, and any declared CJS path.
Complete React Library Example
import { resolve } from "node:path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: {
index: resolve("src/index.ts"),
theme: resolve("src/theme.ts"),
},
formats: ["es", "cjs"],
fileName: (format, entryName) => `${entryName}.${format === "es" ? "js" : "cjs"}`,
cssFileName: "style",
},
rolldownOptions: {
external: ["react", "react-dom", "react/jsx-runtime"],
output: {
assetFileNames: "assets/[name][extname]",
},
},
},
})Check Yourself
- Why should a component library externalize React and list it as a peer dependency?
- What is the difference between
exportsandmain? - Why might CSS need to be listed in
sideEffects? - How should multi-entry
build.lib.entrymap topackage.json exports? - Why verify a library in a separate consumer project?
// Configure a utility library:
// - entries: index / date / string
// - output ESM + CJS
// - lodash-es is an implementation detail and may be bundled
// - zod is provided by consumers, so external + peerDependencies
// - exports exposes "."、"./date"、"./string"