| |

Vue.js 97 🟢 Building Component Libraries with Vite Library Mode (vite build –lib)

⏱️ Estimated read: 9 min | |

A Vue component library is not an application. The default Vite build treats the project as an app: it reads index.html, bundles every dependency, and produces a deployable website. Library mode changes the build target from a deployable page to a publishable package. The build.lib option in vite.config.ts tells Vite to use the library entry point instead of index.html, to externalize peer dependencies like Vue instead of bundling them, and to emit formats that consumers can import without pulling in a second copy of the framework .

This chapter covers the build.lib configuration, the externalization rule that prevents duplicate framework instances, the type declaration generation that makes the library usable from TypeScript, and the package.json exports that define the public API.

Key point: A component library must not bundle Vue. If Vue is bundled into the library, the consumer application ends up with two Vue instances—one from the library and one from the app—which produces broken reactivity, failed component resolution, and “Invalid hook call” errors. The rollupOptions.external array is what prevents this .


Why library mode exists

The duplicate framework problem. In application mode, Vite bundles every import into the output. A library that imports ref from vue would include the Vue runtime in its dist output. A consumer application that also uses Vue would then have two copies: the library’s bundled copy and the app’s own copy. Vue’s reactivity relies on a single instance; two instances break provide/inject, global components, and the component tree itself .

The tree-shaking problem. An application build produces a single bundled file with no named exports. A consumer that imports one component from the library pulls in the entire bundle, because the bundler cannot see the individual exports. Library mode preserves the module structure and the named exports, so the consumer’s bundler can tree-shake unused components .

The format problem. An application build targets the browser. A library must target the consumer’s bundler (ES modules), older build tools (CommonJS), and CDN script tags (UMD). The build.lib.formats array controls which output formats are produced .


a. The build.lib Configuration

The build.lib object requires an entry field and optionally specifies the global name, formats, and file names. The entry is the library’s public API file—typically src/index.ts—which exports every component and composable that the library makes available .

// vite.config.ts
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyUI',
      formats: ['es', 'cjs'],
      fileName: (format) => `my-ui.${format}.js`,
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: { vue: 'Vue' },
      },
    },
  },
})

The name field is required only when formats includes umd or iife. It sets the global variable name that the UMD build exposes to the browser . The fileName function receives the format and returns the output file name; a common convention is my-ui.es.js and my-ui.cjs.js.

The formats array controls the output. es is the ES module format for modern bundlers. cjs is CommonJS for Node.js and older tools. umd is for direct <script> usage. Most Vue component libraries ship es and cjs, and add umd only if CDN usage is a requirement .


b. Externalizing Vue and Peer Dependencies

The rollupOptions.external array tells the bundler which imports to leave as external references instead of bundling. For a Vue component library, vue must be in this array. If the library uses vue-router or pinia, those should be externalized as well if they are declared as peer dependencies .

rollupOptions: {
  external: ['vue', 'vue-router'],
  output: {
    globals: {
      vue: 'Vue',
      'vue-router': 'VueRouter',
    },
  },
}

The globals object maps the external module names to the global variables that the UMD build expects. This is only relevant for the umd or iife formats; the ES and CJS builds use the module system’s own resolution.

The package.json should declare Vue as a peerDependency, not a dependency. A peer dependency tells the consumer that they must provide Vue themselves. The npm 7+ behavior auto-installs peer dependencies, but the library’s own bundle does not include Vue .

{
  "peerDependencies": {
    "vue": "^3.5.0"
  }
}

c. Type Declarations and package.json Exports

A component library written in TypeScript must ship .d.ts files for consumers to get type checking. vite-plugin-dts generates these declarations from the .ts and .vue source files during the build .

// vite.config.ts
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    vue(),
    dts({
      include: ['src'],
      rollupTypes: true,
    }),
  ],
  // ...
})

The rollupTypes: true option merges all declarations into a single file, which simplifies the package.json types field. Without it, the declarations follow the source structure and the types field must point to the entry declaration file .

The package.json exports field defines the public API. It maps the import specifier to the file for each format, and the types condition to the declaration file.

