convert-esm
Migrate CommonJS to ESM step by step, fixing require/module.exports, imports, and config.
SKILL.mdconvert-esm/SKILL.md
GitHubGoal
Migrate a JavaScript/TypeScript project from CommonJS (require/module.exports) to ESM (import/export) systematically without breaking the runtime.
Scope
- Covers:
require→import,module.exports→export,"type": "module"config, file extensions (.jsvs.cjsvs.mjs), dynamic requires,__dirname/__filenameequivalents, JSON imports - Migrate one package/module at a time — in a monorepo, one workspace at a time
- Action-oriented: change real code — verify with typecheck/build/test at every step
Execute
1. Assess Current State
Goal: Size the work and find blockers
- Count
require/module.exportssites withuse-astgreporsearch-files-patterns - Check the
package.json"type"field and runtime/tooling support (Node version, bundler, test runner) - Find blockers:
require()calls that can't be made static, conditional requires, CJS-only deps - Check the dependencies that import this project — run
/deep-review-codebase
2. Plan Migration Order
Goal: Order the work to minimize breakage
- Do leaf modules first (no internal dependents) → core modules last
- Identify files that need renaming (
.js→.mjsin mixed mode) or config changes - Plan
package.json:"type": "module",exportsmap,engines
3. Convert
Goal: Convert in batches with verification
- Convert
require→importandmodule.exports→exportone module at a time - Replace
__dirname/__filenamewithimport.meta.url+fileURLToPath - Convert dynamic
require()→await import(), or a static import where possible - JSON requires →
import ... with { type: 'json' }orfs.readFile - Add
.jsextensions to relative imports per the ESM spec - Run
/update-referenceswhenever export/import paths change
4. Update Config
Goal: Adapt tooling to support ESM
- Set
"type": "module"inpackage.json+ anexportsmap - tsconfig:
module: "NodeNext"/"ESNext"and the matchingmoduleResolution - Test runner, bundler, scripts — check every tool that reads the code
- Use
.cjsfor files that must stay CommonJS (config files a tool requires)
5. Verify
Goal: Confirm everything runs on ESM
/run-typecheck+/run-lintpass/run-buildpasses — bundle/output correct/run-testpasses — watch for test mocks that rely on CJS semantics (jest.mockvsvi.mock)- Smoke-test the real runtime entry points
Rules
1. Incremental
- Convert one module/package at a time — never big-bang the whole repo at once
- Verify after each batch — if something breaks, know exactly where
2. Compatibility Aware
- If the package is a published library → decide with the user upfront whether to dual-publish (ESM+CJS) or go ESM-only
- Watch for tools that are still CJS-only — use
.cjsfiles or interop instead of forcing a migration
3. No Semantic Changes
- Convert syntax/module system only — never refactor logic along the way
- Circular imports that CJS tolerates may break in ESM — flag them to
/follow-tool-madge
Expected Outcome
- Codebase fully ESM — no leftover
require/module.exports(except necessary.cjs) - typecheck/build/test all pass
package.jsonexports map correct for consumers