require-cjs-transformer Plugin

Transform ESM imports of CJS-only packages to require calls for better performance

require-cjs-transformer Plugin

The require-cjs-transformer plugin automatically transforms ESM imports of CommonJS-only packages into require() calls. This optimization improves Node.js performance by skipping the expensive cjs-module-lexer overhead for packages that only provide CJS builds.

Why Use This Plugin?

The Problem

Many popular packages in the JavaScript ecosystem still only provide CommonJS (CJS) builds:

  • typescript
  • @babel/parser
  • eslint
  • cjs-module-lexer
  • esbuild
  • And many others...

When you import these packages using ESM syntax:

import { parse } from '@babel/parser'
import { transpile } from 'typescript'

Node.js must use the cjs-module-lexer to analyze the CJS module and create ESM compatibility. This analysis is expensive and happens on every import, impacting startup performance.

The Solution

The require-cjs-transformer plugin automatically converts ESM imports of CJS-only packages to require() calls:

// Input (ESM import)
import { parse } from '@babel/parser'
import { transpile } from 'typescript'

// Output (transformed to require)
const { parse } = require('@babel/parser')
const { transpile } = require('typescript')

This allows Node.js to skip the cjs-module-lexer analysis entirely, providing better performance.

Performance Impact

Important: Only use this plugin if the performance benefit is significant for your use case. The plugin adds complexity and maintenance overhead.

For applications that heavily use CJS-only packages, this plugin can provide measurable performance improvements. However, for most applications, the performance difference is negligible.

Configuration

Basic Setup

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'

export default defineConfig({
  transformer,
  rollup: {
    requireCJS: {
      builtinNodeModules: true
    }
  }
})

Options

interface RequireCJSPluginOptions {
  /**
   * Transform Node.js built-in modules (fs, path, etc.)
   * @default false
   */
  builtinNodeModules?: boolean

  /**
   * Custom CWD for resolving modules
   * @default process.cwd()
   */
  cwd?: string

  /**
   * Additional packages to consider as CJS-only
   * @default []
   */
  additionalPackages?: string[]

  /**
   * Packages to exclude from transformation
   * @default []
   */
  exclude?: string[]
}

Advanced Configuration

export default defineConfig({
  transformer,
  rollup: {
    requireCJS: {
      builtinNodeModules: true,
      additionalPackages: ['my-cjs-package'],
      exclude: ['some-package']
    }
  }
})

How It Works

Automatic Detection

The plugin automatically detects which packages are CJS-only by:

  1. Checking if the package has type: "commonjs" in its package.json
  2. Analyzing the package's export conditions
  3. Verifying if only CJS entry points are available

Node.js Built-ins

When builtinNodeModules: true is set, the plugin generates runtime checks to automatically choose the most efficient approach for the executing environment:

// Input
import { readFileSync } from 'fs'
import { join } from 'path'
import { cwd } from 'process'

// Output (runtime-detected)
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);
        // Node.js 20.16.0+ and 22.3.0+
        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;

// Use helpers for clean module resolution
const { readFileSync } = __cjs_getBuiltinModule("fs");
const { join } = __cjs_getBuiltinModule("path");
const { cwd } = __cjs_getProcess;

The generated code automatically detects the runtime environment and Node.js version at execution time, providing optimal performance in any supported environment.

Selective Transformation

The plugin only transforms imports that are determined to be CJS-only. ESM packages are left untouched:

// ESM package - NOT transformed
import { someFunction } from 'esm-package'

// CJS package - transformed to require
const { someFunction } = require('cjs-package')

Usage Examples

Basic Usage

// src/index.ts
import { transpile } from 'typescript'
import { parse } from '@babel/parser'
import { readFileSync } from 'fs'

export function processCode(code: string) {
  const ast = parse(code)
  const js = transpile(code)
  const file = readFileSync('file.txt')
  return { ast, js, file }
}

After transformation:

// dist/index.mjs
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 { transpile } = __cjs_require("typescript")
const { parse } = __cjs_require("@babel/parser")
const { readFileSync } = __cjs_getBuiltinModule("fs")

export function processCode(code) {
  const ast = parse(code)
  const js = transpile(code)
  const file = readFileSync('file.txt')
  return { ast, js, file }
}

React Component Library

// src/Button.tsx
import React from 'react'
import { transpile } from 'typescript'

export function Button({ children }) {
  // Use typescript for runtime transpilation
  const code = transpile('const x = 1')
  return <button>{children}</button>
}

CLI Tool

// src/cli.ts
import { Command } from 'commander'
import { readFileSync } from 'fs'
import { parse } from '@babel/parser'

const program = new Command()
program
  .argument('<file>')
  .action((file) => {
    const content = readFileSync(file, 'utf8')
    const ast = parse(content)
    console.log(ast)
  })

program.parse()

Ecosystem Appeal

We encourage the JavaScript ecosystem to continue its transition toward ESM. If you maintain a package that is still CJS-only, please consider offering an ESM build or migrating fully to ESM.

The need for plugins like require-cjs-transformer highlights the importance of the ecosystem's transition to ESM. By providing ESM builds, packages can:

  • Eliminate the cjs-module-lexer overhead
  • Provide better tree-shaking capabilities
  • Offer improved performance for all consumers
  • Reduce complexity in build tools

When NOT to Use

Don't use this plugin if:

  • Your application doesn't heavily use CJS-only packages
  • Performance is not a critical concern
  • You prefer to keep your build configuration simple
  • You're building for environments where require() is not available

Alternative Approaches

  1. Use ESM-compatible packages: Choose packages that provide ESM builds
  2. Runtime requires: Use dynamic require() calls where needed

Troubleshooting

Plugin Not Transforming Imports

  1. Verify package detection: The plugin only transforms detected CJS packages
  2. Check exclude list: Make sure the package isn't in the exclude array

Build Errors

  1. CommonJS compatibility: Ensure your environment supports require()
  2. Module resolution: Verify package paths are correct
  3. TypeScript types: Update type imports if needed

Performance Issues

If you experience performance issues:

  1. Disable the plugin: Remove the requireCJS configuration
  2. Selective transformation: Use exclude to skip problematic packages
  3. Alternative bundling: Consider using a different bundling strategy

Migration Guide

From Manual require() Calls

// Before
const { parse } = require('@babel/parser')
const { transpile } = require('typescript')

// After - plugin handles this automatically
import { parse } from '@babel/parser'
import { transpile } from 'typescript'

From Dynamic Imports

// Before
const { parse } = await import('@babel/parser')

// After - plugin provides static transformation
import { parse } from '@babel/parser'

See Also

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues