Build Status

build

Defines the basic pieces of how a build happens and how they interact.

Builder

The business logic for code generation. Most consumers of the build package will create custom implementations of Builder.

BuildStep

The way a Builder interacts with the outside world. Defines the unit of work and allows reading/writing files and resolving Dart source code.

Resolver class

An interface into the dart analyzer to allow resolution of code that needs static analysis and/or code generation.

Differences between the build package and pub + barback.

If you currently implement transformers with package:barback for use with Dart v1 pub build and pub serve, see Upgrading from barback.

Implementing your own Builders

If you have written a barback Transformer in the past, then the Builder API should be familiar to you. The main difference is that Builders must always configure outputs based on input extensions.

The basic API looks like this:

abstract class Builder {
  /// You can only output files in `build` that are configured here. You are not
  /// required to output all of these files, but no other [Builder] is allowed
  /// to produce the same outputs.
  Map<String, List<String>> get buildExtensions;

  /// Similar to `Transformer.apply`. This is where you build and output files.
  Future build(BuildStep buildStep);
}

Here is an implementation of a Builder which just copies files to other files with the same name, but an additional extension:

/// A really simple [Builder], it just makes copies!
class CopyBuilder implements Builder {
  final String extension;

  CopyBuilder(this.extension);

  Future build(BuildStep buildStep) async {
    /// Each [buildStep] has a single input.
    var inputId = buildStep.inputId;

    /// Create a new target [AssetId] based on the old one.
    var copy = inputId.addExtension(extension);
    var contents = await buildStep.readAsString(inputId);

    /// Write out the new asset.
    ///
    /// There is no need to `await` here, the system handles waiting on these
    /// files as necessary before advancing to the next phase.
    buildStep.writeAsString(copy, contents);
  }

  /// Configure output extensions. All possible inputs match the empty input
  /// extension. For each input 1 output is created with `extension` appended to
  /// the path.
  Map<String, List<String>> get buildExtensions => {'': [extension]};
}

It should be noted that you should never touch the file system directly. Go through the buildStep#readAsString and buildStep#writeAsString methods in order to read and write assets. This is what enables the package to track all of your dependencies and do incremental rebuilds. It is also what enables your Builder to run on different environments.

Using the analyzer

If you need to do analyzer resolution, you can use the BuildStep#resolver object. This makes sure that all Builders in the system share the same analysis context, which greatly speeds up the overall system when multiple Builders are doing resolution.

Here is an example of a Builder which uses the resolve method:

class ResolvingCopyBuilder implements Builder {
  Future build(BuildStep buildStep) async {
    // Get the [LibraryElement] for the primary input.
    var entryLib = await buildStep.inputLibrary;
    // Resolves all libraries reachable from the primary input.
    var resolver = buildStep.resolver;
    // Get a [LibraryElement] for another asset.
    var libFromAsset = await resolver.libraryFor(
        new AssetId.resolve('some_import.dart', from: buildStep.inputId));
    // Or get a [LibraryElement] by name.
    var libByName = await resolver.findLibraryByName('my.library');
  }

  /// Configure outputs as well....
}

Once you have gotten a LibraryElement using one of the methods on Resolver, you are now just using the regular analyzer package to explore your app.

Sharing expensive objects across build steps

The build package includes a Resource class, which can give you an instance of an expensive object that is guaranteed to be unique across builds, but may be re-used by multiple build steps within a single build (to the extent that the implementation allows). It also gives you a way of disposing of your resource at the end of its lifecycle.

The Resource<T> constructor takes a single required argument which is a factory function that returns a FutureOr<T>. There is also a named argument dispose which is called at the end of life for the resource, with the instance that should be disposed. This returns a FutureOr<dynamic>.

So a simple example Resource would look like this:

final resource = new Resource(
  () => createMyExpensiveResource(),
  dispose: (instance) async {
    await instance.doSomeCleanup();
  });

You can get an instance of the underlying resource by using the BuildStep#fetchResource method, whose type signature looks like Future<T> fetchResource<T>(Resource<T>).

Important Note: It may be tempting to try and use a Resource instance to cache information from previous build steps (or even assets), but this should be avoided because it can break the soundness of the build, and may introduce subtle bugs for incremental builds (remember the whole build doesn't run every time!). The build package relies on the BuildStep#canRead and BuildStep#readAs* methods to track build step dependencies, so sidestepping those can and will break the dependency tracking, resulting in inconsistent and stale assets.

Features and bugs

Please file feature requests and bugs at the issue tracker.

Libraries

build