{
  "name": "my-ui",
  "type": "module",
  "main": "./dist/my-ui.cjs.js",
  "module": "./dist/my-ui.es.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/my-ui.es.js",
      "require": "./dist/my-ui.cjs.js"
    },
    "./style.css": "./dist/my-ui.css"
  },
  "files": ["dist"],
  "sideEffects": ["**/*.css"]
}

The sideEffects field tells the consumer’s bundler that CSS files must be preserved during tree-shaking. Without it, an import of my-ui/style.css might be dropped, and the library would render without styles .


Complete Example Session

// ============================================
// PART 1: LIBRARY ENTRY POINT
// ============================================
// src/index.ts
export { default as MyButton } from './components/MyButton.vue'
export { default as MyCard } from './components/MyCard.vue'
export { useMyComposable } from './composables/useMyComposable'


// ============================================
// PART 2: VITE CONFIGURATION
// ============================================
// vite.config.ts
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    vue(),
    dts({ include: ['src'], rollupTypes: true }),
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyUI',
      formats: ['es', 'cjs'],
      fileName: (format) => `my-ui.${format}.js`,
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: { vue: 'Vue' },
      },
    },
  },
})


// ============================================
// PART 3: PACKAGE.JSON
// ============================================
{
  "name": "my-ui",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/my-ui.cjs.js",
  "module": "./dist/my-ui.es.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/my-ui.es.js",
      "require": "./dist/my-ui.cjs.js"
    },
    "./style.css": "./dist/my-ui.css"
  },
  "files": ["dist"],
  "sideEffects": ["**/*.css"],
  "peerDependencies": {
    "vue": "^3.5.0"
  }
}


// ============================================
// PART 4: BUILD
// ============================================
// pnpm build
// Output:
// dist/my-ui.es.js
// dist/my-ui.cjs.js
// dist/index.d.ts
// dist/my-ui.css


// ============================================
// PART 5: CONSUMER USAGE
// ============================================
// import { MyButton } from 'my-ui'
// import 'my-ui/style.css'

The five parts trace the entry point, the Vite configuration, the package manifest, the build output, and the consumer usage. The critical configuration is Part 2: the external: ['vue'] prevents the duplicate framework problem, and dts generates the declarations.


Quick Reference

build.lib Fields

FieldPurpose
entryLibrary API entry point
nameGlobal variable name for UMD/IIFE
formatsOutput formats (es, cjs, umd, iife)
fileNameOutput file name pattern

Externalization

ModuleExternalPeer Dependency
vueYesYes
vue-routerYes, if usedYes
piniaYes, if usedYes
Lodash utilityDependsOptional

package.json Fields

FieldPurpose
mainCJS entry point
moduleESM entry point
typesTypeScript declarations
exportsPublic API contract
peerDependenciesHost packages
sideEffectsPreserve CSS

Best Practices

✅ Do This:

// Externalize Vue
rollupOptions: { external: ['vue'] }                                    // ✅
// Declare Vue as peer dependency
"peerDependencies": { "vue": "^3.5.0" }                                 // ✅
// Generate declarations with vite-plugin-dts
plugins: [dts({ include: ['src'] })]                                    // ✅
// Mark CSS as side effect
"sideEffects": ["**/*.css"]                                             // ✅

❌ Don’t Do This:

// Bundle Vue into the library
rollupOptions: {}  // no external                                          // ❌
// Use a single format only
"formats": ["es"]  // misses CJS consumers                                // ❌
// Omit the exports field
// Consumers cannot resolve the entry point                              // ❌

Common Pitfalls

PitfallWhy It HappensFix
Duplicate Vue instanceVue bundled into libraryexternal: ['vue']
Missing type declarationsvite-plugin-dts not configuredAdd the plugin
CSS dropped by tree-shakingsideEffects not set"sideEffects": ["**/*.css"]
Consumer cannot importexports field missingAdd the exports map
UMD build failsname field missingSet name for UMD

Real-World Examples

1. Library Entry

export { default as Button } from './components/Button.vue'

2. External Vue

