Webpackとinversify-express-utilsの組み合わせでの名前圧縮問題の解決

Webpack とinversify-express-utilsを組み合わせて Web API を実装する際に遭遇した、 クラス名や関数名の圧縮に関する問題とその解決方法について説明します。

npx webpackコマンドでのビルド後、node dist/index.jsを実行した時に特定のエラーが発生しました。 このエラーの内容や発生原因、そして解決策について詳しく見ていきます。

Error: Two controllers cannot have the same name: s
    at /Users/sugimoto/Desktop/GitHub/api-server_V2/dist/index.js:2:884127
    at Array.forEach (<anonymous>)
    at e.registerControllers (/Users/sugimoto/Desktop/GitHub/api-server_V2/dist/index.js:2:884042)
    at e.build (/Users/sugimoto/Desktop/GitHub/api-server_V2/dist/index.js:2:883828)
    at t.createApp (/Users/sugimoto/Desktop/GitHub/api-server_V2/dist/index.js:2:2048727)
    at 63607 (/Users/sugimoto/Desktop/GitHub/api-server_V2/dist/index.js:2:2104226)
    at n (/Users/sugimoto/Desktop/GitHub/api-server_V2/dist/index.js:2:4180918)
    at /Users/sugimoto/Desktop/GitHub/api-server_V2/dist/index.js:2:4181422
    at Object.<anonymous> (/Users/sugimoto/Desktop/GitHub/api-server_V2/dist/index.js:2:4181449)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)

解決方法として、webpack.config.js内で TerserPlugin を使用し、 Class 名と Function 名の圧縮・最小化を防ぐ設定を行うことです。

// targetがes6以降の場合
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  entry: "./src/server.ts",
  mode: "production",
  target: "node",
  output: {
    filename: "index.js",
    path: path.resolve(__dirname, "dist"),
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        // ここ重要
        terserOptions: {
          keep_classnames: true, // 必須
        },
        parallel: true,
      }),
    ],
  },
};
// targetがes5以前の場合
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  entry: "./src/server.ts",
  mode: "production",
  target: "node",
  output: {
    filename: "index.js",
    path: path.resolve(__dirname, "dist"),
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        // ここ重要
        terserOptions: {
          keep_fnames: true, // 必須
        },
        parallel: true,
      }),
    ],
  },
};

この問題は、inversify-express-utilsの特定の箇所でクラスや関数の名前が圧縮された結果、 同じ名前を持つコントローラーが複数存在すると判断されてしまうことが原因でした。 具体的には以下のようなメタデータが生成され、エラーが発生します。

// targetがes6以降の場合
Metadata: [
  {
    middleware: [ [Function (anonymous)] ],
    path: '/api',
    target: [class s]
  },
  {
    middleware: [ [Function (anonymous)] ],
    path: '/api',
    target: [class s]
  }
]
// targetがes5以前の場合
Metadata: [
  {
    middleware: [ [Function (anonymous)] ],
    path: '/api',
    target: [function s]
  },
  {
    middleware: [ [Function (anonymous)] ],
    path: '/api',
    target: [function s]
  }
]

補足: ES5 と ES6 のクラス名表示の違い

ES5 までの JavaScript 環境では、クラスはコンストラクタ関数として扱われ、ログに関数として出力されるのが一般的でした。 しかし、ES6 からは新しいクラス構文が導入され、ログの出力形式も変わりました。 この違いを理解することで、上記の問題がどのように発生するのかの背景をより深く理解できます。 (TypeScript でのクラス定義が ES6 と ES5 でどのようにコンパイルされるか少し詳しく書きました。)

// targetがes6以降の場合
class MyNewClass {}
console.log(MyNewClass); // 出力: [class MyNewClass]
// targetがes5以前の場合
function MyClass() {}
console.log(MyClass); // 出力: [Function: MyClass]

関連記事