Dual-module TypeScript libraries — maximum compatibility, automatic dual builds
• Usage • Configuration • Comparison • FAQ •
This is a configuration template that works with Presetter, the configuration management tool.
# Install hybrid preset
npm i -D presetter presetter-preset-hybrid
# Create presetter.config.ts
echo "export { default } from 'presetter-preset-hybrid';" > presetter.config.ts
# Bootstrap your project
npx presetter bootstrapYour project now generates both CommonJS and ESM outputs from single TypeScript source — maximum Node.js ecosystem compatibility!
Modern Node.js ecosystem is divided: legacy tools need CommonJS, modern environments prefer ESM, and library authors face an impossible choice. Choose CommonJS and miss modern optimizations. Choose ESM and break compatibility.
What if you could have both?
| Challenge | Single Format | With preset-hybrid |
|---|---|---|
| Legacy Node.js | ❌ ESM breaks old versions | ✅ CommonJS works everywhere |
| Modern Bundlers | ❌ CJS misses tree-shaking | ✅ ESM enables optimizations |
| Library Adoption | ✅ Works in any environment | |
| Package Complexity | ✅ Automated dual builds | |
| Maintenance Burden | ✅ Single source, dual output | |
| Import Compatibility | ❌ Breaking changes | ✅ Seamless for all users |
presetter-preset-hybrid is a configuration template that provides dual CommonJS/ESM TypeScript library builds.
When used with Presetter (the configuration management tool), this preset extends preset-essentials with sophisticated dual-module generation, automatically creating both .js (CommonJS) and .mjs (ESM) outputs from your single TypeScript codebase.
- 🔄 Dual Builds: Generates both CommonJS and ESM from single source
- 🛠️ Automatic Transforms: Handles import extensions,
__dirnameconversion, path aliases - 📦 Package Integration: Perfect dual-module package.json structure
- ⚡ Modern DX: Write TypeScript once, support all Node.js environments
- 🎯 Library-First: Optimized for npm packages and open-source libraries
- 🚀 Zero Config: Complex dual builds work perfectly out of the box
Creating dual CommonJS/ESM packages manually is extraordinarily complex:
- Multiple TypeScript configs: Need separate CommonJS and ESM compilation targets
- Import transformations: ESM requires
.jsextensions,__dirnamedoesn't exist - Package.json complexity: Conditional exports, dual entry points, proper module fields
- Build orchestration: Sequential builds, post-processing, source map handling
- Path alias resolution: TypeScript paths must work in both output formats
Getting it right requires mastering TypeScript compiler edge cases, Node.js module systems, and complex build tooling.
# Before: Manual dual-module setup
my-library/
├── tsconfig.json ← Single config, wrong for dual builds
├── tsconfig.cjs.json ← Missing or incorrect CommonJS config
├── tsconfig.esm.json ← Missing or incorrect ESM config
├── webpack.config.js ← Complex bundling setup
├── src/
│ └── index.ts ← Import paths break in one format
└── lib/
- ├── index.js ← Only CommonJS OR only ESM
- └── index.d.ts ← Single declaration file
# After: Generated from hybrid template
my-library/
+├── presetter.config.ts ← References this hybrid preset template
├── package.json ← Perfect dual-module structure with exports
+├── tsconfig.json ← Generated base configuration
+├── tsconfig.cjs.json ← Generated CommonJS build config
+├── tsconfig.mjs.json ← Generated ESM build config
+├── All dev tools ← Generated with dual-module awareness
├── src/
│ └── index.ts ← Import paths work in both formats
└── lib/
+ ├── index.js ← CommonJS output (require/exports)
+ ├── index.mjs ← ESM output (import/export)
+ └── index.d.ts ← TypeScript declarations for both- Dual TypeScript Compilation — Separate
tsconfig.cjs.jsonandtsconfig.mjs.jsonwith format-specific settings - Automated Transformations —
tsc-esm-fixhandles import extensions,__dirnameconversion,.mjsrenaming - Sequential Build Pipeline — ESM build → CommonJS build → alias resolution → transformations
- Package Structure — Perfect dual-module package.json with conditional exports
- Maximum compatibility: Works in any Node.js environment or bundler
- Automatic transformations: No manual import fixing or environment-specific code
- Library distribution: Perfect for npm packages that need broad adoption
- Single codebase: Write TypeScript once, get both CommonJS and ESM
- Production ready: Handles all the edge cases and gotchas of dual modules
Important distinction:
| Component | Role | What it does |
|---|---|---|
| Presetter | Configuration management tool | CLI that processes presets, generates config files, executes scripts |
| presetter-preset-hybrid | Dual-module configuration template | Extends essentials with sophisticated dual CommonJS/ESM builds |
Think of it like:
- Presetter = The engine that builds houses
- This preset = The blueprint for a sophisticated, dual-compatible foundation
This preset extends preset-essentials with dual-module build capabilities. For advanced usage, customization, and troubleshooting, visit the main Presetter documentation.
// presetter.config.ts
export { default } from 'presetter-preset-hybrid';npm install
# Perfect dual-module configuration generated automatically
# Write TypeScript, get both CommonJS AND ESM output!That's it! TypeScript compiles to both formats. Your library works everywhere.
// presetter.config.ts
import { preset } from 'presetter';
import hybrid from 'presetter-preset-hybrid';
export default preset('my-hybrid-library', {
extends: [hybrid],
override: {
variables: {
target: 'ES2020', // Custom compilation target
},
assets: {
'tsconfig.cjs.json': {
compilerOptions: {
target: 'ES2017', // Conservative CommonJS target
},
},
'tsconfig.mjs.json': {
compilerOptions: {
target: 'ES2022', // Modern ESM target
},
},
},
},
});Need more customization options? Check the main Presetter documentation for complete guides on overrides, extensions, and advanced configurations.
This preset extends preset-essentials with dual-module build capabilities:
| Configuration | Purpose | Hybrid Features |
|---|---|---|
| TypeScript Configs | Dual compilation | Separate CommonJS and ESM tsconfig files |
| Build Pipeline | Sequential builds | ESM → CommonJS → transformations → aliases |
| Package Structure | Dual exports | Conditional exports for require/import |
| Transformations | Format compatibility | Import extensions, __dirname conversion, .mjs renaming |
{
"extends": "./tsconfig.build",
"compilerOptions": {
"module": "commonjs",
"target": "ES2017",
"moduleResolution": "node"
}
}{
"extends": "./tsconfig.build",
"compilerOptions": {
"module": "ES2022",
"target": "ES2022"
}
}Inherited from preset-essentials with hybrid-optimized defaults:
| Variable | Default | Description |
|---|---|---|
source |
"src" |
Source code directory |
output |
"lib" |
Build output directory |
test |
"spec" |
Test files directory |
target |
"ES2022" |
Base TypeScript compilation target |
# Generated build scripts
build:typescript: run-s build:typescript:mjs build:typescript:cjs build:typescript:alias
build:typescript:mjs: run-s build:typescript:mjs:tsc build:typescript:mjs:fix
build:typescript:mjs:tsc: tsc -p tsconfig.mjs.json
build:typescript:mjs:fix: tsc-esm-fix --sourceMap --target {output} --ext .mjs
build:typescript:cjs: run-s build:typescript:cjs:*
build:typescript:cjs:tsc: tsc -p tsconfig.cjs.json- Import Extension Addition:
import './foo'→import './foo.js' - ESM Global Replacement:
__dirname→import.metaequivalents - File Extension Renaming:
.js→.mjsfor ESM outputs - Path Alias Resolution: TypeScript paths work in both formats
{
"main": "lib/index.js", // CommonJS entry
"module": "lib/index.mjs", // ESM entry
"types": "lib/index.d.ts", // TypeScript definitions
"exports": {
".": {
"require": "./lib/index.js", // CommonJS conditional export
"import": "./lib/index.mjs", // ESM conditional export
},
},
}| Metric | Single Format | With preset-hybrid |
|---|---|---|
| Build time | Fast (single target) | Moderate (dual builds) |
| Bundle compatibility | Limited | Universal |
| Tree shaking | Format-dependent | Both CJS + ESM optimized |
| Package adoption | Restricted | Maximum |
| User experience | May break | Seamless |
| Environment | Support |
|---|---|
| Node.js | CommonJS: ≥8, ESM: ≥14 |
| Bundlers | Universal (webpack, Rollup, Vite, etc.) |
| Tools | Works with both CommonJS and ESM tooling |
| TypeScript | ≥ 5.0 |
presetter-preset-essentials- Core TypeScript development toolkit
- Broad Node.js ecosystem compatibility
- Modern bundler optimization support
- Seamless user experience across environments
| Feature | preset-hybrid | preset-cjs | preset-esm |
|---|---|---|---|
| CommonJS Support | ✅ Native | ✅ Native | ❌ Limited |
| ESM Support | ✅ Native | ❌ Limited | ✅ Native |
| Tree Shaking | ✅ ESM optimized | ❌ No | ✅ Optimized |
| Legacy Compatibility | ✅ Full | ✅ Full | ❌ Limited |
| Build Complexity | ✅ Simple | ✅ Simple | |
| Library Distribution | ✅ Perfect |
✅ Use preset-hybrid when:
- Building npm packages or open-source libraries
- Need maximum Node.js ecosystem compatibility
- Users require both CommonJS and ESM support
- Want modern bundler optimizations without breaking legacy support
- Library adoption is more important than build simplicity
❌ Consider alternatives when:
- Building applications (not libraries)
- Team can commit to single module format
- Build simplicity is more important than compatibility
- Target environment is known and consistent
General Presetter issues? See the main troubleshooting guide for common Presetter problems and solutions.
| Issue | Symptoms | Solution |
|---|---|---|
| Import extension errors | Cannot resolve module in ESM |
tsc-esm-fix automatically adds .js extensions |
| __dirname undefined | ReferenceError in ESM |
Automatically replaced with import.meta equivalents |
| Wrong module format | CJS syntax in .mjs files | Check build pipeline runs both compilations |
| Package.json exports | Module resolution failures | Ensure proper conditional exports structure |
Need help with Presetter CLI commands? Check the CLI reference in the main documentation.
General Presetter questions? Check the main FAQ for general usage, configuration, and customization questions.
preset-hybrid extends preset-essentials with sophisticated dual-module capabilities:
- Generates both CommonJS (.js) and ESM (.mjs) outputs
- Separate TypeScript configurations for each format
- Automatic import transformations and path resolution
- Perfect dual-module package.json structure
Automatically by tsc-esm-fix:
// Your TypeScript code
import { utils } from './utils';
// CommonJS output (no change needed)
const { utils } = require('./utils');
// ESM output (extensions added automatically)
import { utils } from './utils.js';Automatically converted for ESM compatibility:
// Your TypeScript code
const configPath = path.join(__dirname, 'config.json');
// CommonJS output (unchanged)
const configPath = path.join(__dirname, 'config.json');
// ESM output (automatically converted)
const configPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'config.json',
);No! Write TypeScript once, get both formats:
- Single TypeScript source in
src/ - Automatic dual compilation to
lib/index.jsandlib/index.mjs - Shared TypeScript declarations in
lib/index.d.ts
Seamlessly with conditional exports:
// CommonJS users
const myLib = require('my-hybrid-lib');
// ESM users
import myLib from 'my-hybrid-lib';
// Both work automatically based on user's environmentWe'd love your ideas and contributions! Submit issues or suggestions via GitHub Issues. See the Contribution Guide for more details.
Released under the MIT License. © 2020, Alvis Tang.