build_runnerから実行できるミニマムなBuilder

Dartを使った開発では自動生成されたコードを利用することが多い。freezedjson_serializableなどのパッケージでは、以下のようにbuild_runnerを使ってコードを自動生成する。

$ dart run build_runner build

build_runnerはbuild.yamlによって定義されたbuilderを使ってビルドを行い、Dartのコードを生成する。ビルドに関わるいくつかのパッケージがDart開発チームによって提供されている。

  • build: コード生成を実際に行うBuilderのためのインターフェイスを提供する
  • build_runner: ビルドを実行するCLIを提供する
  • build_config: build.yamlをパースすることで得られるビルド設定を提供する

なので、buildが提供するBuilderインターフェイスを実装すれば、build_runnerからコード生成を行うパッケージが自分でも作れるようになる。

ミニマムなBuilder

最小構成のBuilderを実装するため、まずはパッケージを作る。

$ dart create minimum_builder
$ cd minimum_builder

dart createで作られたダミーのコードを消す。

$ code . # remove dummy codes

buildなどの依存するパッケージをインストールする。buildのみが必要な依存関係で、build_runnerとbuild_configは開発のみに必要な依存関係となる。

$ dart pub add build dev:build_runner dev:build_config

build.yaml

build.yamlにビルドのための設定を書く。

$ code build.yaml

それぞれの設定項目の詳細については公式ドキュメントに書かれている。

targets:
  $default:
    builders:
      minimum_builder|todo_builder:
        generate_for:
          - example/*
        enabled: True

builders:
  todo_builder:
    import: 'package:minimum_builder/minimum_builder.dart'
    builder_factories: ['todoBuilder']
    build_extensions:
      .dart:
        - .g.dart
    build_to: source
    auto_apply: dependents
  • generate_for: マッチするファイルがビルダーの入力として渡される。
  • builder_factories: BuilderOptionsを受け取ってBuilderを返す関数を指定する。ここで自分が実装するビルダーを返すようにする。
  • build_extensions: ビルドの入力となるファイルの拡張子とその出力のマップ。

builderの実装

Builderインターフェイスを実装するクラスにコードの生成処理を実装する。

$ code lib/src/todo_builder.dart
import 'dart:async';

import 'package:build/build.dart';

class TodoBuilder implements Builder {
  @override
  FutureOr<void> build(BuildStep buildStep) async {
    final newInputId = buildStep.inputId.changeExtension('.g.dart');
    await buildStep.writeAsString(newInputId, '// TODO: implement');
  }

  @override
  Map<String, List<String>> get buildExtensions => {
      '.dart': ['.g.dart'],
    };
}

Builderインターフェイスが実装を要求するメソッドが2つある。

  • build(): 引数に渡されるBuildStepを利用して、コード生成を行う。inputIdが入力となるファイルを表している。BuildStep.writeAsString()を使うことでファイルに生成したコードを書き込める。ここではコメントを書いているだけだけど、入力したファイルを使って処理した結果を書き込むことになる。
  • buildExtensions: build.yamlでも指定したマップをここでも指定する必要がありそう。

build_factoriesの実装

build.yamlのbuild_factoriesで指定した関数で実装したビルダーを返すようにする。

$ code lib/minimum_builder.dart
import 'package:build/build.dart';
import 'src/todo_builder.dart';

Builder todoBuilder(BuilderOptions options) => TodoBuilder();

試してみる

build.yamlでgenerate_forexample/*と指定したので、ダミーのコードを置いて試してみる。

$ mkdir example
$ touch example/example.dart
$ dart run build_runner run

example/example.g.dartが生成されていることがわかる。

// TODO: implement

まとめ

ほぼなにもしないビルダーを作ってみて、build_runnerによって実行可能なビルダーを実装する方法を理解できた。あとは、Builderを実装するクラスで入力として受け取ったファイルを元にコードを生成する実装を書いていけば、自分オリジナルのビルダーを実装できるだろう。

今回は触れなかったけど、source_genという別のパッケージを使うことでビルダーをより簡単に実装できるようになるため、機会があれば別の記事に使い方をまとめてみたいと思う。