Hello! Welcome back to our course on developing a modern JavaScript framework.
In our last lesson, we crafted a robust tsconfig.json. We configured it for maximum type safety and to generate the crucial declaration files (.d.ts) that describe our library's API to other TypeScript projects.
Today, we'll configure our bundler, Vite, to take our TypeScript source code and package it for distribution. Our goal is to create a library that can be used in a wide variety of JavaScript projects, from modern web applications to older Node.js environments.
By the end of this lesson, you will be able to configure Vite in library mode to produce both ESM and CommonJS outputs.
1. Why Two Formats? ESM and CommonJS
As an experienced developer, you've likely encountered JavaScript's two major module systems:
- ECMAScript Modules (ESM): The modern, standardized system using
importandexportsyntax. It's supported natively in modern browsers and Node.js. - CommonJS (CJS): The traditional system used by Node.js, with
require()andmodule.exports. Many legacy projects and tools still rely on it.
To ensure our library has the widest possible reach, we need to provide bundles for both systems. Vite's library mode is designed to make this straightforward.
2. Introducing Vite's Library Mode
Vite has a dedicated configuration for building libraries rather than applications. This mode uses Rollup (Vite's underlying bundler) with a preset optimized for creating distributable packages.
Let's start by looking at the official Vite documentation to understand the core concepts.
Building for Production - Library Mode
The official Vite documentation provides the best overview of library mode. We'll use this as our foundation.
Please read the 'Library Mode' section. Focus on the structure of the build.lib object in the vite.config.js example. Note the entry, name, and fileName properties.
As you saw, the build.lib object is the control center for our library build. It tells Vite what to build and what to name the output files.
3. Configuring vite.config.ts
Now, let's apply this to our project. We'll configure Vite to build our library from our src/index.ts entry point and output both ESM and CJS formats.
Create a library using Vite lib mode
This video provides a clear, practical walkthrough of setting up the vite.config.ts file for library mode. It will help visualize the concepts from the documentation.
Watch from 05:33 to 09:46. The first part covers the initial setup of build.lib. The second part shows how to explicitly define the output formats, which is exactly what we need to do.
Based on this, let's update your vite.config.ts. We need to import resolve from Node's path module and add the build configuration.
Open vite.config.ts and modify it to look like this:
import { defineConfig } from 'vite'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
// Could also be a dictionary or array of multiple entry points
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyObservableFramework', // Choose a name for your library
// the proper extensions will be added
fileName: (format) => `my-observable-framework.${format}.js`,
formats: ['es', 'cjs'],
},
},
})
entry: We useresolve(__dirname, 'src/index.ts')to create an absolute path to our main source file.name: This is the PascalCase name for your library, which is used for UMD builds if you choose to generate one.fileName: We provide a function to dynamically name our output files based on the format (e.g.,my-observable-framework.es.js).formats: We explicitly tell Vite to generate bundles fores(ESM) andcjs(CommonJS).
4. Including TypeScript Declaration Files
In the last lesson, we configured tsc to generate .d.ts files. However, Vite's build process only transpiles—it doesn't run the full TypeScript type checker or emit declarations. We need a plugin to integrate this step.
The vite-plugin-dts is the standard tool for this.
Creating a TypeScript Package with Vite | Onur Önder
This blog post, 'Creating a TypeScript Package with Vite', explains why an extra plugin is needed for declaration files and how to use it.
Read the first two paragraphs of the 'Bundling the Package' section. The key takeaway is the need for vite-plugin-dts to bundle our .d.ts files.
First, install the plugin as a development dependency:
npm install -D vite-plugin-dts
Next, update your vite.config.ts to include the plugin:
import { defineConfig } from 'vite'
import { resolve } from 'path'
import dts from 'vite-plugin-dts' // Import the plugin
// https://vitejs.dev/config/
export default defineConfig({
plugins: [dts()], // Add the plugin
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyObservableFramework',
fileName: (format) => `my-observable-framework.${format}.js`,
formats: ['es', 'cjs'],
},
},
})
Now, when you run the build, this plugin will execute tsc to generate the declaration files and place them correctly in the dist folder.
5. Building and Inspecting the Output
With the configuration complete, let's run the build process.
In your terminal, execute:
npm run build
You should see a new dist folder in your project root. Go ahead and inspect its contents. You should find:
my-observable-framework.es.js: The ESM bundle.my-observable-framework.cjs.js: The CommonJS bundle.index.d.ts: The main TypeScript declaration file for your library.
Your output should look similar to the structure shown in this image, with different module formats generated by the build process.

6. Guiding Consumers with package.json
Creating the bundles is only half the battle. We now need to update our package.json to tell package managers and build tools which file to use in which context.
Create a library using Vite lib mode
This final clip from the 'Create a library using Vite lib mode' video clearly explains how to configure package.json to correctly expose your library's different formats.
Watch from 16:07 to 17:44. Pay close attention to the files, main, module, and exports fields. This is the standard way to publish a modern dual-format library.
The Vite documentation also provides an excellent template for this. Open your package.json and add/modify the following fields. Make sure to replace the filenames with the ones you defined in your vite.config.ts.
{
"name": "my-observable-framework",
"private": false, // Make sure your package is not private
"version": "0.0.1",
"type": "module", // Signifies this package uses ES Modules primarily
"files": [
"dist"
],
"main": "./dist/my-observable-framework.cjs.js",
"module": "./dist/my-observable-framework.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/my-observable-framework.es.js",
"require": "./dist/my-observable-framework.cjs.js",
"types": "./dist/index.d.ts"
}
},
// ... rest of your package.json
}
Let's break down these essential fields:
"type": "module": Tells Node.js and bundlers that your package's.jsfiles are ES modules."files": ["dist"]: An explicit whitelist of files and directories to be included when your package is published to npm. This prevents accidental inclusion of source files or configuration."main": The entry point for CommonJS environments (e.g.,require('my-observable-framework'))."module": A widely used, though non-standard, field that points to the ESM entry point. It's used by bundlers like Webpack and Rollup."types": Points to the main declaration file, enabling TypeScript support for your library's users."exports": The modern, standard way to define package entry points. It allows for "conditional exports," providing the correct file forimport(ESM) versusrequire(CJS) resolution. This field takes precedence overmainandmodulein modern tooling.
Conclusion
Congratulations! You have successfully configured Vite to build a professional, dual-format TypeScript library. Your project is now set up to produce clean, distributable code that can be consumed by a vast range of JavaScript projects.
Key Takeaways:
- Vite's library mode (
build.lib) is a powerful feature for bundling libraries. - Producing both ESM (
.es.js) and CJS (.cjs.js) bundles ensures maximum compatibility. - The
vite-plugin-dtsis essential for including TypeScript declaration files in your Vite build. - A correctly configured
package.jsonwithmain,module,types, and especially theexportsfield is critical for your library to work correctly for consumers.
Next Lesson Preview:
Currently, if our library used any external dependencies (like Lodash or date-fns), Vite would bundle them directly into our output files. This is usually not what we want for a library. In the next lesson, we will refine the Vite build by configuring Rollup options to externalize peer dependencies, ensuring our library remains small and avoids version conflicts in consumer projects.
Can't find a good explanation? Sign up and we'll make it for you
Sign up