rollupOptions: { external: ['vue'] }

3. Peer Dependency

"peerDependencies": { "vue": "^3.5.0" }

4. Type Declarations

dts({ include: ['src'], rollupTypes: true })

5. Exports Map

"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.es.js" } }

6. CSS Side Effect

"sideEffects": ["**/*.css"]

7. UMD Global

output: { globals: { vue: 'Vue' } }

8. Multiple Formats

formats: ['es', 'cjs']

9. File Name Pattern

fileName: (format) => `my-ui.${format}.js`

10. Consumer Import

import { Button } from 'my-ui'

Visual

Application Build vs. Library Build

┌──────────────────────────────────────────────────────────────┐
│  APPLICATION BUILD              LIBRARY BUILD                │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ index.html      │            │ src/index.ts    │         │
│  └────────┬────────┘            └────────┬────────┘         │
│           │                              │                   │
│           ▼                              ▼                   │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ Bundle all deps │            │ Externalize Vue │         │
│  │ (including Vue) │            │ (peer dep)      │         │
│  └────────┬────────┘            └────────┬────────┘         │
│           ▼                              ▼                   │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ Single JS file  │            │ ES + CJS + d.ts │         │
│  │ No named exports│            │ Named exports   │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  The library does not bundle the framework.                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Duplicate Vue Problem

┌──────────────────────────────────────────────────────────────┐
│  WITHOUT EXTERNAL                                             │
│                                                              │
│  my-ui bundle:  includes Vue runtime                         │
│  app bundle:    includes Vue runtime                         │
│         │                                                    │
│         ▼                                                  │
│  Two Vue instances → broken reactivity                       │
│                                                              │
│  WITH EXTERNAL                                                │
│                                                              │
│  my-ui bundle:  import { ref } from 'vue' (external)         │
│  app bundle:    provides Vue                                 │
│         │                                                    │
│         ▼                                                  │
│  One Vue instance → correct                                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Type Declaration Generation

┌──────────────────────────────────────────────────────────────┐
│  SOURCE                          OUTPUT                      │
│                                                              │
│  src/index.ts                    dist/index.d.ts             │
│  src/components/Button.vue       (types merged)              │
│  src/components/Card.vue                                     │
│         │                                                    │
│         ▼                                                  │
│  vite-plugin-dts                                            │
│  (rollupTypes: true)                                         │
│                                                              │
│  Consumers get TypeScript support for every export.          │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Library mode optionbuild.lib
Entry pointsrc/index.ts
Vue externalizationrollupOptions.external: ['vue']
Vue dependencypeerDependencies
Type declarationsvite-plugin-dts
CSS preservationsideEffects: ["**/*.css"]
Output formatses, cjs (and umd if needed)
Public API contractpackage.json exports

Key takeaways:

  • Library mode changes the build target from an application to a package. The build.lib option replaces index.html with the library entry point, and the output is a set of importable modules rather than a deployable website .
  • Vue must be externalized. The rollupOptions.external: ['vue'] array tells Vite not to bundle Vue into the library. Without it, the consumer ends up with two Vue instances, which breaks reactivity and component resolution .
  • Vue is a peer dependency, not a dependency. The library expects the consumer to provide Vue. The peerDependencies field declares this expectation, and the consumer’s package manager handles the installation .
  • vite-plugin-dts generates the type declarations. The plugin produces .d.ts files from the .ts and .vue sources. The rollupTypes: true option merges them into a single file, which simplifies the types field in package.json .
  • The exports field defines the public API. It maps the import specifier to the ES module, the CommonJS module, and the type declaration. The sideEffects field preserves CSS imports during tree-shaking .

Remember: A Vue component library is a package, not an application. The build must produce importable modules with named exports, externalize the framework, and ship type declarations. The build.lib configuration is the switch that changes Vite’s behavior from application bundling to library packaging. The external array is the rule that prevents the duplicate Vue instance. The exports field is the contract that tells the consumer how to import the library. When these three pieces are configured correctly, the library can be published to a registry, installed in an application, and tree-shaken like any other dependency.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!