Migrates Node.js CommonJS (.js, .cjs) to ES modules.
Relative imports get an explicit .js suffix where appropriate for Node ESM resolution.
TypeScript is out of scope for this package. TS CommonJS uses different syntax (import x = require(...), export =, import type, etc.) and different tooling (tsconfig / emit). Plan a separate codemod or pipeline (e.g. compile to JS then run this tool, or a dedicated commonjs-to-esm-typescript package) for .ts sources.
What it does
1. require() → import
Before
After
const x = require('mod')
import x from 'mod'
var x = require('mod')
import x from 'mod'
const { a, b } = require('mod')
import { a, b } from 'mod'
const { a: alias } = require('mod')
import { a as alias } from 'mod'
require('./setup')
import './setup'
const join = require('path').join
import { join } from 'path'
const fs = require('node:fs/promises')
import fs from 'node:fs/promises'
const legacy = require('./x.cjs')
import legacy from './x.cjs' (existing extension preserved)
unchanged (second-argument paths is not equivalent to import.meta.resolve)
Only single-argument require.resolve(...) calls are rewritten. Multi-argument forms are skipped per Node.js migrating imports.
Requires Node.js >= 20.6.0. Returns a URL string; some call sites may need fileURLToPath() wrapping.
6. JSON imports with type attribute
Before
After
const pkg = require('./package.json')
import pkg from './package.json' with { type: 'json' }
const cfg = require('./config.json')
import cfg from './config.json' with { type: 'json' }
Uses the with syntax (Node.js 21+). For Node.js 17.1-20.x, manually change with to assert.
7. "use strict" removal
ESM runs in strict mode by default. The codemod removes top-level "use strict" directives automatically.
8. Dynamic require() → dynamic import()
Before
After
const mod = require(variable)
const mod = await import(variable)
Only top-level dynamic requires with non-string arguments are converted.
9. Automatic .js extension for relative imports
Before
After
require('./utils')
import './utils.js'
require('./data.json')
import './data.json' with { type: 'json' } (extension preserved)
require('express')
import express from 'express' (bare specifier, no extension added)
Relative require('./folder') becomes ./folder.js. If CommonJS actually resolved a directory to ./folder/index.js, you must fix that path manually (directory imports, esmodules.com).
Patterns intentionally left unchanged (safety)
The codemod is conservative — it will NOT transform patterns that could produce incorrect code:
Pattern
Why it's skipped
const { a, ...rest } = require('mod')
Rest patterns have no ESM import equivalent
require('x').a.b.c (deep chains)
Ambiguous — can't safely map to a single named import
require('express')() (immediately invoked)
Chained call after require is not a simple import
{ ...require('./defaults') } (spread in object)
Nested require in expression position
exports = { ... } (direct reassign)
Reassigning exports is a CJS no-op, not a real export
module.exports inside if/try/functions
Conditional exports can't safely become static ESM
require() inside functions/loops/conditionals
Non-top-level requires are left for manual conversion
require.main === module
No clean ESM equivalent; left for manual refactoring
require.resolve(spec, { paths: [...] })
Skipped — options differ from import.meta.resolve
module.parent
No ESM equivalent; left for manual follow-up
require.cache / require.extensions
Not available in ESM; hot-reload / loader hooks need a different design
Top-level this
In CJS, top-level this is exports; in ESM it is undefined — not rewritten (pawelgrzybek.com)
Usage
From the registry
bash
npx codemod run commonjs-to-esm --target ./src
Dry run (preview changes)
bash
npx codemod run commonjs-to-esm --target ./src --dry-run
Manual follow-up after running
Add "type": "module" to your package.json
Update package.jsonexports field — replace "main" with "exports"
Directory imports — ./utils may need to become ./utils/index.js
import.meta.resolve() returns URLs — some call sites may need fileURLToPath() wrapping
Named imports from CJS packages may fail — use import pkg from 'cjs-pkg' then destructure
Circular dependencies — ESM handles these differently from CJS
Dynamic requires inside functions — left as require() calls; refactor to async patterns or use createRequire(import.meta.url)
Test runner config — update Jest/Vitest configuration for ESM
Metrics
Metric
Description
require-to-import
require() calls converted to import
exports-to-export
module.exports/exports converted to export
dirname-filename-replaced
__dirname/__filename replaced with import.meta
dynamic-require-to-import
Dynamic require() converted to import()
require-resolve-to-import-meta
require.resolve() converted to import.meta.resolve()
use-strict-removed
"use strict" directives removed
json-import-with-attribute
JSON imports with with { type: 'json' } attribute added
License
MIT
Ready to evolve your codebase?
Build tailored, compiler-aware skills to assess, plan, and automate complex codebase changes.