node.jsを10->12に更新したときに起きたエラーと対処内容

表題通りの記事です。
これからnode.jsを10->12に更新する方の助けになれば幸いです。

## 確認環境
* Ubuntu: 18.04.3 LTS
* node.js: 10.16.0、12.14.1
* ビルドツール: gulp (後述しますが4系へのアップデートが必須でした)

## やったこと(全体像)
* [n](https://www.npmjs.com/package/n) をつかってローカル環境のnode.jsを10->12に更新　(参考: [nodeとnpmのバージョン更新方法](https://parashuto.com/rriver/tools/updating-node-js-and-npm))
* `npm ci`して、出てきたエラーを潰す
  * firebaseの更新
  * node-sassの更新
* `npm run build`して、出てきたエラーを潰す
  * gulpを3系->4系へ移行


## エラー1 (firebaseの更新により解消)
最初はイージーケースから。

#### 事象
`npm ci`したところ、大量(体感3分)のログが出力され、最後に以下のエラーが吐かれました。

```
# これ以前にも大量のエラーログが出る
cc1plus: error: unrecognized command line option ‘-Wno-cast-function-type’ [-Werror]
cc1plus: all warnings being treated as errors
grpc_node.target.mk:188: recipe for target 'Release/obj.target/grpc_node/ext/channel.o' failed
make: *** [Release/obj.target/grpc_node/ext/channel.o] Error 1

# 最後の方に出るmakeのログを見ることで、どのパッケージのビルドに失敗したかが分かる
# (repository-nameでgrepすると早く見つかる)
make: ディレクトリ '/home/user1/repository-name/node_modules/grpc/build' から出ます

gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:194:23)
gyp ERR! stack     at ChildProcess.emit (events.js:223:5)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Linux 4.15.0-74-generic
gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "build" "--fallback-to-build" "--library=static_library" "--module=/home/user1/repository-name/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc/grpc_node.node" "--module_name=grpc_node" "--module_path=/home/user1/repository-name/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc" "--napi_version=5" "--node_abi_napi=napi" "--napi_build_version=0" "--node_napi_label=node-v72"
gyp ERR! cwd /home/user1/repository-name/node_modules/grpc
gyp ERR! node -v v12.14.1
gyp ERR! node-gyp -v v5.0.5
gyp ERR! not ok 
node-pre-gyp ERR! build error 
node-pre-gyp ERR! stack Error: Failed to execute '/usr/local/bin/node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js build --fallback-to-build --library=static_library --module=/home/user1/repository-name/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc/grpc_node.node --module_name=grpc_node --module_path=/home/user1/repository-name/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc --napi_version=5 --node_abi_napi=napi --napi_build_version=0 --node_napi_label=node-v72' (1)
node-pre-gyp ERR! stack     at ChildProcess.<anonymous> (/home/user1/repository-name/node_modules/grpc/node_modules/node-pre-gyp/lib/util/compile.js:83:29)
node-pre-gyp ERR! stack     at ChildProcess.emit (events.js:223:5)
node-pre-gyp ERR! stack     at maybeClose (internal/child_process.js:1021:16)
node-pre-gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:283:5)
node-pre-gyp ERR! System Linux 4.15.0-74-generic
node-pre-gyp ERR! command "/usr/local/bin/node" "/home/user1/repository-name/node_modules/grpc/node_modules/.bin/node-pre-gyp" "install" "--fallback-to-build" "--library=static_library"
node-pre-gyp ERR! cwd /home/user1/repository-name/node_modules/grpc
node-pre-gyp ERR! node -v v12.14.1
node-pre-gyp ERR! node-pre-gyp -v v0.12.0
node-pre-gyp ERR! not ok 
```
#### 調べたこと
ログにあるとおり、grpcのビルドに失敗しています。
(最初は大量のログにとまどいました。どのパッケージがインストールできていないか調べるのが第一歩です)

更新対象リポジトリのpackage.jsonにはgrpcの記述がなく、
`npm ls `で依存パッケージを調べたところ、firebaseがgrpcに依存していました。

```
$ npm ls
# 結果を抜粋
├─┬ firebase@5.5.7
│ ├─┬ @firebase/firestore@0.8.6
│ │ ├─┬ grpc@1.13.1 <- ここでエラー
```

#### 解決策
執筆時点でfirebaseは7系が最新。単純に古いので、アップデートしたところ解決。
(メジャーバージョン更新による影響も今のところなし)

```
$ npm install -D firebase
```



<br>
<br>

## エラー2 (node-sassの更新により解消)
エラー1をちょっとひねったパターン。

#### 事象
`npm ci`したところ、大量(体感3分)のログが出力され、最後に以下のエラーが吐かれました。

```
# これ以前にも大量のエラーログが出る
/home/user1/.node-gyp/12.14.1/include/node/v8.h:3039:5: note:   candidate expects 2 arguments, 1 provided
binding.target.mk:129: recipe for target 'Release/obj.target/binding/src/create_string.o' failed
make: *** [Release/obj.target/binding/src/create_string.o] Error 1

# 最後の方に出るmakeのログを見ることで、どのパッケージのビルドに失敗したかが分かる
make: ディレクトリ '/home/user1/repository-name/node_modules/node-sass/build' から出ます

gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/home/user1/repository-name/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack     at ChildProcess.emit (events.js:223:5)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Linux 4.15.0-74-generic
gyp ERR! command "/usr/local/bin/node" "/home/user1/repository-name/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--verbose" "--libsass_ext=" "--libsass_cflags=" "--libsass_ldflags=" "--libsass_library="
gyp ERR! cwd /home/user1/repository-name/node_modules/node-sass
gyp ERR! node -v v12.14.1
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok 
Build failed with error code: 1
```

#### 調べたこと
ログにあるとおり、node-sassのビルドに失敗しています。
とりあえずググったところ、node-sassは[4.12.0](https://github.com/sass/node-sass/releases/tag/v4.12.0)でnode.jsに12対応したそうです。

更新対象リポジトリのpackage.jsonにはnode-sassの記述がなく、
`npm ls `で依存パッケージを調べたところ、gulp-sassがnode-sassの4.11に依存していました。

```
$ npm ls
# 結果を抜粋
├─┬ gulp-sass@4.0.2
│ ├─┬ node-sass@4.11.0  ←ここでエラー(4.12以降にする必要がある)
```
私は最初、この段階で「詰んだ」と勘違いしました。
gulp-sassの依存パッケージを変えるには、gulp-sass公式にissueなりPRなりを投げてpackage.jsonを書き換えなければならないと思ったからです。

#### 解決策
明示的にnode-sassをインストールしたところ、nodeがよしなにやってくれました。

```
$ npm install -D node-sass@4.13.0  # 実行時点での最新バージョン
$ npm ls
# 結果を抜粋
├─┬ gulp-sass@4.0.2
│ ├── node-sass@4.13.0 deduped     <- 明示的に4.13を入れたところ、nodeが重複パッケージをまとめてくれた 
```

上記にdeduped(=重複排除)と表示されていますが、
これはnodeが「バージョン違いの同一パッケージを1つに統合した」ことを表しています。
(今回で言うとnode-sassの4.11, 4.13のうち、より新しい4.13に統合された)

この仕様は、今回のようなケースのほかに、
セキュリティ上の脆弱性を持つ孫パッケージ(こういう呼び方があるかは不明)のバージョンを更新したいときにも役立ちます。
(参考: https://cloudpack.media/41572)

```
$ npm ls
├─┬ lib-nobody-update@1.1.0 # 更新が止まっているライブラリ
│ ├── lib-something@2.2.0   # 2.3.0で脆弱性が修正されたライブラリ

$ npm install -D lib-something@2.3.0
$ npm ls
├─┬ lib-nobody-update@1.1.0
│ ├── lib-something@2.3.0 deduped
Ｌlib-something@2.3.0
```

今回初めて知りましたが、なかなかに素敵。

<br>
<br>

## エラー3 (gulpを3系->4系へ移行することで解消)
最後はgulpの話です。

#### 事象
`npm ci`実行後、`npm run build`したところ以下のエラーが発生。

```
[18:21:08] Requiring external module @babel/register
fs.js:27
const { Math, Object } = primordials;
                         ^

ReferenceError: primordials is not defined
    at fs.js:27:26
    at req_ (/home/user1/repository-name/node_modules/natives/index.js:143:24)
    at Object.req [as require] (/home/user1/repository-name/node_modules/natives/index.js:55:10)
    at Object.<anonymous> (/home/user1/repository-name/node_modules/vinyl-fs/node_modules/graceful-fs/fs.js:1:37)
    at Module._compile (internal/modules/cjs/loader.js:955:30)
    at Module._compile (/home/user1/repository-name/node_modules/pirates/lib/index.js:99:24)
    at Module._extensions..js (internal/modules/cjs/loader.js:991:10)
    at Object.newLoader [as .js] (/home/user1/repository-name/node_modules/pirates/lib/index.js:104:7)
    at Module.load (internal/modules/cjs/loader.js:811:32)
    at Function.Module._load (internal/modules/cjs/loader.js:723:14)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! my-package-name@0.0.0 build-gulp: `gulp build`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the my-package-name@0.0.0 build-gulp script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/user1/.npm/_logs/2020-01-15T09_21_13_322Z-debug.log
ERROR: "build-gulp" exited with 1.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! my-package-name@0.0.0 build: `run-p build-gulp build-client-ts`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the my-package-name@0.0.0 build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/user1/.npm/_logs/2020-01-15T09_21_13_421Z-debug.log
```

#### 調べたこと

一見して何のエラーか分かりませんでしたが、とりあえずエラーメッセージでググったところ
gulp3系がnode12に未対応なのが原因だと判明。gulp4系は対応済み。

いっそビルド関連をwebpackに置き換えることも考えましたが、
今回は対応速度を重視してgulpを更新する方針にしました。

#### 解決策

以下を参考にgulpを更新しました。
(破壊的変更が多く、そこそこ面倒でした)

* [公式ドキュメント: CommonJS(require)での記載例(英語)](https://gulpjs.com/docs/en/getting-started/creating-tasks)
* [公式GitHub: ESModule(import)での記載例(英語)](https://github.com/gulpjs/gulp#use-latest-javascript-version-in-your-gulpfile)
* [概要を把握しやすかった記事(日本語)](https://satoyan419.com/gulp-v4/)
* [gulp.task()が非推奨であることに言及した記事(日本語)](https://qiita.com/cognitom/items/7a097d0f6e239d4fb5bf)

<br>

要点をかいつまむと、以下のようになります。

* gulp.task()は非推奨になった(この点に触れない記事が多い)
  * 後方互換性のため、一応まだ使える 
  * 代わりに関数をexportするスタイルになった
* タスクの直列実行、並列実行は新関数gulp.series(), gulp.parallel()で制御

```js
// 3系: gulp.task(”タスク名”, タスク用の関数)
gulp.task("buildFrontend", () => { /* フロントエンドのビルド処理 */ });
gulp.task("buildBackend", () => { /* バックエンドのビルド処理 */ });
gulp.task("build", ["buildFrontend", "buildBackend"]);  // []は並列実行

// 4系(パターン1): export const タスク名 = タスク用の関数;
export const buildFrontend = () => { /* フロントエンドのビルド処理 */ };
export const buildBackend = () => { /* バックエンドのビルド処理 */ };
export const build = gulp.parallel(buildFrontend, buildBackend);

// 4系(パターン2): 大元のタスクを上に記載する場合はfunctionで書く
export async function build() { await gulp.parallel(buildFrontend, buildBackend) };
export function buildFrontend() { /* フロントエンドのビルド処理 */ };
export function buildBackend() { /* バックエンドのビルド処理 */ };
```

3系に比べて可読性があがりましたね。

なお、パターン1で大元のタスクを上に持ってくると、エラーになります。
大元のタスクを上に書きたい場合は、パターン2のように書くとうまくいきました。(@shisama さん、指摘ありがとうございました!)

```
// ダメな例
export const build2 = gulp.parallel(buildFrontend2, buildBackend2);
export const buildFrontend2 = () => { /* フロントエンドのビルド処理 */ };
export const buildBackend2 = () => { /* バックエンドのビルド処理 */ }; 


// 実行すると以下のエラーが出る
$ npx gulp build
> my-package-name@0.0.0 build /home/duser1/repository-name
> gulp build

/home/duser1/repository-name/gulpfile.js:10
const build = gulp.parallel(buildFrontend2, buildBackend2);
                            ^
ReferenceError: Cannot access 'buildFrontend2' before initialization
    at Object.<anonymous> (/home/duser1/repository-name/gulpfile.js:10:29)
    at Module._compile (internal/modules/cjs/loader.js:955:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:991:10)
    at Module.load (internal/modules/cjs/loader.js:811:32)
    at Function.Module._load (internal/modules/cjs/loader.js:723:14)
    at Module.require (internal/modules/cjs/loader.js:848:19)
    at require (internal/modules/cjs/helpers.js:74:18)
    at execute (/home/duser1/repository-name/node_modules/gulp/node_modules/gulp-cli/lib/versioned/^4.0.0/index.js:36:18)
    at Liftoff.handleArguments (/home/duser1/repository-name/node_modules/gulp/node_modules/gulp-cli/index.js:201:24)
    at Liftoff.execute (/home/duser1/repository-name/node_modules/liftoff/index.js:201:12)
    at module.exports (/home/duser1/repository-name/node_modules/flagged-respawn/index.js:51:3)
    at Liftoff.<anonymous> (/home/duser1/repository-name/node_modules/liftoff/index.js:191:5)
    at /home/duser1/repository-name/node_modules/liftoff/index.js:149:9
    at /home/duser1/repository-name/node_modules/v8flags/index.js:138:14
    at /home/duser1/repository-name/node_modules/v8flags/index.js:41:14
    at /home/duser1/repository-name/node_modules/v8flags/index.js:53:7
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! my-package-name@0.0.0 build: `gulp build`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the my-package-name@0.0.0 build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/duser1/.npm/_logs/2020-01-15T10_04_26_061Z-debug.log
```    


<br>
<br>

## まとめ
* バージョン更新は思った以上に大変
* こまめな更新が、バージョン更新のコストを下げる
* npm lsでパッケージの依存関係をチェックできる
* nodeにはdedupeという素敵機能がある
* node12でgulp3系は使えない。4系に移行するか、webpackなど他ツールへ移行する

<br>

webpackちゃんと使えるようになりたいです。