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]

関連記事