• 作成:

electron-builderがnode_modulesのディレクトリをapp.asarにパッケージングしない原因はapp/package.jsonに依存を書いていないからでした

問題解決を行おうと思ったのですが徐々に行き詰まってきてしまったので, 問題点と調べた内容を整理するためにメモを取ります.

問題

Electronアプリケーションをelectron-builderでパッケージングすると, Semantic UI LESS のアイコンが表示されません.

原因

ビルドとパッケージの手順は, webpackでsrcからappにデータをビルド, appの内容をdistにパッケージするようになっています.

ビルドしたときはきちんと app/node_modules/semantic-ui-less/themes/default/assets/fonts にアイコンフォントはコピーされるのですが, electron-builderでパッケージングすると, node_modulesディレクトリはapp.asarにパッケージングされずに除外されてしまいます.

副産物としてビルド組み込みを改善できました

Semantic UI LESSをアプリケーションに組み込むにあたって, postinstallのたびにnode_modulesの内部を書き換えるようになっていました. しかし本来バージョンが同じな場合, パッケージ管理システムの外にあるnode_modulesの内容は常に同じになっているのが望ましいので, あまり好ましい方法とは思っていませんでした. しかし他の方法で組み込む方法がわからなかったので異論は挟みませんでした.

しかしこの問題を追いかけていく過程で常に書き換えなくてもビルドに組み込むことが出来るとわかったので, そのように書き換えました.

theme.configを上書きしないと自分のテーマが作れない問題

Using semantic-ui in webpack project - Stack Overflow を参考にしました.

webpack.configに以下のようにresolve.aliasを設定すれば良いです.

  resolve: {
    alias: {
      '../../theme.config': path.join(
        __dirname,
        'src',
        'theme.config'
      ),
    },
  },

imageとfontのパスがズレてしまう問題

Erroneous Icon Font URL · Issue #4174 · Semantic-Org/Semantic-UI を参考にしました.

要するに変数@imagePath@fontPathの内容を上書きすれば良いので, site/site.variablesに以下のように書けば良いです.

@imagePath : '../../themes/default/assets/images';
@fontPath : '../../default/assets/fonts';

なんでデフォルトのテーマを使うために変数を上書き的に上書きしないといけないのか, 読み込みパスの初期位置が違うのか全くわかりません.

こういうことがあるのでSemantic UIはあまり信用できない…

Selenium(Spectron)によるwebフォントを含めた404エラーの検知自動化

直す前にまずテストを自動化しないと, 直ったかを調べるために一々手動で確認しないといけないのでストレスですし, 直してもリグレッションバグが発生する可能性も高いです.

しかし「コンソールにエラーログが出ているか」を調べる方法が1メソッドで用意されていなかったので, 自分で書く必要がありました.

404エラーの検知などのワードで検索しましたが,

  • imgタグを全部見てheightをチェックする
  • titleに404を含むかチェックする

といったものしか出なくて, 私の要望を満たすものではありませんでした.

読み込みに失敗しているのはアイコンフォントであって, タイトルにもエラーを示す文章は無いからです.

Webdriverにはログを取るlogメソッドがあったので, これを使えばログをJSONで取得できます. WebdriverIO - log

jestに素直にcallbackでデータをチェックする述語(hspecで言うshouldSatisfy)がなかったので, extendで述語を書く必要がありました.

// ログに致命的なエラーデータを含むことのテスト
expect.extend({
  toHaveSevereLog(received) {
    if (received.value.some(v => v.level === 'SEVERE')) {
      return {
        message: () =>
          `${this.utils.printReceived(received)}が致命的なエラーを含みます`,
        pass: true,
      };
    }
    return {
      message: () =>
        `${this.utils.printReceived(received)}が致命的なエラーを含みません`,
      pass: false,
    };
  },
});

これを以下のように呼び出せばテスト作成完了です.

expect(await client.log('browser')).not.toHaveSevereLog();

electron-builderのpackage.jsonに書ける設定を1つずつ見ていきます

filesオプションは一見ピッタシに見えますが, これはデフォルトで何も無視することは無いのですよね. semantic-ui-lessはdevDependenciesではなくdependenciesに存在しますし.

一応試しに"files": "**/*"と書いてみましたが変化なしです.

本当にこれ設定が反映されているのか? と思ったので"files": "!**/*"と書いてみました. そうしたらパッケージに失敗したので効いてはいるのでしょう.

