vite-mastery

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.

Vite 8.1Stable

Library Mode Is About Being Imported

Application output is deployed to browsers:

text
index.html -> assets/index-HASH.js -> assets/index-HASH.css

Library output is published to npm and imported by another project:

ts
import { Button } from "@myorg/ui"
DimensionApp modeLibrary mode
EntryHTMLJS/TS public API
Consumerbrowseranother bundler/runtime
Filenameshash-friendlystable paths
Dependenciesoften bundledframeworks often peer
package.jsonnot browser entrydefines import surface
CSSinjected/extracted for appdelivery strategy required

The question is not just "does it build?" It is "can consumers resolve the right JS and types?"

Minimal Config

ts
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:

ts
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

ts
lib: {
  entry: {
    index: "src/index.ts",
    react: "src/react.ts",
    theme: "src/theme.ts",
  },
}

Multiple entries are useful when you intend to expose subpaths:

ts
import { Button } from "@myorg/ui/react"
import { tokens } from "@myorg/ui/theme"

formats

FormatUse case
esmodern bundlers and tree-shaking
cjstools still using require()
umdCDN script global
iiferare 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:

ts
lib: {
  name: "MyLibrary",
  formats: ["es", "umd"],
}

It is not central for pure es / cjs output.

fileName and cssFileName

ts
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:

ts
export default defineConfig({
  build: {
    lib: {
      entry: "src/index.ts",
      formats: ["es", "cjs"],
    },
    rolldownOptions: {
      external: ["react", "react-dom", "react/jsx-runtime"],
    },
  },
})
json
{
  "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

json
{
  "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:

ts
import { Button } from "@myorg/ui"
import { tokens } from "@myorg/ui/theme"
import "@myorg/ui/style.css"

CSS Delivery

StrategyUsageFits
separate CSSimport "@myorg/ui/style.css"classic component libraries
per-component side effect CSScomponent entry imports CSSsmall internal libraries
runtime stylesJS generates styleshighly dynamic themes

If consumers import CSS explicitly:

json
{
  "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.

See 10.2 · Type Declarations.

Verify with a Consumer

bash
pnpm build
pnpm pack

Then install the tarball in a temporary Vite app:

bash
pnpm create vite@latest consumer --template react-ts
cd consumer
pnpm add ../myorg-ui-1.0.0.tgz
pnpm dev
pnpm build

Verify import types, CSS loading, production build, no duplicate React, and any declared CJS path.

Complete React Library Example

ts
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

  1. Why should a component library externalize React and list it as a peer dependency?
  2. What is the difference between exports and main?
  3. Why might CSS need to be listed in sideEffects?
  4. How should multi-entry build.lib.entry map to package.json exports?
  5. Why verify a library in a separate consumer project?
ts
// 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"