eslint-cjs-to-esm: CJSをESMへとマイグレーションするツールを書いた
最近、色々なライブラリをCommonJS(CJS)からECMAScript Module(ESM)へとマイグレーションしています。
その際に、ESMでは__dirname
やrequire
などCommonJS特有の機能は使えなくなっています。
また、TypeScriptやBabelなど多くのツールはCJSではimport
時に拡張子はなくても大丈夫ですが、ESMの場合はimport
時に拡張子が必要になります。
import url from "node:url";
- import { mdEscape } from "./mdEscape";
+ import { mdEscape } from "./mdEscape.js"; // ESMでは相対パスに拡張子は省略できない
+ const __filename = url.fileURLToPath(import.meta.url); // __filenameはESMにはないためimport.meta.urlから取得する
+ const __dirname = path.dirname(__filename); // __dirnameはESMにはない
console.log(__filename, __dirname)
具体的にAPIや構文などでCJSからESMへと移行する際に気を付けるNode.jsのAPIは次にまとめてあります。
ほとんど対応する構文や機能があるので機械的に置き換えたり、チェックできるものばかりです。 そのため、CJSとESMへと機械的に移行するのを補助するツールを書きました
eslint-cjs-to-esm
eslint-cjs-to-esmは、名前の通りESLintを使ってCJSをESMへと移行するためのツールです。 ESLint自体を同梱しているため、別途ESLintの設定をせずにルール入りのESLintとして利用できます。
含まれているESLintのルールとしては次のgistのものをベースにしています。
具体的には次のようなESLintのルールを同梱しています。
eslint-plugin-file-extension-in-import-ts
eslint-plugin-node
ESLint Plugin | Rule | Source | Description | Fixable |
---|---|---|---|---|
node | no-extraneous-import | disallow import declarations which import extraneous modules | - | |
node | no-sync | disallow synchronous methods | - | |
node | file-extension-in-import | enforce the style of file extensions in import declarations |
Yes |
eslint-plugin-import
ESLint Plugin | Rule | Source | Description | Fixable |
---|---|---|---|---|
import | extensions | Ensure consistent use of file extension within the import path. | - | |
import | no-unresolved | Ensure imports point to a file/module that can be resolved. | - | |
import | no-useless-path-segments | Prevent unnecessary path segments in import and require statements. | Yes | |
import | no-extraneous-dependencies | Forbid the use of extraneous packages. | - | |
import | no-commonjs | Report CommonJS require calls and module.exports or exports.* . |
- |
eslint-plugin-unicorn
ESLint Plugin | Rule | Source | Description | Fixable |
---|---|---|---|---|
unicorn | prefer-module | Prefer JavaScript modules (ESM) over CommonJS. | Yes | |
unicorn | prefer-node-protocol | Prefer using the node: protocol when importing Node.js builtin modules. |
Yes | |
unicorn | prefer-top-level-await | Prefer top-level await over top-level promises and async function calls. | Suggest |
使い方
使い方は単純で、次のようにESLintと同じ引数を渡して対象のファイルをLintと修正できます。
npx eslint-cjs-to-esm [ESLint Arguments!]
ESLintのオプションは次のページを参照してください。
具体的に./src/*
以下のJSとTSがESMとして問題ないかは次のようにチェックできます。
npx eslint-cjs-to-esm "./src/**/*.{js,ts}"
自動的に修正できるものは--fix
オプションを渡すと修正できます。
npx eslint-cjs-to-esm "./src/**/*.{js,ts}" --fix
Note: ちょっとした制限があって、ファイルパスは.
から始める必要があります。
NG:
npx eslint-cjs-to-esm "src/**/*.ts"
OK:npx eslint-cjs-to-esm "./src/**/*.ts"
具体的にこのツールのコマンド叩くだけでコードの9割ぐらいはESMへと移行できました。
面倒な.js
を追加する作業もほとんどやってくれます。
さらにCJSとESMを一つのパッケージ内でサポートするDual packagesにするtsconfig-to-dual-packageと次のようなスクリプトと使うとほぼ機械的にできます。
Dual Packageについてはtsconfig-to-dual-packageを紹介する記事を別途書きます。
まとめ
ESLintとルールをラップしてCJSからESMへと移行するeslint-cjs-to-esmというツールを作りました。
実装も eslint
コマンドをラップして叩いているだけではあるので単純です(ちょっと設定の渡し方やFORCE_COLORなどの色付けは工夫が必要)。
CJSからESMへとマイグレーションするツールは色々あるのですが、codemod的なツールではなくTranspilerだったり、なんかうまく動かなかったりして、求めてたものが見つかりませんでした。
- Jack-Works/ts-esm-migrate: Migrate code to NodeNext module resolution by add .js extension
- beenotung/fix-esm-import-path: Auto fix import path for esm compatibility
- wessberg/cjstoesm: A tool that can transform CommonJS to ESM
その中で、ESLint rules for migrating projects from CommonJS to ESMを見て、ESLintとルールを使えば大体は実現できそうと思ってESLintのラッパーを実装しました。
ESLintをラップしたのは、自分のライブラリプロジェクトではESLintを使ってないことが多く、また単なるマイグレーションツールが欲しかったからです。
一度移行すれば、TypeScriptのmodule: ESNext
の設定だとCJSのコードを使うとコンパイルエラーになるため、継続的にLintする必要性が薄いです。
そのため、codemod的なマイグレーションツールをイメージしてeslint-cjs-to-esmを作りました。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。