Webpackで起こるトラブル
WebpackはJavascriptやTypescriptのコード、CSSやHTMLなどをまとめてくれるツールだが、ネイティブモジュールはまとめることができない。これがサーバ側のプログラムを作る上でトラブルの元になる。
例えば、データベースを使うためにSequelizeとSqlite3をインストールすると、Webpackでビルドエラーが発生する。sqlite3が使用しているモジュールが見つからないというもので、使わない AWS-SDKが無いということらしい。
ERROR in ./node_modules/sqlite3/node_modules/node-pre-gyp/lib/info.js
Module not found: Error: Can't resolve 'aws-sdk' in '\server\node_modules\sqlite3\node_modules\node-pre-gyp\lib'
@ ./node_modules/sqlite3/node_modules/node-pre-gyp/lib/info.js 14:14-32
@ ./node_modules/sqlite3/node_modules/node-pre-gyp/lib sync ^\.\/.*$
@ ./node_modules/sqlite3/node_modules/node-pre-gyp/lib/node-pre-gyp.js
@ ./node_modules/sqlite3/lib/sqlite3.js
@ ./node_modules/sequelize/lib/dialects/sqlite/connection-manager.js
@ ./node_modules/sequelize/lib/dialects/sqlite/index.js
@ ./node_modules/sequelize/lib/sequelize.js
@ ./node_modules/sequelize/index.js
@ ./app.ts
@ ./bin/www.ts
Externalsを使ってみる
ネイティブモジュールをバンドル対象から除外することで、ビルド時の問題を回避することができる。Webpackのコンフィギュレーションファイルで以下のように externals を追加することで、対象が外部から与えられる = バンドルしなくてよいと指定できる。
以下は、Sequelizeが必要とするネイティブモジュール(と思われるもの)で、使わないものを手動で除外すると前記のビルドエラーはなくなる。(ここはトライアルアンドエラーになりそうで、自動化はできそうにない)
module.exports = {
// 中略
externals: [ 'sqlite3', 'pg-hstore', 'tedious', 'aws-sdk' ],
};
ただし、ビルドエラーがなくなっただけで、起動はできない。
Error: package.json does not exist at <prj>\dist\package.json
at Object../node_modules/sqlite3/node_modules/node-pre-gyp/lib/pre-binding.js.exports.find (<prj>\dist\server\main.js:165289:15)
at Object../node_modules/sqlite3/lib/sqlite3.js (<prj>\dist\server\main.js:151746:27)
at __webpack_require__ (<prj>\dist\server\main.js:23:30)
at new ConnectionManager (<prj>\dist\server\main.js:138450:20)
問題個所を見てみると、以下の箇所でエラーが起きていた。
class ConnectionManager extends AbstractConnectionManager {
constructor(dialect, sequelize) {
super(dialect, sequelize);
// 中略
try {
// ここで
if (sequelize.config.dialectModulePath) {
this.lib = __webpack_require__("./node_modules/sequelize/lib/dialects/sqlite sync recursive")(sequelize.config.dialectModulePath).verbose();
} else {
// ↓エラーメッセージ「at __webpack_require__ (<prj>\dist\server\main.js:23:30)」よりこの中でエラーが起きた模様
this.lib = __webpack_require__(/*! sqlite3 */ "./node_modules/sqlite3/lib/sqlite3.js").verbose();
}
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
throw new Error('Please install sqlite3 package manually');
}
throw err;
}
なぜエラーが起きたかというと、 package.json が無かったということらしい。
/***/ "./node_modules/sqlite3/lib/sqlite3.js":
/*!*********************************************!*\
!*** ./node_modules/sqlite3/lib/sqlite3.js ***!
\*********************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
var binary = __webpack_require__(/*! node-pre-gyp */ "./node_modules/sqlite3/node_modules/node-pre-gyp/lib/node-pre-gyp.js");
var path = __webpack_require__(/*! path */ "path");
// ↓ エラーメッセージ「Error: package.json does not exist at <prj>\dist\package.json」が起きた個所
var binding_path = binary.find(path.resolve(path.join(__dirname,'../package.json')));
var binding = __webpack_require__("./node_modules/sqlite3/lib sync recursive")(binding_path);
var sqlite3 = module.exports = exports = binding;
var EventEmitter = __webpack_require__(/*! events */ "events").EventEmitter;
エラーが起きた .find() は、元々は sqlite3 自身のpackage.jsonを相対パスで参照するように作られていた。その package.jsonには以下のようにバイナリ(.node)がどこにあるかの情報が書かれていたので、恐らくバイナリの場所を調べに行って見つからなかったということらしい。
"binary": {
"module_name": "node_sqlite3",
"module_path": "./lib/binding/{node_abi}-{platform}-{arch}",
"host": "https://mapbox-node-binary.s3.amazonaws.com",
"remote_path": "./{name}/v{version}/{toolset}/",
"package_name": "{node_abi}-{platform}-{arch}.tar.gz"
},
- sqlite3 を require すると、node-pre-gyp の find に「../package.json」を渡す (これは、sqlite3自身のpackage.json)
- 実行時はバンドルファイルを起点とするため「../package.json」は存在せず、エラーが起こる
ネイティブモジュールはWebpackを使うとバイナリの情報にたどり着けなくなり、うまくいかないことが分かった。Webpackのプラグインでも作れば良いかもしれないが、あまりそれに時間をかけたくはないので、ネイティブモジュールは Webpackを諦めるというのが確実のよう。
当座の解決方法
Webpackを使わないようにする必要があるのはElectronも同じで、やはり Externals を使うしかないみたい。
dist/server/package.json
Externalsにすればビルドはできるが起動はできないので、サーバのバンドルファイルと同じ階層に、package.json を用意して、sqlite3をインストールできるようにする。
{
"name": "server",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node-dev boot"
},
"dependencies": {
"sqlite3": "^4.0.0"
},
"devDependencies": {}
}
dist/server/boot.js
バンドルファイル(main.js)は、sqlite3が外部から提供されることが前提なので、そのまま起動するとエラーになる。
Error: Please install sqlite3 package manually
at new ConnectionManager (<prj>\dist\server\main.js:84230:15)
at new SqliteDialect (<prj>\dist\server\main.js:84611:30)
at new Sequelize (<prj>\dist\server\main.js:93712:20)
at Object../app.ts (<prj>\dist\server\main.js:255:17)
これを回避するために、以下のような main の前に sqlite3 が読み込まれるよう以下のような require だけを行うコードを用意する。
sqlite3 = require( 'sqlite3' );
main = require( './main' );
package.json
main.js の代わりに boot.js で起動するために package.json を書き換える。(ビルド時は sqlite3 の本体はいらないので、devDependenciesに移動している)
{
"scripts": {
"start": "node-dev ../dist/server/boot"
},
"devDependencies": {
"sqlite3": "^4.0.0",
}
}