2023年12月4日 • ☕️ 6 min read

ライブラリをパブリッシュする際に、CommonJS(cjs)とECMAScript Module(esm)を両方サポートするように設定するのが一般的です。サイズの小さい、tree-shakingしやすいesmを優先するのが今のトレンドでしょう。

以前は理解が曖昧でしたが、最近になってようやくコツをつかみましたので、その設定方法を共有したいと思います。

cjsとesmは何?

CommonJSとES Moduleとなります。書き方の違いの例は下記を参考してください。

CommonJS
Copy
const path = require("path");

// その他の処理

module.exports = config;
ESModule
Copy
import path from "path";

// その他の処理

export default config;

具体的な比較はGoogle先生にお任せします。

何で両方サポートする?

  1. Node.js環境では通常cjs形式が利用されますが、一部の環境ではesmが使えるようになっています。一方、ブラウザー側はすでにサイズの小さいesmを全面サポートしています。
  2. React界に一番有名なフレームワークNext.jsがSSR(サーバーサイドレンダリング)を押しました。Node.jsとブラウザー両方サポートした方がバグりづらいです。
  3. 一部のプロジェクトやツールは引き続きcjs形式を使用している場合があります。既存のエコシステムとの互換性を維持するために、cjs形式のモジュールも提供することが役立ちます。

cjsは消えてゆくですが、今サポートした方が無難です。

package.jsonの設定方法

https://github.com/frehner/modern-guide-to-packaging-js-library#packagejson-settingsを参考すれば大体わかるでしょう。

まとめると、cjsとesm関連の設定は下記の通りです。

package.json
Copy
{
  "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として処理するためです。

package.json(拡張子改良版)
Copy
{
  "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"の設定の追加も解決策となります。

package.json(type:module版--不正)
Copy
{
  "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パッケージとして使えなくなることにも気をつけましょう。

Node.js-error(type:module版)
Copy
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();

cjs-error.png

エラーメッセージ通りに修正すれば、エラーが消えます。

package.json(type:module版--修正)
Copy
{
  "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"を消したらどうでしょうか。

package.json(type:module消すcjs--不正)
Copy
{
  "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ライブラリとして使うとエラーになります。

Copy
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();

./esm-error.png

究極案

"type": "module"の有無、cjs, mjs拡張子の有無で簡単にこける罠がありすぎます。個人的に"type": "module"の有無に関わらず動く案は究極案として採用しています。

方法は拡張子でcjsとesmを区別することです。

package.json(究極案)
Copy
{
  "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して試してみてください。

https://github.com/thundermiracle/monorepo-packagejson


ThunderMiracle

Blog part of ThunderMiracle.com