Angularからwasm-bindgenで作成したパッケージを読み込む

# Angularからwasm-bindgenで作成したパッケージを読み込む

## はじめに

wasm-bindgen等のライブラリ・ツールの整備によって、Rustで書いたプログラムをWebフロントエンドで動かすことが現実味を帯びてきました。
しかし、それらを本格的なWebフロントエンドで活用するためには、Angular等のWebフロントエンドフレームワークと共存させる必要があるでしょう。
本稿では、[wasm-bindgenでマンデルブロ集合](https://qiita.com/_likr/items/8868665b2e5238cbb543)の記事で作成したmandelbrotパッケージを例に、[angular-cli](https://cli.angular.io/)（バージョン7.0）で作成したAngularプロジェクトからWebAssemblyのコードを読み込んで動かす際の注意点をご紹介します。

## 準備

```shell-session
$ ng new angular-example
$ cd angular-example
$ npm i path/to/mandelbrot/pkg
```

## コンポーネントの実装

mandelbrotパッケージを動かすのに必要な最小限の部分をコンポーネントに実装します。
必要に応じてモジュール化を行なってください。

```src/app/app.component.ts
import { AfterViewInit, Component, ViewChild } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
  @ViewChild('display') display;

  ngAfterViewInit () {
    const mod = import('mandelbrot')
    const bg = import('mandelbrot/mandelbrot_bg')
    Promise.all([mod, bg]).then(([{ mandelbrot, Screen }, { memory }]) => {
      const canvas = this.display.nativeElement;
      const screen = new Screen(canvas.width, canvas.height);

      const bytes = new Uint8ClampedArray(memory.buffer, screen.pointer(), screen.size());
      const image = new ImageData(bytes, screen.width, screen.height);
      mandelbrot(screen, -3, -2, 0.01, 100);

      const ctx = canvas.getContext('2d');
      ctx.putImageData(image, 0, 0);
    })
  }
}
```

```src/app/app.component.html
<canvas #display width="600" height="400"></canvas>
```

この段階でビルドを行うと以下のようにエラーが出ているかと思います。

```
ERROR in src/app/app.component.ts(12,17): error TS1323: Dynamic import is only supported when '--module' flag is 'commonjs' or 'esNext'.
src/app/app.component.ts(13,16): error TS1323: Dynamic import is only supported when '--module' flag is 'commonjs' or 'esNext'.
src/app/app.component.ts(13,23): error TS2307: Cannot find module 'mandelbrot/mandelbrot_bg'.
```

これを解消していきます。

## Dynamic importの有効化

まず、エラーメッセージの通りDynamic importを有効化するために、`tsconfig.json` を以下のように編集します。

```diff:tsconfig.json
@@ -5,7 +5,7 @@
     "outDir": "./dist/out-tsc",
     "sourceMap": true,
     "declaration": false,
-    "module": "es2015",
+    "module": "esNext",
     "moduleResolution": "node",
     "emitDecoratorMetadata": true,
     "experimentalDecorators": true,
```

再度ビルドを行うと以下のようにエラーが減っていることが確認できます。

```console
ERROR in src/app/app.component.ts(13,23): error TS2307: Cannot find module 'mandelbrot/mandelbrot_bg'.
```

## bgモジュールの型付け

**[追記] `mandelbrot_bg.d.ts` を生成するようにwasm-bindgenにパッチが入ったので今後のリリースでは起こらなくなるかと思われます**

残りのエラーは、`mandelbrot/mandelbrot_bg` の型定義（`mandelbrot_bg.d.ts`）が存在しないために発生しています。
回避方法はいくつか考えられますが、`mandelbrot`パッケージの方であらかじめ作成しておくのが簡単かと思います。

※ wasm-bindgenに[issue]()を上げたら、wasm-bindgen側で生成するように対応してくれそうです。


`mandelbrot`パッケージ側で以下のファイルを追記します。

```mandelbrot_bg.d.ts
import 'webassembly-js-api';

export var memory : WebAssembly.Memory;
```

また、依存パッケージを追加しておきます。

```shell-session
$ npm i @types/webassembly-js-api
```

Angular側に戻ってビルドをすると以下のようなエラーに代わりました。

```console
ERROR in ../mandelbrot/pkg/mandelbrot.js
Module not found: Error: Can't resolve './mandelbrot_bg' in '/Users/likr/src/ionic20181122demo/mandelbrot/pkg'
ERROR in ./src/app/app.component.ts
Module not found: Error: Can't resolve 'mandelbrot/mandelbrot_bg' in '/Users/likr/src/ionic20181122demo/angular-example/src/app'
```

## .wasmファイルのimport

`'mandelbrot/mandelbrot_bg'`の拡張子は.wasmとなっていて、今度は.wasm拡張子のファイルをimportできないことでエラーが発生しています。
通常のwebpack環境であれば.wasm拡張子のファイルをimportできるのですが、angular-cliの環境ではそれができなくなっています。

`@angular-builders/custom-webpack`を使って、angular-cli標準のwebpack設定を拡張しましょう。
まず必要なパッケージのインストールを行います。

```shell-session
$ npm i -D @angular-builders/custom-webpack @angular-builders/dev-server
```

拡張する設定を記述するために`extra-webpack.config.js`を以下のように作成します。

```extra-webpack.config.js
module.exports = {
  resolve: {
    extensions: ['.wasm']
  }
}
```

`@angular-builders/custom-webpack`を使用するように`angular.json`を編集します。

```diff:angular.json
@@ -11,8 +11,12 @@
       "schematics": {},
       "architect": {
         "build": {
-          "builder": "@angular-devkit/build-angular:browser",
+          "builder": "@angular-builders/custom-webpack:browser",
           "options": {
+            "customWebpackConfig": {
+              "path": "./extra-webpack.config.js"
+            },
+            "showCircularDependencies": false,
             "outputPath": "dist/angular-example",
             "index": "src/index.html",
             "main": "src/main.ts",
@@ -55,7 +59,7 @@
           }
         },
         "serve": {
-          "builder": "@angular-devkit/build-angular:dev-server",
+          "builder": "@angular-builders/dev-server:generic",
           "options": {
             "browserTarget": "angular-example:build"
           },
@@ -72,8 +76,11 @@
           }
         },
         "test": {
-          "builder": "@angular-devkit/build-angular:karma",
+          "builder": "@angular-builders/custom-webpack:karma",
           "options": {
+            "customWebpackConfig": {
+              "path": "./extra-webpack.config.js"
+            },
             "main": "src/test.ts",
             "polyfills": "src/polyfills.ts",
             "tsConfig": "src/tsconfig.spec.json",
```

これで無事に `ng build` と `ng serve` がエラーなく通るようになりました。

## プロダクションビルド

まだ `ng build --prod` したときにエラーが発生してしまいます。

```console
 70% finish module graph WasmFinalizeExportsPlugin/Users/likr/src/ionic20181122demo/angular-example/node_modules/webpack/lib/wasm/WasmFinalizeExportsPlugin.js:33
									const importedNames = ref.importedNames;
									                          ^

TypeError: Cannot read property 'importedNames' of null
    at compilation.hooks.finishModules.tap.modules (/Users/likr/src/ionic20181122demo/angular-example/node_modules/webpack/lib/wasm/WasmFinalizeExportsPlugin.js:33:36)
    at SyncHook.eval [as call] (eval at create (/Users/likr/src/ionic20181122demo/angular-example/node_modules/tapable/lib/HookCodeFactory.js:19:10), <anonymous>:16:1)
    at SyncHook.lazyCompileHook (/Users/likr/src/ionic20181122demo/angular-example/node_modules/tapable/lib/Hook.js:154:20)
    at Compilation.finish (/Users/likr/src/ionic20181122demo/angular-example/node_modules/webpack/lib/Compilation.js:1133:28)
    at hooks.make.callAsync.err (/Users/likr/src/ionic20181122demo/angular-example/node_modules/webpack/lib/Compiler.js:545:17)
    at _done (eval at create (/Users/likr/src/ionic20181122demo/angular-example/node_modules/tapable/lib/HookCodeFactory.js:32:10), <anonymous>:9:1)
    at _err1 (eval at create (/Users/likr/src/ionic20181122demo/angular-example/node_modules/tapable/lib/HookCodeFactory.js:32:10), <anonymous>:36:22)
    at _addModuleChain (/Users/likr/src/ionic20181122demo/angular-example/node_modules/webpack/lib/Compilation.js:1065:12)
    at processModuleDependencies.err (/Users/likr/src/ionic20181122demo/angular-example/node_modules/webpack/lib/Compilation.js:981:9)
    at process._tickCallback (internal/process/next_tick.js:61:11)
```

現行angular-cliでインストールされる `@angular-devkit/build-angular@0.10` が依存している `webpack@4.19.1` は、プロダクションビルドで.wasmファイルを読み込む際にエラーを起こしてしまいます。

次期リリースの`@angular-devkit/build-angular@0.11` では、webpackが修正済みバージョンとなっているので、以下のコマンドで `@angular-devkit/build-angular` を更新します。

```shell-session
$ npm i @angular-devkit/build-angular@next
```

これについては時間が解決してくれるでしょう。

## おわりに

紆余曲折ありましたが、なんとかAngularプロジェクト内からwasm-bindgenで生成したパッケージを読み込むことができました。
angular-cli標準の設定から変更しなければならない部分があるという点にはご注意ください。
ちなみにIonic v4でも同様の手順でWebAssemblyのパッケージを読み込むことができます。