Appearance
CJS Utilities in ESM
Convert ESM module metadata (import.meta) to CommonJS equivalents (require, __filename, __dirname). The native ESM alternatives are verbose and repetitive — fileURLToPath(import.meta.url) and createRequire(import.meta.url) in every file that needs them. A single cjsify(import.meta) call returns all three.
When to use
Use this utility in any ESM module that needs require, __filename, or __dirname. CJS got these right — they are concise and obvious. ESM replaced them with verbose incantations like fileURLToPath(import.meta.url) that nobody wants to write in every file. This wrapper restores the ergonomics.
The pattern
Loading CJS-only packages
Call cjsify(import.meta) and destructure the require function to load packages that only support CommonJS:
js
import { cjsify } from "./module.js";
const { require } = cjsify(import.meta);
// tree-kill and pidtree are CJS-only
const kill = require("tree-kill");
const pidtree = require("pidtree");Getting the current directory
Destructure __dirname from cjsify(import.meta) to resolve paths relative to the current module, just as you would in CommonJS:
js
import { cjsify } from "./module.js";
const { __dirname } = cjsify(import.meta);
const configPath = join(__dirname, "config.json");Combining require and paths
Destructure all three values when a module needs both CJS package loading and path resolution:
js
import { cjsify } from "./module.js";
const { require, __dirname, __filename } = cjsify(import.meta);
console.log(`Running: ${__filename}`);
console.log(`From: ${__dirname}`);
const pkg = require("./package.json");Implementing cjsify
The cjsify function takes import.meta and returns the three CommonJS globals that ESM removes:
js
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
export function cjsify(importMeta) {
const require = createRequire(importMeta.url);
const __filename = fileURLToPath(importMeta.url);
const __dirname = dirname(__filename);
return { require, __filename, __dirname };
}The returned object provides:
require— a workingrequire()function scoped to the calling module's location, created viacreateRequire()__filename— the absolute file path of the calling module, converted fromimport.meta.url__dirname— the directory containing the calling module
How it works
ESM uses URL-based module resolution (import.meta.url returns a file:// URL), while CJS uses file-path-based resolution. The three conversions bridge this gap:
createRequire(importMeta.url)— builds arequire()function that resolves paths relative to the URLfileURLToPath(importMeta.url)— convertsfile:///path/to/file.jsto/path/to/file.jsdirname(__filename)— extracts the directory from the file path
Trade-offs
Benefits:
- One function call gives you all three CJS globals
require()is scoped to the caller's directory, matching CJS behavior exactly- No runtime overhead beyond the initial conversion
- Works on any platform (the URL-to-path conversion handles Windows drive letters)
Costs:
require()is synchronous — it blocks the event loop while loading modules. Use dynamicimport()when the package supports ESM.- Calling
cjsify(import.meta)at the top of every file is verbose. Consider re-exporting from a shared module (e.g.,shared/nodejs/module.js) and importing the destructured result. - Some bundlers may not optimize
require()calls the same way they optimizeimportstatements.