ライブラリをパブリッシュする際に、CommonJS(cjs)とECMAScript Module(esm)を両方サポートするように設定するのが一般的です。サイズの小さい、tree-shakingしやすいesmを優先するのが今のトレンドでしょう。
以前は理解が曖昧でしたが、最近になってようやくコツをつかみましたので、その設定方法を共有したいと思います。
cjsとesmは何?
CommonJSとES Moduleとなります。書き方の違いの例は下記を参考してください。
const path = require("path");
// その他の処理
module.exports = config;
import path from "path";
// その他の処理
export default config;
具体的な比較はGoogle先生にお任せします。
何で両方サポートする?
- Node.js環境では通常cjs形式が利用されますが、一部の環境ではesmが使えるようになっています。一方、ブラウザー側はすでにサイズの小さいesmを全面サポートしています。
- React界に一番有名なフレームワークNext.jsがSSR(サーバーサイドレンダリング)を押しました。Node.jsとブラウザー両方サポートした方がバグりづらいです。
- 一部のプロジェクトやツールは引き続きcjs形式を使用している場合があります。既存のエコシステムとの互換性を維持するために、cjs形式のモジュールも提供することが役立ちます。
cjsは消えてゆくですが、今サポートした方が無難です。
package.jsonの設定方法
https://github.com/frehner/modern-guide-to-packaging-js-library#packagejson-settingsを参考すれば大体わかるでしょう。
まとめると、cjsとesm関連の設定は下記の通りです。
{
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"module": "./dist/esm/index.js",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"default": "./dist/esm/index.js"
},
"./package.json": "./package.json"
},
}
動くように設定されると見えます。
- main, module両方とも設定している
- exportsにもimportとrequire両方とも設定している
- exportsの順番として、module(esm)が優先されている
が、実際に動かしてみると、意外にこける場合があります。
拡張子は重要
上記の設定であれば、Node.jsのプログラム、Webpackでサーバーサイドをバンドルするフレームワークで作ったプログラムがimportして実行する際には、cjsとesmのどちらとして処理されますか。
答えはmoduleの./dist/esm/index.jsファイルが使われるが、esmではなく、cjsとして処理
されます。exportsの中に優先順位をつけても、拡張子が*.js
のためなのです。
問題を解決するために、一番簡単な方法はesmのファイルの拡張子を変更することです。Node.jsは拡張子が*.mjs
のファイルをesmとして処理するためです。
{
"main": "dist/cjs/index.js",
"module": "dist/esm/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"module": "./dist/esm/index.mjs",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.js",
"default": "./dist/esm/index.mjs"
},
"./package.json": "./package.json"
},
}
type: module
より脱cjsの方であれば、拡張子の設定以外、"type": "module"
の設定の追加も解決策となります。
{
"type": "module",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"module": "./dist/esm/index.js",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"default": "./dist/esm/index.js"
},
"./package.json": "./package.json"
},
}
そうすると、esmが一番優先されます。でも、まだesmをサポートしていないライブラリが含まれているのであれば、runtimeエラーになる恐れ
があることに十分注意する必要があります。
そして、"type": "module"
で宣言されたライブラリをNode.jsのプログラムがcjsパッケージとして使えなくなることにも気をつけましょう。
const { userSchema } = require('zod-schema');
const { zodToJsonSchema } = require('zod-to-json-schema');
function generate() {
const userJsonSchema = zodToJsonSchema(userSchema);
fs.mkdirSync(outputFolder, { recursive: true });
fs.writeFileSync(
path.resolve(outputFolder, 'user.json'),
JSON.stringify(userJsonSchema, null, 2),
);
console.log('Successfully generated JSON Schema');
}
generate();
エラーメッセージ通りに修正すれば、エラーが消えます。
{
"type": "module",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"module": "./dist/esm/index.js",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.cjs",
"default": "./dist/esm/index.js"
},
"./package.json": "./package.json"
},
}
もう1つのパターン
では、"type": "module"
のパターンを踏まえて、cjsの部分に拡張子をつけて、"type": "module"
を消したらどうでしょうか。
{
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"module": "./dist/esm/index.js",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.cjs",
"default": "./dist/esm/index.js"
},
"./package.json": "./package.json"
},
}
cjsのライブラリとして使うのに問題ないが、今回esmライブラリとして使うとエラーになります。
import { userSchema } from 'zod-schema';
import { zodToJsonSchema } from 'zod-to-json-schema';
function generate() {
const userJsonSchema = zodToJsonSchema(userSchema);
fs.mkdirSync(outputFolder, { recursive: true });
fs.writeFileSync(
path.resolve(outputFolder, 'user.json'),
JSON.stringify(userJsonSchema, null, 2),
);
console.log('Successfully generated JSON Schema');
}
generate();
究極案
"type": "module"
の有無、cjs, mjs拡張子の有無で簡単にこける罠がありすぎます。個人的に"type": "module"
の有無に関わらず動く案は究極案として採用しています。
方法は拡張子でcjsとesmを区別することです。
{
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"module": "./dist/esm/index.mjs",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs",
"default": "./dist/esm/index.mjs"
},
"./package.json": "./package.json"
},
}
完了
試してみたい方はこのレポジトリをcloneして試してみてください。