Corredor

ウェブ、プログラミングの勉強メモ。

Angular ライブラリを AoT コンパイルした上で Uglify する方法は?

速習TypeScript: altJSのデファクトスタンダートを素早く学ぶ! 速習シリーズ

速習TypeScript: altJSのデファクトスタンダートを素早く学ぶ! 速習シリーズ

TypeScript実践プログラミング Programmer's SELECTION

TypeScript実践プログラミング Programmer's SELECTION

Angular4 以降のプロジェクトで NgModule にまとめた便利ライブラリを作り、npm で公開しようと思っていた。そこで、Angular ライブラリを AoT コンパイルして提供できたらいいかもな?と思ってたら結構ドツボにハマったのでメモ…。

AoT コンパイル

Angular には AoT (Ahead-of-Time) というコンパイル方式がある。@angular/compiler-cli パッケージが提供する ngc というコマンドを利用して、Angular プロジェクトを事前にコンパイルしておくのだ。これにより動作速度が速くなったり、ファイルサイズを小さく抑えられたりする。

Angular ライブラリ

Angular は NgModule のみを提供し、「Angular アプリ向けのライブラリ」を作ることができる。Angular ライブラリを npm 等でインストールし、AppModuleimports してもらうものだ。

この場合も、AoT コンパイルを行っておき、Angular ライブラリのファイルサイズを押さえておくことができる。

AoT + Uglify

ところで、この ngc コマンドは、生成するソースファイルを Uglify (ソース圧縮・難読化) してくれない。難読化するオプションもないようなのだ。ソースが圧縮できれば、提供する Angular ライブラリのファイルサイズももう少し減らせそうなのだが…。

そう思い、調べてみた。

Webpack プラグインを使う方法…

Angular CLI の中で Webpack を使っている関係か、Webpack を使った Angular プロジェクトのビルドに関する文献は結構多かったので、色々調べてみた。

日本人 Angular コントリビュータとして有名な Laco さんの記事。@ngtools/webpack が提供する AotPlugin と、UglifyJsPlugin を組み合わせてビルドする方法が紹介されていた。

しかしこれは Angular アプリを AoT コンパイル + Uglify する方法で、Angular ライブラリをビルドする方法として応用できなかった。

次に、ngc-webpack という npm パッケージがあったので試してみたが、どうもビルド時にエラーが発生してしまい使えなかった。

というワケで Webpack を使う方法は断念。

Rollup プラグインを使う方法…

Webpack の後にちょっと話題になってからあんまり話を聞かなくなった Rollup。

アプリケーションにはwebpackを、ライブラリにはRollupを使おう

なんて云われていたりするようなので、ちょっと調べてみた。

すると、Rollup のプラグインに rollup-plugin-angular-aot というモノがあったので、これを使って AoT コンパイルを試みた。

しかし、import * as という記法があるとビルドができなかったり、Import していいる外部 npm パッケージまでまとめてバンドルされてしまったりで、TypeScript のコンパイルを制御しきれず断念…。

もしかして : Angular ライブラリは Uglify できない?

ngc コマンドで Angular ライブラリを生成すると、ビルドされた .js ファイルの他に、.metadata.json.d.ts というファイルを同時に生成してくる。

.js.map については単なるソースマップなので、利用者に配布しなくても問題はない。tsconfig.json"sourceMap": false を指定して出力しないようにしても問題ない。

しかし、.metadata.json.d.ts についてはライブラリの利用者にも配布しなければならず、これらのファイルが欠けると「Error encountered resolving symbol…」といったエラーが発生してしまう。

All referenced libraries must include the .metadata.json file along side any .d.ts files they produce otherwise they will not work correctly with ngc.
The .metadata.json file contains the information we need that was in the original .ts file but was not included in the .d.ts file.
If we don't have that information we cannot generate the factories for the library.

自分の拙い英語力で翻訳すると、

