DartからNode.js用のJavaScriptを作成する

# 概要

Dartにはdartdevcやdart2jsといったJavaScriptへのトランスパイラが含まれていますが、どちらも出力されたJavaScriptそのままではNode.jsで実行しようとしてもエラーが出て使えません。
しかしながら、[Dart Sass](https://sass-lang.com/dart-sass)はDartコードからJavaScriptのトランスパイルによりNode.js向けのsassコンパイラの作成に成功しているようです。
この記事はDart SassがどのようにしてNode.js向けのJavaScriptトランスパイルを実現したのか自分なりに探った結果を残しておこうというものです。

https://github.com/sass/dart-sass

# 使用パッケージ

### Node.js型定義 node_interop

Node.jsの型定義ファイルです。全部はカバーしていないようです。どれくらいカバーされているかは[GitHub](https://github.com/pulyaevskiy/node-interop/tree/master/node_interop#status)のページで確認してください。

関連パッケージにnode_io、node_http、build_node_compilersがあるようですが、node_io、node_httpはdart:io、dart:httpをNode.jsで実装したラッパーのようです。今回は使いません。
またbuild_node_compilersはNode.js用にトランスパイルするためのビルダだそうですが、長らく更新されていない上にNull Safetyにも対応していないので使い物になりません。Dart Saasでも使われていません。

https://pub.dev/packages/node_interop

https://github.com/pulyaevskiy/node-interop/tree/master/node_interop

### Dart用タスクランナーGrinder

Node.jsでいうところのgulpみたいなものです。Dart言語でタスクを定義できます。

今回のトランスパイルに使う分には別パッケージで用意されているタスクを利用するだけなので本格的に理解する必要はありません。

https://pub.dev/packages/grinder

https://github.com/google/grinder.dart

#### 日本語解説記事

https://zenn.dev/k9i/articles/bcfa83b08e56d6

https://qiita.com/0maru/items/b134c5ee319e3cac2a99

### Node.js JavaScriptトランスパイル用追加スクリプトnode_preamble

トランスパイルしたJavaScriptコードをNode.jsで動かすために必要な追加のJavaScriptコードを出力するパッケージです。

このパッケージ単体ではJavaScriptコードを出力するだけでトランスパイル自体には影響しないので、トランスパイル後のJavaScriptコードにこのパッケージが出力するJavaScriptコードを追加する処理を書く必要があるのですが、その処理は別パッケージのタスクに定義済みなので、今回のトランスパイルではこのパッケージをインストールするだけでOKです。

https://pub.dev/packages/node_preamble

https://github.com/mbullington/node_preamble.dart

### 配布パッケージ作成用Grinderタスク集cli_pkg

Dart(コマンドライン)アプリケーションをリリースするためのGrinderタスクをまとめたものです。

この中に、JavaScriptにトランスパイルしてnpmパッケージを作成するタスクがあるのでこれを利用します。npmパッケージまで作らなくてもトランスパイルするタスクだけを単独で実行することができます。

[トランスパイルするタスクのコード](https://github.com/google/dart_cli_pkg/blob/bfea4852dbb46aac40de6dfcf3952f4d28aa9921/lib/src/npm.dart#L444)を見る感じdart2jsの出力にいろいろコードを付け足したり書き換えたりしているようで力業感がスゴいです。

細かいオプションなんかのドキュメントは見当たらなかったので[Dart Sassのタスク設定](https://github.com/sass/dart-sass/blob/main/tool/grind.dart)を参考に推測するしかなさそう。

https://pub.dev/packages/cli_pkg

https://github.com/google/dart_cli_pkg


# ビルド手順

## インストール

※Dart(Flutter)とNode.js(npm)のインストールとPATHへの登録は省略。

#### grinderのインストール

grinderのCLIであるgrindが使えるようにグローバルインストールしておきます。

```
dart pub global activate grinder
```

## ファイルの準備

Node.jsのJavaScriptトランスパイルではDartコマンドやFlutterコマンド等でプロジェクトのひな型を作る必要はありません、というか**ひな形を作ってはいけません**。

以下のような構成でファイルを手動で準備します。多分これがトランスパイルに必要な最小限の設定ファイルだと思います。
package.jsonは`npm init`で生成してもいいかもしれません。

```
プロジェクトフォルダ/
　├─ lib/
　│　　└─ main.dart
　├─ tool/
　│　　└─ grind.dart
　├─ package.json
　└─ pubspec.yaml
```

```pubspec.yaml
name: nodetest
description: An absolute bare-bones web app.
version: 1.0.0
# homepage: https://www.example.com

environment:
  sdk: '>=2.18.1 <3.0.0'

dependencies:
  js:
  node_interop:

dev_dependencies:
  cli_pkg:
  grinder:
  node_preamble:
```

:::note info
いつの間にかgrind.dartに設定するオプションにESM対応が追加されていたので追加しました(2023/3/31現在)
:::
```tool/grind.dart
import 'package:cli_pkg/cli_pkg.dart' as pkg;
import 'package:grinder/grinder.dart';

void main(List<String> args) {
  pkg.jsModuleMainLibrary.value = "lib/main.dart";
  //pkg.jsEsmExports.value = {};  //ESM対応にする場合
  pkg.addAllTasks();
  grind(args);
}

```

```package.json
{
  "name": "test"
}
```

Dartコードはとりあえずこんな感じで。

```lib/main.dart
import 'dart:js';
import 'package:js/js.dart';
import 'package:js/js_util.dart';
import 'package:node_interop/node.dart';

void main() {
  print("Hello world!!.");
}
```

手動での準備が面倒な場合は一応上記ファイルをgithubにまとめましたので、cloneするかzipダウンロードして解凍してください。

https://github.com/uniras/dart_node_minimum

## パッケージのインストール

pubspec.yamlで設定したパッケージをインストールします。

```
dart pub get
```

## トランスパイル

開発用のトランスパイルとリリース用のトランスパイルの2種類のタスクがあります。
デフォルトの設定ではリリース用でもminifyはしないようなので違いは最適化の有無と末尾の`//# sourceMappingURL`の有無になるようです。
dart2jsに渡すオプションは`tool/grind.dart`で開発用とリリース用で個別に設定できるようです。

:::note info
いつの間にかやり方が変わっていたので修正しています(2023/3/31現在)
:::

開発用
```
grind pkg-npm-dev
```

リリース用
```
grind pkg-npm-release
```

## 実行

トランスパイルタスクの実行に成功すると`build/npm/(package.jsonのnameに設定した文字列).dart.js`や`build/npm/(package.jsonのnameに設定した文字列).default.js`等が生成されるので、`(package.jsonのnameに設定した文字列).default.js`の方をnodeコマンドで実行します。

```
node ./build/npm/test.default.js
```

:::note warn
grind.dartでEMS対応のオプションを有効にした場合は`build/npm/(package.jsonのnameに設定した文字列).default.cjs`も生成されているので、`.cjs`のほうを実行します。
```
node ./build/npm/test.default.cjs
```
:::




## 感想

正直DartのJavaScriptトランスパイラはどう考えても必要のなさそうなランタイムらしきコードを大量に出力するのでトランスパイルされたJavaScriptファイルを見ると眩暈がします。
`main(){}`だけの何もしないコードをリリース用ビルドしても出力されたJavaScriptは3000行を超えファイルサイズも110KBになります。
せめて使わない部分は出力から除外するコードストリッピングをしてくれればなぁ・・・