できない.dev

babel-loader で node_modules 内のパッケージが変換できない

babel-loader の定番設定 exclude: /node_modules/ は依存パッケージを丸ごと変換対象から外す。
素の構文で配布されたパッケージだけを例外にする書き方に変えれば解決する。

公開: 更新:

症状できない
npx webpack
asset main.js 1.42 KiB [emitted] (name: main)
./src/index.js 67 bytes [built] [code generated]
./node_modules/modern-lib/index.js 69 bytes [built] [code generated]
webpack 5.112.0 compiled successfully in 395 ms
解決後できた · exit 0
npx webpack
asset main.js 1.53 KiB [emitted] (name: main)
./src/index.js 67 bytes [built] [code generated]
./node_modules/modern-lib/index.js 190 bytes [built] [code generated]
webpack 5.112.0 compiled successfully in 307 ms

2026-10-21 に node:20(Babel 7.29.7 / Node.js 20.20.2) で実際に打って取った出力。検証環境の詳細

要約

自分のコードは変換されるのに、依存パッケージ由来のファイルで構文エラーが出るときは、exclude: /node_modules/ が効いている。
この 1 行は「node_modules の中は変換しない」という指示なので、素の構文のまま配布されたパッケージはそのままバンドルに入る。

babel-loader は、この用途のために exclude を条件オブジェクトで書く形を用意している。

// webpack.config.js
{
  test: /\.(?:js|mjs|cjs)$/,
  exclude: {
    and: [/node_modules/],
    not: [/unfetch/, /d3-array|d3-scale/],
  },
  use: { loader: "babel-loader" },
}

not に挙げたパッケージだけが変換対象に戻る。

実行例

実際に上記の手順を node:20 で動かし、素の ES2020 構文のまま置いた最小の依存パッケージを通すと、exclude: /node_modules/ のままではバンドルに ?. と ?? がそのまま残った。exclude を条件オブジェクトにして対象パッケージを not に挙げると、同じ構文が var と void 0 を使った ES5 向けの書き方に変わった。
依存が .mjs で配布されている場合は、test が /\.js$/ だけだと変換されず、.mjs と .cjs を含めると変換された。

$ npx webpack
asset main.js 1.42 KiB [emitted] (name: main)
./src/index.js 67 bytes [built] [code generated]
./node_modules/modern-lib/index.js 69 bytes [built] [code generated]
webpack 5.112.0 compiled successfully in 395 ms
$ echo $?
0
$ grep -n -F -e '?.' -e '??' dist/main.js
8:  return opts?.size ?? 42;
$ npx webpack
asset main.js 1.53 KiB [emitted] (name: main)
./src/index.js 67 bytes [built] [code generated]
./node_modules/modern-lib/index.js 190 bytes [built] [code generated]
webpack 5.112.0 compiled successfully in 307 ms
$ echo $?
0
$ grep -n -F -e '?.' -e '??' dist/main.js
$ grep -n opts dist/main.js
7:module.exports = function size(opts) {
8:  var _opts$size;
9:  return (_opts$size = opts === null || opts === void 0 ? void 0 : opts.size) !== null && _opts$size !== void 0 ? _opts$size : 42;
$ npx webpack
asset main.js 2.87 KiB [emitted] (name: main)
runtime modules 614 bytes 3 modules
cacheable modules 130 bytes
  ./src/index.js 64 bytes [built] [code generated]
  ./node_modules/modern-lib/index.mjs 66 bytes [built] [code generated]
webpack 5.112.0 compiled successfully in 314 ms
$ grep -n -F -e '?.' -e '??' dist/main.js
13:  return opts?.size ?? 42;
$ grep -n test: webpack.config.js
7:        test: /\.(?:js|mjs|cjs)$/,
$ npx webpack
asset main.js 2.98 KiB [emitted] (name: main)
runtime modules 614 bytes 3 modules
cacheable modules 251 bytes
  ./src/index.js 64 bytes [built] [code generated]
  ./node_modules/modern-lib/index.mjs 187 bytes [built] [code generated]
webpack 5.112.0 compiled successfully in 423 ms
$ grep -n -F -e '?.' -e '??' dist/main.js

— 2026-10-21 時点の出力

検証環境Node.js 20.20.2 / npm 10.8.2 / Python 3.11.2 / 2026-10-21 検証(ほか 2 件)
検証日
実行環境
node:20 Debian GNU/Linux 12 (bookworm)
バージョン
  • Node.js 20.20.2
  • npm 10.8.2
  • Python 3.11.2
  • Git 2.39.5
  • Babel 7.29.7

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

解決策

1. exclude を条件オブジェクトに変える

正規表現ひとつでは「node_modules を除く。
ただしこれは除かない」を表現できない。
babel-loader の公式リポジトリが示している書き方は次の形だ。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(?:js|mjs|cjs)$/,
        exclude: {
          and: [/node_modules/],
          not: [/unfetch/, /d3-array|d3-scale/, /@hapi[\/\\]joi-date/],
        },
        use: {
          loader: "babel-loader",
          options: {
            presets: [["@babel/preset-env", { targets: "ie 11" }]],
          },
        },
      },
    ],
  },
};

and / not は webpack の条件(Condition)の記法で、babel-loader 固有の拡張ではない。

2. 対象パッケージを名指しで特定する

not に何を書くかは、ビルドエラーが指しているファイルのパスから決める。

ERROR in ./node_modules/some-lib/dist/index.js 12:24
Module parse failed: Unexpected token (12:24)
You may need an appropriate loader to handle this file type

この場合は /some-lib/ を not に足す。
パッケージ名を丸ごと書くと、そのパッケージが依存している別のパッケージまでは含まれない点に注意する。
エラーが次のパッケージに移ったら、同じ手順を繰り返す。

3. .mjs を test に含める

拡張子の指定漏れは見落としやすい原因だ。

// before
test: /\.js$/,
 
// after
test: /\.(?:js|mjs|cjs)$/,

4. targets を実態に合わせる

そもそも古いブラウザを対象にしていなければ、依存パッケージの変換は不要だ。@babel/preset-env の対象が広すぎないかを見直すと、not に足すパッケージ自体が減る。
対象の決め方は /babel/preset-env-not-transpiling/ を参照のこと。

{
  "browserslist": ["> 0.5%", "last 2 versions", "not dead"]
}

なお node_modules を丸ごと変換対象にするとビルド時間が大きく伸びる。
範囲は必要なパッケージだけに絞ること。

よくある原因

  1. exclude: /node_modules/ が一律に効いている: これはビルド時間を守るための定番設定で、ほとんどの場合は正しい。
    ただし依存パッケージが変換を必要とする場合は、そのパッケージだけを例外にする必要がある。
  2. 依存パッケージが素の構文で配布されている: 近年は「変換は利用側の責任」という方針で、オプショナルチェーンやクラスフィールドをそのまま含んだまま公開するパッケージが増えている。
    babel-loader の公式リポジトリも、レガシーな対象をサポートする場合は node_modules 配下の変換が必要になると説明している。
  3. main が変換前のソースを指している: module や exports の指す先が変換済みでも、webpack の解決結果が変換前のファイルになっていることがある。
  4. test に .mjs が入っていない: ESM で配布されたパッケージは .mjs を持つ。test: /\.js$/ だけだと loader に渡らず、変換されないまま通過する。