「全ての参照されるライブラリ (Angular アプリから利用する Angular ライブラリ、ということだろう) には、.metadata.json.d.ts ファイルが同梱されていないといけない。そうしないと ngc コマンドが正しく動作しない。.metadata.json は、.d.ts ファイルには含まれていない、元の .ts ファイルの情報が書かれている。この .metadata.json の情報がないと、ライブラリからファクトリを生成できない。」

ということみたいだ。

ということは、Uglify-JS によって複数の .js ファイルを一つに結合してしまうと、後で情報を復元できなくなりそうだ。

個別に .js ファイルを Uglify したら…?

$ ng build --prod --aot のような感じで、AoT コンパイルした結果を1つのファイルにまとめて難読化できたら最高だったが、ngc コマンドでビルドする Angular ライブラリの場合はこうした仕組みは用意されていないようだ。

しかし、ngc で生成した .js ファイルを一つずつ Uglify-JS で変換してみると、コレは上手く Angular アプリで利用できた。ひとまずこのやり方しかないだろうか。

個別に .js ファイルを Uglify するためのスクリプト

ということで、ngc コマンドが生成したディレクトリを対象に、.js ファイルを見付けては個別に Uglify-JS を実行する Node.js スクリプトを書いてみた。

const path = require('path');
const fs   = require('fs');
const uglifyJs  = require('uglify-js');          // Uglify-JS
const recursive = require('recursive-readdir');  // ディレクトリ配下のファイルを再帰的に取得する

// ngc でコンパイルしたファイルが dist/ ディレクトリ配下に置いてあるテイ
// .js 以外のファイルを予め避けておく
recursive(path.resolve(__dirname, 'dist'), ['*.d.ts', '*.metadata.json'], (error, files) => {
  // エラーがあった場合は異常終了
  if (error) {
    console.error(error);
    process.on('exit', () => {
      process.exit(1);
    });
    return;
  }
  
  files.forEach((file) => {
    // .js 以外のファイルを処理しないようにする
    if (!file.endsWith('.js')) {
      return;
    }
    
    // Uglify したコードを取得しておく
    const resultCode = uglifyJs.minify({
      file: fs.readFileSync(file, 'utf8')
    }).code;
    // 同ファイルに書き出す
    fs.writeFileSync(file, resultCode, 'utf8');
  });
});

雑だけどコレで一応難読化っぽいことはできた。ソース圧縮の観点でいえば、単一ファイルにまとめ、Tree Shaking とかできるといいのかもしれないけど…。

何でこんなに情報がないのだろう?

Angular ライブラリを AoT コンパイルして Uglify する、という情報がこんなにも少ないのは何故?と思ったが、それは多分 Angular ライブラリを AoT コンパイルしてから Uglify するメリットがあまりないからかなと思った。

  • OSS としてソースを公開している場合が多く、難読化したコードのみを展開する機会がない。
    • npm パッケージとして提供し、GitHub で管理している場合が多いだろう。
    • そもそも JavaScript なので、ソースの盗用などは回避しきれない面もある。いくら難読化していても、盗用防止の面では限界がある。
  • Angular ライブラリは Angular アプリに Import されて初めて使われるモノ。であれば、ソース圧縮はライブラリを利用するアプリ側でまとめてやれば良いこと。ライブラリ側で予め圧縮しておいて展開するメリットがあまりない。
    • だから Angular アプリを AoT コンパイルして Uglify するノウハウの方は出回っている。

…こんなところかな。元々、盗用を気にするプロダクト向けのノウハウが少ない言語・FW ということもあるし、「圧縮は利用するアプリでやれば結果いいじゃん」という至極もっともな考え方かな、と。

やり方があったら教えてください!

とはいえ、できないことがあるのは、それだけでモヤモヤする。

Angular ライブラリを AoT コンパイル + Uglify するやり方があったら教えてください!

その他

以下試していないけど、AoT とか Uglify とかやれそうな感じのモノ。時間ができたら試す…。