概要
Dartにはdartdevcやdart2jsといったJavaScriptへのトランスパイラが含まれていますが、どちらも出力されたJavaScriptそのままではNode.jsで実行しようとしてもエラーが出て使えません。
しかしながら、Dart SassはDartコードからJavaScriptのトランスパイルによりNode.js向けのsassコンパイラの作成に成功しているようです。
この記事はDart SassがどのようにしてNode.js向けのJavaScriptトランスパイルを実現したのか自分なりに探った結果を残しておこうというものです。
使用パッケージ
Node.js型定義 node_interop
Node.jsの型定義ファイルです。全部はカバーしていないようです。どれくらいカバーされているかはGitHubのページで確認してください。
関連パッケージに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でも使われていません。
Dart用タスクランナーGrinder
Node.jsでいうところのgulpみたいなものです。Dart言語でタスクを定義できます。
今回のトランスパイルに使う分には別パッケージで用意されているタスクを利用するだけなので本格的に理解する必要はありません。
日本語解説記事
Node.js JavaScriptトランスパイル用追加スクリプトnode_preamble
トランスパイルしたJavaScriptコードをNode.jsで動かすために必要な追加のJavaScriptコードを出力するパッケージです。
このパッケージ単体ではJavaScriptコードを出力するだけでトランスパイル自体には影響しないので、トランスパイル後のJavaScriptコードにこのパッケージが出力するJavaScriptコードを追加する処理を書く必要があるのですが、その処理は別パッケージのタスクに定義済みなので、今回のトランスパイルではこのパッケージをインストールするだけでOKです。
配布パッケージ作成用Grinderタスク集cli_pkg
Dart(コマンドライン)アプリケーションをリリースするためのGrinderタスクをまとめたものです。
この中に、JavaScriptにトランスパイルしてnpmパッケージを作成するタスクがあるのでこれを利用します。npmパッケージまで作らなくてもトランスパイルするタスクだけを単独で実行することができます。
トランスパイルするタスクのコードを見る感じdart2jsの出力にいろいろコードを付け足したり書き換えたりしているようで力業感がスゴいです。
細かいオプションなんかのドキュメントは見当たらなかったのでDart Sassのタスク設定を参考に推測するしかなさそう。
ビルド手順
インストール
※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
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:
いつの間にかgrind.dartに設定するオプションにESM対応が追加されていたので追加しました(2023/3/31現在)
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);
}
{
"name": "test"
}
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ダウンロードして解凍してください。
パッケージのインストール
pubspec.yamlで設定したパッケージをインストールします。
dart pub get
トランスパイル
開発用のトランスパイルとリリース用のトランスパイルの2種類のタスクがあります。
デフォルトの設定ではリリース用でもminifyはしないようなので違いは最適化の有無と末尾の//# sourceMappingURL
の有無になるようです。
dart2jsに渡すオプションはtool/grind.dart
で開発用とリリース用で個別に設定できるようです。
いつの間にかやり方が変わっていたので修正しています(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
grind.dartでEMS対応のオプションを有効にした場合はbuild/npm/(package.jsonのnameに設定した文字列).default.cjs
も生成されているので、.cjs
のほうを実行します。
node ./build/npm/test.default.cjs
感想
正直DartのJavaScriptトランスパイラはどう考えても必要のなさそうなランタイムらしきコードを大量に出力するのでトランスパイルされたJavaScriptファイルを見ると眩暈がします。
main(){}
だけの何もしないコードをリリース用ビルドしても出力されたJavaScriptは3000行を超えファイルサイズも110KBになります。
せめて使わない部分は出力から除外するコードストリッピングをしてくれればなぁ・・・