Why Dual Packages Matter
The JavaScript ecosystem currently operates with two module systems:
- CommonJS (CJS) - The traditional Node.js system using require()
- ES Modules (ESM) - The modern standard using import/export
This divide creates real challenges. For example, when popular libraries like chalk
transitioned to ESM-only in version 5, many existing CommonJS projects faced compatibility issues. While ESM is the future, the reality is that numerous production systems still rely on CJS.
The Solution: Dual-Package Support
By building libraries that support both formats, we can:
- Maintain backward compatibility
- Support modern JavaScript workflows
- Reduce ecosystem fragmentation
- Provide a smoother migration path
Here's a straightforward approach to implement dual-package support.
Implementation Guide
Project structure
your-lib/
├── dist/ # Generated output (added to .gitignore)
├── src/ # Source files in TypeScript/ES6
│ ├── utils.ts # Library functionality
│ └── index.ts # Main entry point
├── package.json # Dual-package configuration
├── rollup.config.js # Build setup
├── tsconfig.declarations.json # TS declarations config
└── tsconfig.json # TS base config
TypeScript Support
We use two tsconfig
files for optimal compilation:
Base Config (tsconfig.json
)**
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"target": "ESNext",
"module": "Preserve",
"moduleResolution": "bundler",
"rootDir": "src"
},
"include": ["src/**/*.ts"]
}
Key features:
- Handles the main transpilation
- Outputs modern JavaScript (ESM by default)
- Used by Rollup during build
Declarations Config (tsconfig.declarations.json
)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "dist/types"
}
}
Key features:
- Generates type definitions (.d.ts files)
- Runs separately from main build
- Ensures clean type output without JS files
Build Configuration (rollup.config.js
)
import typescript from '@rollup/plugin-typescript';
export default {
input: 'src/index.ts',
output: [
{
dir: 'dist/esm',
format: 'esm',
entryFileNames: '[name].mjs',
},
{
dir: 'dist/cjs',
format: 'cjs',
entryFileNames: '[name].cjs',
},
],
plugins: [
typescript({
tsconfig: './tsconfig.json',
}),
],
};
Key features:
- Creates separate ESM (.mjs) and CJS (.cjs) builds
- Uses TypeScript plugin for compilation
- Maintains clean output structure
Configure package.json
The package.json
file is crucial for dual-package support. Here are the key configuration aspects:
Module System Configuration:
{
"type": "module",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"require": "./dist/cjs/index.cjs",
"import": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts"
}
}
}
It's critical to properly separate dependencies for build tools (Rollup, TypeScript, etc.)
devDependencies
:
{
"@rollup/plugin-typescript": "^12.1.2",
"@types/node": "^22.14.1",
"rollup": "^4.40.0",
"tslib": "^2.8.1",
"typescript": "^5.8.3"
}
Use dependencies
only for packages your library actually uses at runtime.
Why this separation matters:
- Installation Efficiency: npm/yarn won't install devDependencies for end users
- Smaller Bundle Size: Prevents unnecessary packages from being included
- Clear Dependency Documentation: Shows what's needed for building vs running
- Security: Reduces potential attack surface in production
Best Practices:
- Only include truly required packages in dependencies
- Keep all build/test tools in
devDependencies
- Specify exact versions (or use
^
) for important compatibility - Run
npm install --production
to test your runtime dependencies
Remember: Well-structured dependencies make your library more reliable and easier to maintain.
For a complete working example, check out this boilerplate project on GitHub:
👉 Dual-Package Library Example
It includes all the configurations discussed, so you can fork it or use it as a reference.
If you found this guide helpful:
⭐ Give it a star on GitHub to support the project!
💬 Leave a comment below with your thoughts or questions.
🔄 Share with others who might benefit from it.
Happy coding, and may your libraries work everywhere! 🚀
With respect,
Evgeny Kalkutin - kalkutin.dev