Ttechflare641

commonjs-to-esm

Migrates Node.js CommonJS (.js, .cjs) to ES modules

commonjsesmes-modulesrequireimportmodule-exportsmigrationnodejscjs-to-esmjavascriptuse-strictjson-importre-export__dirname__filename
Public
2 executions
Run locally
npx codemod commonjs-to-esm
Documentation

commonjs-to-esm

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

BeforeAfter
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').joinimport { 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)

Multiline destructuring is fully supported.

2. module.exports / exportsexport

BeforeAfter
module.exports = exprexport default expr
module.exports = function fn() {}export default function fn() {}
module.exports = require('./other')export { default } from './other.js'
module.exports.name = nameexport { name }
module.exports.name = otherexport { other as name }
module.exports.PI = 3.14export const PI = 3.14
exports.name = nameexport { name }
exports.name = exprexport const name = expr
exports.default = exprexport default expr (fixes invalid export const default)
module.exports.default = exprexport default expr

3. Named re-export pattern (from Node.js official guide)

BeforeAfter
exports.foo = require('./foo')export { default as foo } from './foo.js'
exports.bar = require('./bar')export { default as bar } from './bar.js'
module.exports.utils = require('./utils')export { default as utils } from './utils.js'

4. __dirname / __filenameimport.meta

BeforeAfter
__dirnameimport.meta.dirname
__filenameimport.meta.filename

Requires Node.js >= 21.2.0 (or >= 20.11.0 LTS).

5. require.resolve()import.meta.resolve()

BeforeAfter
require.resolve('./config')import.meta.resolve('./config')
require.resolve('express')import.meta.resolve('express')
require.resolve('./a', { paths: [__dirname] })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

BeforeAfter
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()

BeforeAfter
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

BeforeAfter
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:

PatternWhy 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/functionsConditional exports can't safely become static ESM
require() inside functions/loops/conditionalsNon-top-level requires are left for manual conversion
require.main === moduleNo clean ESM equivalent; left for manual refactoring
require.resolve(spec, { paths: [...] })Skipped — options differ from import.meta.resolve
module.parentNo ESM equivalent; left for manual follow-up
require.cache / require.extensionsNot available in ESM; hot-reload / loader hooks need a different design
Top-level thisIn CJS, top-level this is exports; in ESM it is undefined — not rewritten (pawelgrzybek.com)

Usage

From the registry

bash

Dry run (preview changes)

bash

Manual follow-up after running

  1. Add "type": "module" to your package.json
  2. Update package.json exports field — replace "main" with "exports"
  3. Directory imports./utils may need to become ./utils/index.js
  4. import.meta.resolve() returns URLs — some call sites may need fileURLToPath() wrapping
  5. Named imports from CJS packages may fail — use import pkg from 'cjs-pkg' then destructure
  6. Circular dependencies — ESM handles these differently from CJS
  7. Dynamic requires inside functions — left as require() calls; refactor to async patterns or use createRequire(import.meta.url)
  8. Test runner config — update Jest/Vitest configuration for ESM

Metrics

MetricDescription
require-to-importrequire() calls converted to import
exports-to-exportmodule.exports/exports converted to export
dirname-filename-replaced__dirname/__filename replaced with import.meta
dynamic-require-to-importDynamic require() converted to import()
require-resolve-to-import-metarequire.resolve() converted to import.meta.resolve()
use-strict-removed"use strict" directives removed
json-import-with-attributeJSON imports with with { type: 'json' } attribute added

License

MIT

Before

This is one example from the codemod's test cases. The codemod may handle many more cases.

Ready to contribute?

Build your own codemod and share it with the community.