esbuild に CJS として import してほしいのに ESM として import されちゃうときのプラグイン

2022/04/07に公開

import 文を書くと esbuild は該当パッケージを ESM として読みに行く。

どういうことかというと、import 先のパッケージが Conditional Exports でこんな風に書いてる場合、./foo.mjs をバンドルする。

package.json
{
  "exports": {
    "import": "./foo.mjs",
    "require": "./bar.js"
  }
}

基本的にこれは望ましい挙動なんだけど、場合によってはうまくバンドルできなかったりする。

たとえば、foo.mjsがこんな感じのとき。実装の実態は bar.js にあるんだけど createRequire を使って foo.mjs が ESM の口になってるような感じ。

foo.mjs
imprt module from "node:module";

const require = module.createRequire(import.meta.url);

const bar = require("./bar.js");

export { bar };

こういうのを esbuild でバンドルすると imoport.meta.url は空オブジェクトになるし、それを解決したとしても createRequire で作った require で読み込んだモジュールはそもそもバンドルされない。

なので指定したモジュールを ESM ではなく CJS として読みに行くようにするプラグインがあると便利なのかもしれない。

require の方のモジュール解決のアルゴリズムに乗っかるので CJS の方を読みに行く。

import path from 'node:path';
import module from 'node:module';

const require = module.createRequire(import.meta.url);

/**
 * @param {Array<string>} specifiers
 * @returns {import('esbuild').Plugin}
 */
function resolveCjsPlugin(specifiers) {
  const filter = new RegExp(specifiers.join('|'));
  return {
    name: 'resolveCjs',
    setup(build) {
      build.onResolve({ filter }, (args) => {
        return {
          path: path.join(require.resolve(args.path)),
        };
      });
    },
  };
}

export default resolveCjsPlugin;

本当はこんなことしたくない...

Discussion