"files": "node_modules"と書いてみたらpackage.jsonしかasarに含まれませんでした.

LESSとかがdevDependenciesだからなのでは?

全部dependenciesに移してみましたがダメでした.

extraFilesに書いてみては?

これapp以下のリレーションじゃないからダメだと思いますがやってみます. extraFilesだとasarにパッケージングされないのでダメですね. extraResourcesもパッケージしないからダメでしょう.

node_modulesにインストールしない形式には出来ないの?

これはかなり難しいです. 何故ならカスタムしたlessファイル達はみんな @import "~semantic-ui-less/definitions/globals/reset"; のようにチルダ~を使ってimportしているからです. node_modulesではない別ディレクトリにインストールする方法に切り替えると, これを全て修正するだけではなく, 開発版とパッケージ版でコードを切り替える必要があるからです.

issueを検索すると未解決問題だと思えてきました

ドキュメントの情報が足りなかったのでissueをnode_modulesというワードで掘ってみることにしました.

すると以下のissueが出てきました.

electron-builder does not copy directories named "node_modules" into my application · Issue #3104 · electron-userland/electron-builder

node_modulesがコピーされないという私の状況と似た問題です.

これに7月12日に解決方法はないのでafterPack hookでどうにかすると書かれています…

とりあえずafterPackをどう書けば良いのかわからないので, 呼び出してみてcontextをconsole.logで出力してみました.

lazy getterばかりで何もわからない. Nodeのデバッガーを呼び出してみましょう. hookでdebuggerを呼び出す方法がわかりません. 内部でrequireしているようでnodeコマンドを実行させることが出来ないです.

まあデバッガが使えないからconsole.logするにしてもちょっと頭使えば楽になります. Object.entriesを使えば一気にconsole.log出来ます.

  Object.entries(context.packager.info._configuration).forEach(([key, value]) =>
    console.log(key, ":", value)
  );

一気にlazy getterをforceする簡単な方法は無いものでしょうか.

asarにディレクトリをねじ込めば良いのでは?

afterPackのcontextの中身はさっぱりわかりませんが, afterPackということは既にパッケージングは終わっているはずで, そこでapp.asarにファイルを追加してしまえば解決するのではと思いつきました.

最悪に強引な手段なのでやりたくないのですが…

asar読み込み専用でした.

というか読み込み専用という時点で, どうcontextをこねくり回してもasarにディレクトリを追加するのは出来無いことがわかってきました.

ソースコードを読むしかない

electron-builderのソースコードをnode_modulesで検索して全件読みました.

すると electron-builder/packager.ts at 6f8e4ec6c65d3a951f774276b5943c0e66704fb4 · electron-userland/electron-builder が気になりました.

ここのソースコードは

   private get productionDeps(): Lazy<Array<Dependency>> {
	    let result = this._productionDeps
	    if (result == null) {
	      // https://github.com/electron-userland/electron-builder/issues/2551
	      result = new Lazy(async () => {
	        if (this.config.beforeBuild == null || (await exists(path.join(this.appDir, "node_modules")))) {
	          return await getProductionDependencies(this.appDir)
	        }
	        else {
	          return []
	        }
	      })
	      this._productionDeps = result
	    }
	    return result
	  }

となっていてプロダクションの依存関係をappディレクトリで見ています.

そして我々はelectron-builderの推奨する, rootとappの両方にpackage.jsonを置くアーキテクチャを採用しています. Two package.json Structure - electron-builder

よってapp/package.jsondependenciessemantic-ui-lessを追加してやれば解決です.

解決に時間がかかった原因

私はwebpackを使ってほとんどのファイルをバンドルしていました. よってelectron-builderのプロダクション限定での, node_modulesのパッケージ挿入に左右されることがありませんでした.

よってpackage.jsonのプロダクション判断基準を把握せずに運用していたので, コピーされない原因がなかなかわかりませんでした.

もしwebpackを使わずに, 全てasarにJSファイルなどもバンドルして生でrequireしていれば, 必然的にelectron-builder install-app-depsを実行することになっていたので, 同期されていて気が付かなかったことでしょう.

他のnode_modulesのコピーが必要ない環境を作ってしまったのがあだになってしまいましたね.

本当に疲れました… もしかしたら最初からソースコードを見ていれば2時間で解決する話だったのかもしれないと思うと, 徒労感が酷いです.