SeaPig が依存する node モジュールアップデート時の作業メモ

![SeaPig](https://raw.githubusercontent.com/yasumichi/seapig/master/seapig.png)[SeaPig is converter from markdown to html with marked.js and highlight.js.](https://github.com/yasumichi/seapig)

丸三年、SeaPig の開発を放置していたら、世界は一変してしまいました。

開発者にしか表示されないらしいですが、GitHub で Securiy Alert が表示されていたので依存パッケージのアップデートを開始しました。

# npm-check-updates の導入

- [npm-check-updates - npm](https://www.npmjs.com/package/npm-check-updates)
- [tjunnone/npm-check-updates: Find newer versions of package dependencies than what your package.json or bower.json allows](https://github.com/tjunnone/npm-check-updates)
- [package.jsonのnpmのバージョンを一括で書き変えてくれるncuが便利だった - tacamy--blog](http://tacamy.hatenablog.com/entry/2016/08/10/193603)

依存パッケージのアップデートを調べて package.json を更新してくれるということでインストール。

`npm install -g npm-check-updates`

実際に使ってみる。

`npm-check-updates -u`

で、`npm install` したら、色々、Deplicated が…

# Babel 周りのパッケージ命名規則変更

- [Babel · The compiler for next generation JavaScript](https://babeljs.io/)
- [Babelの設定を見直すための逆引きガイド](https://aloerina01.github.io/blog/2018-03-19-1)

```diff
-    "babel-preset-es2015": "^6.14.0",
-    "babel-preset-power-assert": "^1.0.0",
-    "babel-register": "^6.14.0",
+    "@babel/cli": "^7.6.0",
+    "@babel/core": "^7.6.0",
+    "@babel/preset-env": "^7.6.0",
+    "@babel/register": "^7.6.0",
```

※まだ、元の構成に戻せてない…。つか、何で Babel を導入したかも忘却の彼方…:sweat_smile:。以下あたりも後で読もう。

- [Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法](https://aloerina01.github.io/blog/2019-06-21-1)

# electron-prebuilt の名称変更

単に electron になったらしい。

```diff
-    "electron-packager": "^7.3.0",
-    "electron-prebuilt": "^1.2.7",
-    "electron-rebuild": "^1.1.5",
+    "electron": "^6.0.7",
+    "electron-packager": "^14.0.5",
```

`electron-rebuild` は利用していなかったのでついでに削除。その内、`electron-builder` でインストーラーなど、作れるようにしたい…。

- [electron-userland/electron-builder: A complete solution to package and build a ready for distribution Electron app with “auto update” support out of the box](https://github.com/electron-userland/electron-builder)
- [Electronでアプリビルドまでのフロー - Qiita](https://qiita.com/zaburo/items/828051fc7dabb258f0de)
- [electron-builderでElectronアプリのビルド - Qiita](https://qiita.com/nanairo24/items/73356574b0dc65c0e617)

# electron の update で変わった部分に対応

ここで実際に `npm start` してみたが、起動するもののまともに動作していない。Developer Tools で見てみると `Uncaught ReferenceError: require is not defined` ってエラーが。

## Node.js の統合が Default では無効になったらしい

[Electron + Node.js 使用時の “Uncaught ReferenceError: require is not defined” への対処 - Qiita](https://qiita.com/okadato623/items/f8b7573ad911ca97ba49) にそのものずばりの対処法が。

BrowserWindow 生成時に Node.js の統合機能を有効にするらしい。

```diff
diff --git a/js/seapig.js b/js/seapig.js
index 12b20ae..a127798 100644
--- a/js/seapig.js
+++ b/js/seapig.js
@@ -79,7 +79,10 @@ function createWindow() {
     height: W_HEIGHT,
     x: winList.length * SHIFT % (screenWidth - W_WIDTH),
     y: winList.length * SHIFT % (screenHeight - W_HEIGHT),
-    icon: path.join(__dirname, '../seapig.png')
+    icon: path.join(__dirname, '../seapig.png'),
+    webPreferences: {
+        nodeIntegration: true
+    }
   });

        // Load mainwindow.html
```

ただ、デフォルトで無効にしているのは理由がありそうなので追々、調べることにする。

一応、以下あたりもメモしておこう。

- [require is not definedを解消してrequireを使えるようにする - TOEIC940点の文系プログラマー](http://uraway.hatenablog.com/entry/2015/11/30/require_is_not_defined%E3%82%92%E8%A7%A3%E6%B6%88%E3%81%97%E3%81%A6require%E3%82%92%E4%BD%BF%E3%81%88%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B)

### その後、Node.js の統合を無効にした

[Security, Native Capabilities, and Your Responsibility | Electron](https://electronjs.org/docs/tutorial/security) に [2) Do not enable Node.js Integration for Remote Content](https://electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content) って、書いてあるのでリモートコンテンツではないが、`nodeIntegration` の指定を外し、`script` タグではなく、 `webPreferences` の `preload` オプションでレンダラープロセスを指定することにした。この際、相対パスで指定する `require()` がうまくいかなったのでその部分も修正した。差分は、GitHub のコミットビューアーで確認できる。

- [disable nodeIntegration](https://github.com/yasumichi/seapig/commit/39331b55eb5993a7d51b41883229b46b676f368c)

### pleload にしたことで既に編集状態のウィンドウで新たにファイルを開いた際にファイルが開かくなくなったので対処

今までは、WebContents の dom-ready を待っていたが、did-finish-load を待つように変更することで解消した。

```diff:js/seapig.js
@@ -157,7 +157,7 @@ app.on('ready', () => {
       }
       if (isFile && markdownExt.test(fullpath)) {
         let winIndex = winList.push(createWindow()) - IDX_OFFSET;
-        winList[winIndex].webContents.on('dom-ready', () => {
+        winList[winIndex].webContents.on('did-finish-load', () => {
           winList[winIndex].webContents.send('open-file', fullpath);
         });
       } else {
@@ -227,7 +227,7 @@ ipc.on('open-file-dialog', (event, currentFile, isNewWindow) => {
           if (isNewWindow === true) {
             let newWindow = createWindow();
             winList.push(newWindow);
-            newWindow.webContents.on('dom-ready', () => {
+            newWindow.webContents.on('did-finish-load', () => {
               newWindow.webContents.send('open-file', filenames[FIRST_ARG]);
             });
           } else {
```

- [ElectronのWindowをコンテンツの準備が出来てから表示する - Qiita](https://qiita.com/aqua2117822/items/b735d487fd03200448fb)

## 気になる警告（調査中）

```
Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security
    Policy set or a policy with "unsafe-eval" enabled. This exposes users of
    this app to unnecessary security risks.
 
For more information and help, consult
https://electronjs.org/docs/tutorial/security.
 This warning will not show up
once the app is packaged.
```

後で以下あたりをもう少し勉強してみる。

- [Security, Native Capabilities, and Your Responsibility | Electron](https://electronjs.org/docs/tutorial/security)
- [セキュリティを検討する：ElectronでWebブラウザを作る（その３） | 悠雀堂ブログ](http://www.yujakudo.com/blogs/develop/node-jselectron/test-securitycheck-of-electron-app/)
- [Electronの基本/セキュリティ/エンタープライズ - Qiita](https://qiita.com/tkiryu/items/697174c9539deed50bd0)

[Security, Native Capabilities, and Your Responsibility | Electron](https://electronjs.org/docs/tutorial/security)の[6) Define a Content Security Policy](https://electronjs.org/docs/tutorial/security#6-define-a-content-security-policy) に書いてある処置が必要なようだ。

Content Security Policy 自体も勉強する必要があるな…。

- [コンテンツセキュリティポリシー (CSP) - HTTP | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/CSP)
- [javascript – コンテンツセキュリティポリシーはどのように機能しますか？ - コードログ](https://codeday.me/jp/qa/20181210/55743.html)
- [https://qiita.com/clustfe/items/4dce9a3d1444ad245f5d](https://qiita.com/clustfe/items/4dce9a3d1444ad245f5d)

## webview のエラー

ace エディター部分の挙動不審も気になるが、HTML プレビューの部分が全く動いていない。

Developer Tools が、`Uncaught TypeError: webview.send is not a function` と言っているので webview.send は廃止されたか関数でなくなったか？

[&lt;webview&gt; Tag | Electron](https://electronjs.org/docs/api/webview-tag#webviewsendchannel-arg1-arg2-) を見る限りはまだ存在していそうなのでこれも Node.js 統合の関係かしら？

- [ElectronのWebViewで遊ぶ - Qiita](https://qiita.com/shoota_github/items/96779fd150277b89b33b)
- [Electron : webviewと非同期通信する](https://uchy.me/blog/20180628/)

今一度、[&lt;webview&gt; Tag | Electron](https://electronjs.org/docs/api/webview-tag) を読んでみると今は使うことを推奨していなくて、代わりに iframe や Electron の BrowserView を使うように書いてあった :sweat_smile: 

- [&lt;webview&gt;からBrowserViewへの移行 - Qiita](https://qiita.com/nekobato/items/476cf1ce626626972f30)
- [Electron BrowserViewについてサンプルを読みつつ紹介 - Qiita](https://qiita.com/maron8676/items/93b768f2a0206188664f)

先ほどの続きの [Enabling](https://electronjs.org/docs/api/webview-tag#enabling) の節に Electron >= 5 では、デフォルトで無効になっているので BrowserWindow の生成時に webPreferences で webviewTag を指定するように書いてあった。

```diff
     height: W_HEIGHT,
     x: winList.length * SHIFT % (screenWidth - W_WIDTH),
     y: winList.length * SHIFT % (screenHeight - W_HEIGHT),
     icon: path.join(__dirname, '../seapig.png'),
     webPreferences: {
-        nodeIntegration: true
+        nodeIntegration: true,
+        webviewTag: true
     }
   });
```

とりあえず、上記オプションの追加で動作するようになったので &lt;webview&gt; から、別の実装方法への変更は、次の機会に行うことにする。

### その後、&lt;webview&gt;の使用を廃止した（2019/10/05）

移行候補となっている [BrowserView](https://electronjs.org/docs/api/browser-view)は、HTML と CSS による位置制御ができないため、見送り。メインウィンドウおよびレンダラープロセスにプレビューペインを統合することにした。

ネックになったのはPDF出力です。特定の要素を PDF 出力する機能が標準で用意されていないため、 &lt;webview&gt;.printToPdf() を使うために &lt;webview&gt; の使用を継続してとも言えます。

今回、[指定のDIV要素をPDFファイルへ出力する例 (babel等無しに直接実行できる状態) ](https://github.com/jpzukin/electron-sample-print-to-pdf)というサンプルを見つけることができたので統合に踏み切れました。

この際、[worker.html](https://github.com/jpzukin/electron-sample-print-to-pdf/blob/master/worker.html) で

```javascript
// DOM変更後、スタイルが反映される前に処理が進み印刷イメージが崩れる
// 反映待ちを行っているが、対象のイベントが取れるか調査中。
setTimeout(() => { ipcRenderer.send("ready-print-to-pdf"); }, 1000);
```

としている部分は、以下のように定期的に画像の読み込み状態を確認し、すべての画像が読み込まれるのを待ってから、メインプロセスに送信するように変更しました。

```javascript:seapig/js/pdfWorker.js 
  const waitImagesComplete = () => {
    var ilist = document.images;
    for(var i = 0; i < ilist.length; i++) {
      if(ilist[i].complete === false) {
        setTimeout(waitImagesComplete, 1000);
        return;
      }
    }
    ipcRenderer.send('ready-print-to-pdf', thisPdfPath);
  }
```

なお、&lt;webview&gt; の使用廃止に関するコミットは、[remove webview.(close #50) ](https://github.com/yasumichi/seapig/commit/8d7872ec659b6c2a0b408f1274159f393b96ca73) で確認できます。

## ウィンドウを閉じる際に出していた確認ダイアログが出なくなった

```
 Attempting to call a function in a renderer window that has been closed or released.
Function provided here: undefined
```
正確には出てすぐ消える。今までレンダラープロセスの中で window の beforeunload イベントを捕まえてこの処理を行っていたが、このイベントに入る前に BrowserWindow が破棄されてしまうぽい…。

```javascript:js\renderer.js(抜粋)
// before unload
window.addEventListener("beforeunload", (event) => {
  if (docStatus.modified === true) {
    let msg = `The document has not yet been saved.
      Are you sure you want to quit?`;
    let result = dialog.showMessageBox(
        remote.getCurrentWindow(),
        {
          type: "info",
          title: "SeaPig",
          message: msg,
          buttons: ["OK", "Cancel"]
        }
    );
    if (result === CANCEL) {
      event.returnValue = false;
    }
  }
});
```

なお、`dialog.showMessageBox()` は、非同期処理になり `return` されるのは、`Promise` というオブジェクトに変わった。以前と同じ同期処理を行うには、`dialog.showMessageBoxSync()` を利用する必要がある。

メインプロセス側の `close` イベントにこの処理を移譲しようとしたが、`event.preventDefualt()` を発行しないとダイアログが表示されない。かと言って、発行してしまうと BrowserWindow が閉じれなくなってしまうというジレンマ。

やっと参考になるページを発見。そうか、`event.preventDefualt()` を発行しつつ、閉じる場合は、`BrowserWindow.destroy()` でいけるのか。

- [How to show confirmation dialog message box in Electron.JS](https://www.brainbell.com/javascript/dialog-show-message-box.html)

これまでメインプロセス側は、ドキュメントの編集状態を保持していなかったので保持するための変数を用意し、レンダラー側から編集状態が変わった際に `ipcRenderer.send()` でメインプロセスに送信するようにし、メインプロセス側でそれを受け取れるようにした。

長くなるので何を変更したかは、GitHub のコミットビューアーで。

- [fixed process of close request · yasumichi/seapig@bceed63](https://github.com/yasumichi/seapig/commit/bceed63ec003c2416a1c79c63b390b1c83b0b464)

# ace の警告

ace エディター部分の挙動不審は、editor の change イベントの際に渡される event オブジェクトのプロトタイプが変わったためでした。

しかし、DevTools に以下の警告が出ているのに気づく。

```
Automatically scrolling cursor into view after selection change this will be disabled in the next version set editor.$blockScrolling = Infinity to disable this message
```
`editor.$blockScrolling = Infinity` を設定すれば、このメッセージを無効にできそうだけど、ちょっと調べてから。

[setValue() emits $blockScrolling = Infinity warning · Issue #2499 · ajaxorg/ace](https://qiita.com/naga3/items/1bc268243f2e8a6514e5) がズバリかな。

つか、使っている ace-min-noconflict が４年もメンテされていないのに気付いた。これは、本家のをバンドルするようにした方が良さげだな…。

[ajaxorg/ace-builds: Packaged version of Ace code editor](https://github.com/ajaxorg/ace-builds) を git のサブモジュールにして、ビルド時に取り込むようにする。

- [Git - サブモジュール](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E3%82%B5%E3%83%96%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB)

```terminal
git submodule add https://github.com/ajaxorg/ace-builds.git
```

ace-builds 自体は、node モジュールとしても用意されているが、同じ働きをするソースが４種類含まれてしまうので git のサブモジュールとして追加し、ビルド時に必要なファイルだけをコピーするようにするスクリプトを作成。

```javascript:scripts/build-dep.js
const fs = require('fs');
const path = require('path');

const destdir = "ace";
const srcfiles = [
    'ace-builds/src-min-noconflict/ace.js',
    'ace-builds/src-min-noconflict/theme-twilight.js',
    'ace-builds/src-min-noconflict/mode-markdown.js',
    'ace-builds/src-min-noconflict/keybinding-emacs.js',
    'ace-builds/src-min-noconflict/keybinding-vim.js',
];

if ( fs.existsSync(destdir) == false ) {
    fs.mkdirSync(destdir);
    srcfiles.forEach((value) => {
        var destfile = path.join(destdir, path.basename(value));
        fs.copyFileSync(value, destfile);
    });
}
```

`npm run-script build-dep` で実行できるように package.json に script を追加した。

```json
  "scripts": {
    "build-dep": "node scripts/build-dep.js",
  },
```

# mermaid のアップデート

過去のバージョンで配られていた mermaidAPI.min.js を同梱していたが、 `devDependencies` に `mermaid` を追加し、`node_modules/mermaid/dist/mermaid.min.js` を `external/mermaid/` にコピーし、これを参照するように変更した。

以前は、mermaidAPI が独立していたが、mermaid.min.js に同梱されるようになってしまったので呼び出し方を変更する。

```diff:js/utils.js
@@ -1,7 +1,7 @@
 const hljs = require('highlight.js');
 const viz = require("viz.js");
 const uiflow = require("uiflow");
-const mermaidAPI = require('../external/mermaidAPI.min.js');
+const mermaidAPI = require('../external/mermaid/mermaid.min.js').mermaidAPI;
 const FIRST_ITEM = 1;
 const COUNT_UP = 1;
```

また、以前は `window.onload` を待つ必要がなかったが、今は待つ必要があるみたいなので `&lt;webview&gt;` で `preload` に指定していた `js\preview.js` の処理を以下の処理の中に入れるようにした。

```javascript
  window.onload = function () {
// (ここに今までの処理を移動)
  }
```

ここで ace 関連ファイルをコピーした処理に組み込むことにした。

```javascript:scripts/build-dep.js
const fs = require('fs');
const path = require('path');

const extdir = "external";

const copyplan = [
  // for ace
  {
    destdir: path.join(extdir, "ace"),
    srcfiles: [
      'ace-builds/LICENSE',
      'ace-builds/src-min-noconflict/ace.js',
      'ace-builds/src-min-noconflict/keybinding-emacs.js',
      'ace-builds/src-min-noconflict/keybinding-vim.js',
      'ace-builds/src-min-noconflict/mode-markdown.js',
      'ace-builds/src-min-noconflict/theme-twilight.js',
    ]
  },
  // for mermaid
  {
    destdir: path.join(extdir, "mermaid"),
    srcfiles: [
      'node_modules/mermaid/LICENSE',
      'node_modules/mermaid/dist/mermaid.min.js',
    ]
  }
];

copyplan.forEach((value) => {
  var destdir = value.destdir;
  var srcfiles = value.srcfiles;

  if ( fs.existsSync(destdir) == false ) {
    fs.mkdirSync(destdir, {recursive: true});
    srcfiles.forEach((value) => {
      var destfile = path.join(destdir, path.basename(value));
      fs.copyFileSync(value, destfile);
    });
  }
});

```

# Mocha 周り

元々、使いこなしていなかった訳だが…。忘却の彼方だし。

- [Mocha - the fun, simple, flexible JavaScript test framework](https://mochajs.org/)
- [mocha + power-assert環境の構築 - Qiita](https://qiita.com/gitseitanaka/items/ea47d261284879a1d774)
- [Mochaを用いたJavaScriptのテスト実行 - Qiita](https://qiita.com/wifeofvillon/items/baf1956904af5a621887)
- [ユニットテストって何?って人向けのmochaとchaiの使い方](https://qiita.com/y_hokkey/items/f73ea6b3d5f6902396b6)

## assert が未定義になる

```diff
diff --git a/test/md2html.test.js b/test/md2html.test.js
index 5e75d30..f4250b7 100644
--- a/test/md2html.test.js
+++ b/test/md2html.test.js
@@ -1,4 +1,4 @@
-import assert  from 'assert';
+var assert = require('assert');
 const marked = require('marked');
 const Md2Html = require('../js/md2html.js');
 var md2html = new Md2Html();
```

import から require に変えたら、何とかなった。import はトランスパイルするなり、拡張子を mjs にするなりしないと使えないらしい。

:sweat_smile: このために Babel 導入していたんだね…。 

- [【Node.js入門】初心者でも分かるimport / exportでモジュールを使う方法まとめ！ | 侍エンジニア塾ブログ（Samurai Blog） - プログラミング入門者向けサイト](https://www.sejuku.net/blog/86754)
- [Node v9 で ES Module import を使ってみる - Qiita](https://qiita.com/ryohji/items/93f5050b9af6fc15693c)

## テスト対象が mermaid を require する時に `SVGElement is not defined` という例外が発生

ヘッドレスブラウザーとして [jsdom](https://github.com/jsdom/jsdom) を導入。

```
npm install --save-dev jsdom
```

SVGElement を以下のように定義した。

```javascript
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const { window } = new JSDOM();
global.SVGElement = window.SVGElement;
```

## しかし、mermaid を最新にしたら元の木阿弥に…

以下のように window を global で参照できるようにしたら、テストが動くようになった。

```diff
diff --git a/test/md2html.test.js b/test/md2html.test.js
index 50a2837..e61a276 100644
--- a/test/md2html.test.js
+++ b/test/md2html.test.js
@@ -2,6 +2,7 @@
 const jsdom = require("jsdom");
 const { JSDOM } = jsdom;
 const { window } = new JSDOM();
+global.window = window;
 global.SVGElement = window.SVGElement;

 var assert = require('assert');
```

## --compiler オプションは廃止されたらしい

package.json で以下のように test スクリプトを定義していました。


```json
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js",
  },
```

`npm test` を実行すると以下のエラーが出ました。

```
× ERROR: --compilers is DEPRECATED and no longer supported.
          See https://git.io/vdcSr for migration information.
```

- [compilers deprecation · mochajs/mocha Wiki](https://github.com/mochajs/mocha/wiki/compilers-deprecation)

--compiler は廃止され、--require で指定するようになったらしい。でも引数なしで mocha を起動してもテストが実行できたので引数を取っ払うことにした。

```diff
diff --git a/package.json b/package.json
index b922ebb..6e3b3fe 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
   "main": "js/seapig.js",
   "scripts": {
     "start": "electron .",
-    "test": "mocha --compilers js:babel-register test/**/*.test.js",
+    "test": "mocha",
     "lint": "eslint",
     "lint-all": "eslint js",
     "clean": "rimraf releases *.log",
```

Babel いらんやん… :sweat_smile:

# Marked のバージョンアップに伴う修正

[Marked](https://github.com/markedjs/marked) のバージョンが、0.3.5 から 0.7.0 に変わったことで [GitHub Flavored Markdown](https://github.github.com/gfm/) の Task list items をサポートするようになったらしい。

そのため、Block level renderer methods の listitem() のプロトタイプが `listitem(string text)` から `listitem(string text, boolean task, boolean checked)` に変わって、text として返される部分が変わったらしい。

これまで自前で Task list items に対応させていたが、その必要もなくなったので独自のレンダラーメソッドを削除し、テストも削除した。

例によって、差分は GitHub のコミットビューアーに任せます。

- [remove rendererListItem because marked already suppot gfm tasklist](https://github.com/yasumichi/seapig/commit/f333514aff4aaf9daea82756a4ab2792cdb62bd7)

## Marked に任せたは良いが、冒頭にリストマーカーがついてしまう

とりあえず、Marked で HTML に変換した後、最初の子要素にチェックボックスを持つ li タグに class を追加するコードを書く。

```javascript
      // process task list items
      var listitems = document.getElementsByTagName("li");
      for(var i=0; i<listitems.length; i++) {
        var fchild = listitems[i].firstElementChild;
        if(fchild != null && fchild.nodeName === "INPUT" && fchild.type === "checkbox") {
          listitems[i].classList.add("task-list-item");
        }
      }
```

そろそろ、仮想 DOM を使うフレームワークに移行したいところ。

# Viz.js のアップデートに伴う修正

[Viz.js](https://github.com/mdaines/viz.js) の API が丸っきり変わっているため、viz.js support と uiflow support が動いていないことに気づいた。

新しい使い方は、[Usage](https://github.com/mdaines/viz.js/wiki/Usage#as-a-commonjs-module-such-as-in-nodejs) を参照してください。[API (1.x)](https://github.com/mdaines/viz.js/wiki/API-(1.x)) と比べると単純ではなくなりました。

非同期になると Marked の renderer で使いにくいのでとりあえず、1.x 系列で最新の v1.8.2 に戻すことにした。