require-cjs-transformer Plugin
Transform ESM imports of CJS packages to require calls for better performance
require-cjs-transformer Plugin Example
This example demonstrates how to use the require-cjs-transformer plugin to automatically transform ESM imports of CommonJS-only packages into require() calls for better Node.js performance.
Overview
The require-cjs-transformer plugin solves a specific performance issue: when you import CommonJS-only packages using ESM syntax, Node.js must use the expensive cjs-module-lexer to analyze the CJS module. This plugin converts ESM imports to require() calls, allowing Node.js to skip this analysis step.
Basic Example
Project Structure
require-cjs-example/
├── src/
│ ├── index.ts
│ ├── parser.ts
│ └── utils.ts
├── package.json
├── packem.config.ts
└── tsconfig.jsonSource Files
src/index.ts
import { parseCode } from './parser'
import { processUtils } from './utils'
export function main() {
const result = parseCode('const x = 1')
const processed = processUtils(result)
return processed
}src/parser.ts
// These packages are CJS-only and benefit from transformation
import { parse } from '@babel/parser'
import { transpile } from 'typescript'
export function parseCode(code: string) {
// Parse with Babel
const ast = parse(code, {
sourceType: 'module',
plugins: ['typescript']
})
// Transpile with TypeScript
const js = transpile(code, {
target: 'ES2020',
module: 'ESNext'
})
return { ast, js }
}src/utils.ts
// Node.js built-ins also benefit from transformation
import { readFileSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
export function processUtils(data: any) {
// Read a config file
const configPath = join(__dirname, 'config.json')
const config = JSON.parse(readFileSync(configPath, 'utf8'))
return {
...data,
config
}
}Configuration
packem.config.ts
import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
export default defineConfig({
transformer,
sourcemap: true,
declaration: true,
emitESM: true,
rollup: {
requireCJS: {
builtinNodeModules: true
}
}
})package.json
{
"name": "@myorg/require-cjs-example",
"version": "1.0.0",
"type": "module",
"files": ["dist"],
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "packem build",
"dev": "packem build --watch"
},
"dependencies": {
"@babel/parser": "^7.23.0",
"typescript": "^5.3.0"
},
"devDependencies": {
"@visulima/packem": "^2",
"@types/node": "^20.0.0"
}
}Output
After building, the ESM output (dist/index.js) will be transformed:
// dist/index.js (transformed)
import { createRequire as __cjs_createRequire } from "node:module";
const __cjs_require = __cjs_createRequire(import.meta.url);
// Runtime capability helpers
const __cjs_getBuiltinModule = (module) => {
// Check if we're in Node.js and version supports getBuiltinModule
if (typeof process !== "undefined" && process.versions?.node) {
const [major, minor] = process.versions.node.split(".").map(Number);
if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
return process.getBuiltinModule(module);
}
}
// Fallback to createRequire
return __cjs_require(module);
};
const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
const { parse } = __cjs_require("@babel/parser")
const { transpile } = __cjs_require("typescript")
const { readFileSync } = __cjs_getBuiltinModule("fs");
const { join, dirname } = __cjs_getBuiltinModule("path");
const { fileURLToPath } = __cjs_getBuiltinModule("url");
const { cwd } = __cjs_getProcess;
// ... rest of the transformed codeAdvanced Configuration
Custom Package Detection
export default defineConfig({
transformer,
emitESM: true,
rollup: {
requireCJS: {
builtinNodeModules: true,
additionalPackages: ['my-cjs-package'],
exclude: ['some-package']
}
}
})Selective Transformation
export default defineConfig({
transformer,
emitESM: true,
rollup: {
requireCJS: {
// Only transform specific packages
builtinNodeModules: false,
additionalPackages: ['typescript', '@babel/parser']
}
}
})Performance Comparison
Without the Plugin
// src/parser.ts - Original ESM imports
import { parse } from '@babel/parser'
import { transpile } from 'typescript'
export function parseCode(code: string) {
return { ast: parse(code), js: transpile(code) }
}Startup time: ~50ms (includes cjs-module-lexer analysis)
With the Plugin
// dist/parser.js - Transformed to require calls
const { parse } = require('@babel/parser')
const { transpile } = require('typescript')
export function parseCode(code) {
return { ast: parse(code), js: transpile(code) }
}Startup time: ~30ms (skips cjs-module-lexer analysis)
Use Cases
CLI Tools
CLI tools that use CJS-only packages benefit significantly:
// src/cli.ts
import { Command } from 'commander'
import { readFileSync } from 'fs'
import { parse } from '@babel/parser'
import { transpile } from 'typescript'
const program = new Command()
program
.command('analyze <file>')
.action((file) => {
const code = readFileSync(file, 'utf8')
const ast = parse(code)
const js = transpile(code)
console.log('Analysis complete')
})
program.parse()Build Tools
Build tools and bundlers that process many files:
// src/bundler.ts
import { parse } from '@babel/parser'
import { transpile } from 'typescript'
import { transform } from 'swc'
import { build } from 'esbuild'
export class Bundler {
async processFile(file: string) {
const code = await fs.readFile(file, 'utf8')
// Multiple CJS-only transformers
const ast = parse(code)
const ts = transpile(code)
const swc = transform(code)
const esbuild = await build({ ... })
return { ast, ts, swc, esbuild }
}
}Development Servers
Development servers that restart frequently:
// src/dev-server.ts
import express from 'express'
import { transpile } from 'typescript'
import { parse } from '@babel/parser'
const app = express()
app.post('/transform', (req, res) => {
const { code } = req.body
const ast = parse(code)
const js = transpile(code)
res.json({ ast, js })
})
app.listen(3000)Migration Examples
From Manual require() Calls
// Before - manual require calls
const { parse } = require('@babel/parser')
export function transform(code) {
return parse(code)
}
// After - ESM imports (plugin transforms automatically)
import { parse } from '@babel/parser'
export function transform(code) {
return parse(code)
}From Dynamic Imports
// Before - dynamic imports
export async function transform(code) {
const { parse } = await import('@babel/parser')
return parse(code)
}
// After - static imports (better performance)
import { parse } from '@babel/parser'
export function transform(code) {
return parse(code)
}Testing the Transformation
Create a test to verify the transformation works:
test/transformation.test.js
import { readFileSync } from 'fs'
import { expect, test } from 'vitest'
test('transforms CJS imports correctly', () => {
const esmOutput = readFileSync('dist/index.js', 'utf8')
// Should contain require calls for CJS packages
expect(esmOutput).toContain('require("@babel/parser")')
expect(esmOutput).toContain('require("typescript")')
// Should contain runtime capability helpers
expect(esmOutput).toContain('const __cjs_getBuiltinModule = (module) => {')
expect(esmOutput).toContain('const __cjs_getProcess = (() => {')
expect(esmOutput).toContain('__cjs_getBuiltinModule("fs")')
expect(esmOutput).toContain('__cjs_getProcess')
// Should NOT contain original ESM imports
expect(esmOutput).not.toContain('import { parse } from "@babel/parser"')
})Best Practices
When to Use
- ✅ CLI tools with many CJS dependencies
- ✅ Build tools and bundlers
- ✅ Applications with measurable startup time issues
- ✅ Development servers that restart frequently
When NOT to Use
- ❌ Simple applications with few CJS imports
- ❌ Libraries that will be consumed by others
- ❌ When performance gain is negligible
- ❌ If you prefer simpler build configuration
Performance Monitoring
Monitor the impact of the plugin:
// Measure startup time
const start = Date.now()
import('./dist/index.js')
const end = Date.now()
console.log(`Startup time: ${end - start}ms`)Alternative Approaches
- Use ESM-compatible packages when available
- Bundle with esbuild/swc for better optimization
- Use dynamic imports strategically
- Consider dual package publishing for your own libraries
Troubleshooting
Plugin Not Transforming
- Check
emitESM: Must betruefor the plugin to activate - Verify package detection: Only CJS-only packages are transformed
- Check configuration: Ensure
requireCJSoptions are set correctly
Build Errors
- CommonJS compatibility: Ensure target environment supports
require() - Module resolution: Verify all packages are installed
- TypeScript issues: Update import statements if needed
Performance Issues
- Too many transformations: Use
excludeto skip unnecessary packages - Bundle size increase: Consider tree-shaking implications
- Debug overhead: Disable in production if issues arise
Related Examples
- Basic TypeScript Library - Simple library setup
- CLI Tool - Command-line application
- Dual Package - ESM and CJS compatibility