TypeScriptやJavaScriptに記述したrequireをRollupの出力に含める方法3選

## きっかけ

TypeScriptやJavaScriptに記述したrequireをRollupでバンドルした時、出力にrequireしているファイルが含まれませんでした。
調査して得られた3通りの解決方法をまとめます。

```js
// 解決したいこと: これがRollupの出力にこのまま出てきてしまった (一つのバンドルファイルにまとめてもらえなかった)
const sample = require("sample.js") 
```

## 使用したRollupとRollupプラグイン

いずれも執筆日2022-09-25の最新バージョンです。

- [rollup](https://www.npmjs.com/package/rollup) @2.79.1
- [@rollup/plugin-commonjs](https://www.npmjs.com/package/@rollup/plugin-commonjs) @22.0.2
- [@rollup/plugin-typescript](https://www.npmjs.com/package/@rollup/plugin-typescript) @8.5.0

## サンプルリポジトリ

今回扱う全てのソースコードはGitHubに格納してあります。
動作検証を目的としてメソッド実行やログ出力を含んでいますが、この記事で論点としている処理は同じ記述としてあります。
[ljourm/sample-rollup-typescript-require](https://github.com/ljourm/sample-rollup-typescript-require)

## 結論

最初に結論をまとめます。

|No.|方法|使用条件|バンドル後のrequire|サンプルコード(GitHub)|
|---|----|----|----|----|
|00|Rollupオプションなし|特になし|requireがそのまま出力される(実行するとエラー)|[00-first-issue](https://github.com/ljourm/sample-rollup-typescript-require/tree/main/projects/00-first-issue)|
|01|`plugin-commonjs`に`extensions: [".js", ".ts"]`を指定|tsconfigで`"module": "CommonJS"`を設定|バンドルされる|[01-include-ts-js](https://github.com/ljourm/sample-rollup-typescript-require/tree/main/projects/01-include-ts-js)|
|02|`plugin-commonjs`に`transformMixedEsModules: true`を指定|JavaScriptのみを使用|バンドルされる|[02-only-js](https://github.com/ljourm/sample-rollup-typescript-require/tree/main/projects//02-only-js)|
|03|02の設定 + requireをJavaScriptに記述し、それをTypeScriptからimport|JavaScriptとTypeScriptをハイブリッドで使用|バンドルされる|[03-use-loader](https://github.com/ljourm/sample-rollup-typescript-require/tree/main/projects//03-use-loader)|

## 前提として、そもそもrequireを使うべきではない

可能な場合は `require` (CommonJSの記法) を廃止し、 `import` (ES Modulesの記法) の使用に切り替えるべきです。
公式ドキュメント、GitHub Issue、StackOverflowなどを参照していくと、どこもimportの使用に誘導されていました。

## それでもrequireしなければならない時がある

ですが、先日私が出会ったケースでは以下のような制約・条件となっていました。

- CommonJSのライブラリを使用している。
- そのライブラリはベンダー製で、こちらで変更することができない。
- JavaScriptでなくTypeScriptから使用したい。

検討した上でrequireの継続使用を決定し、問題解消の調査に進みます。

## 問題となったソースコード

### 作成したソースコード

```js:calculator.js
// a + b するだけの簡単なサンプルメソッドをexportしている
module.exports.add = function (a, b) {
  return a + b;
};
```

```ts:index.ts
export const output = () => {
  // requreによってcalculator.jsを読み込む
  const { add } = require("./calculator.js");

  // 以降、addメソッドを使って何かする
};
```

### Rollupの出力結果

```js:index.js
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var output = function () {
    // requreによってcalculator.jsを読み込む
    require("./calculator.js").add; // !!!!ポイント!!!!

  // 以降、addメソッドを使って何かする
};

exports.output = output;

```

requireがaddメソッドに置き換わることを想定していましたが、そうはなりませんでした。
このコードを実行すると`require("calculator.js")`でファイルを読み込めず、エラーが発生します。

このrequireをaddメソッドに置き換えることが、この記事の目標です。

## 解決方法

### 01: `plugin-commonjs`に`extensions: [".js", ".ts"]`を指定

まずは、今回紹介する中でTypeScriptにrequireを記述できる唯一の方法です。
サンプルコード: [01-include-ts-js](https://github.com/ljourm/sample-rollup-typescript-require/tree/main/projects/01-include-ts-js)

たいていの場合、プラグイン`@rollup/plugin-commonjs`を使っていると思います。
このプラグインのオプション`extensions`を変更する方法がplugin-typescriptのドキュメントで紹介されていました。

公式ドキュメント: [@rollup/plugin-typescript #Importing CommonJS](https://github.com/rollup/plugins/tree/master/packages/typescript/#importing-commonjs)

|プラグイン|変更するオプション|デフォルト値|変更後|
|----|----|----|----|
|`@rollup/plugin-commonjs`|`extensions`(読み込み対象の拡張子)|`['.js']`|`['.js', '.ts']`|

ただし、これを実現するためには`tsconfig.json`で`"module": "CommonJS"`と設定することも必須のため、適用できないケースも多いと思います。
ドキュメントにも`it is not recommended`(＝推奨していない)とあり、バンドル時に`"module": "esnext"`の使用を推奨する警告も表示されました。


### 02: `plugin-commonjs`に`transformMixedEsModules: true`を指定

次はTypeScriptのことを考えず、JavaScriptとしてrequireを使用する方法です。
サンプルコード: [02-only-js](https://github.com/ljourm/sample-rollup-typescript-require/tree/main/projects/02-only-js)

これも先ほどと同じく、プラグイン`@rollup/plugin-commonjs`のオプションから設定します。

公式ドキュメント: [@rollup/plugin-commonjs #transformMixedEsModules](https://github.com/rollup/plugins/tree/master/packages/commonjs#transformmixedesmodules)
参考記事: [かもメモ Rollup require がバンドルされないにハマる](https://chaika.hatenablog.com/entry/2022/08/09/083000)

|プラグイン|変更するオプション|デフォルト値|変更後|
|----|----|----|----|
|`@rollup/plugin-commonjs`|`transformMixedEsModules`|`false`|`true`|

TypeScriptを使用しない場合はこれで全て解決ですが、今回の目標ではTypeScriptの使用を継続したいため、さらに別の方法の模索へと進みます。


### 03: 02の設定 + requireをJavaScriptに記述し、それをTypeScriptで読み込む

最後に実際に採用した方法です。
これによってTypeScriptとrequireによって参照するCommonJSの共存が実現できました。
サンプルコード: [03-use-loader](https://github.com/ljourm/sample-rollup-typescript-require/tree/main/projects/03-use-loader)

この方法ではJavaScriptとTypeScriptのハイブリットによって解決を図ります。
RollupのオプションはNo.2と同じですが、自分にとっては発想の転換が必要だったため、別の方法として記載します。

#### ソースコード

`transformMixedEsModules`が結果的にJavaScriptのみに適用される特性に着目し、requireとexportだけを実現するJavaScriptファイルを新たに作成しました。

```js:calculator-loader.js
// このファイルではrequireした結果をexportするのみとする
export const loadCalculator = () => {
  return require("./calculator.js");
};
```

そして、作成したJavaScriptをTypeScriptから読み込みます。

```ts:index.ts
import { loadCalculator } from "./calculator-loader";

export const output = () => {
  const { add } = loadCalculator();

  const result = add(1, 2);
};
```

これによってTypeScriptとrequireによって参照するCommonJSの共存が実現できました。
No.1のような`tsconfig.json`の変更も不要です。

## 最後に

全部がモダンで綺麗なプロジェクトばかりならよいのですが、そうでないケースもどうしてもあります。
同じようなケースでお役に立てたら幸いです。