From 3b525b483cbdeb3240fda46e83472b0116793c07 Mon Sep 17 00:00:00 2001
From: "nweiz@google.com" <nweiz@google.com>
Date: Thu, 12 Sep 2013 19:40:21 +0000
Subject: [PATCH] Support passing configuration data to a transformer plugin.

This also requires that transformer plugins define an [asPlugin] named
constructor in order to be loaded as plugins.

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//23522029

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@27444 260f80e4-7a28-3924-810f-c04153c831b5
---
 lib/src/barback.dart                          | 32 ++++++++
 lib/src/barback/load_all_transformers.dart    | 43 +++++-----
 lib/src/barback/load_transformers.dart        | 78 +++++++++++++------
 lib/src/pubspec.dart                          | 32 ++++++--
 test/pubspec_test.dart                        |  9 +++
 ...figuration_defaults_to_empty_map_test.dart | 59 ++++++++++++++
 ...with_reserved_transformer_config_test.dart | 36 +++++++++
 ...ransformer_when_config_is_passed_test.dart | 35 +++++++++
 ...s_configuration_to_a_transformer_test.dart | 61 +++++++++++++++
 ...rints_a_transform_error_in_apply_test.dart |  2 +-
 ...ints_a_transform_interface_error_test.dart |  2 +-
 test/serve/utils.dart                         |  4 +-
 ...tiates_configurable_transformers_test.dart | 76 ++++++++++++++++++
 13 files changed, 415 insertions(+), 54 deletions(-)
 create mode 100644 test/serve/configuration_defaults_to_empty_map_test.dart
 create mode 100644 test/serve/fails_to_load_a_pubspec_with_reserved_transformer_config_test.dart
 create mode 100644 test/serve/fails_to_load_an_unconfigurable_transformer_when_config_is_passed_test.dart
 create mode 100644 test/serve/passes_configuration_to_a_transformer_test.dart
 create mode 100644 test/serve/with_configuration_only_instantiates_configurable_transformers_test.dart

diff --git a/lib/src/barback.dart b/lib/src/barback.dart
index 001b0739..3c14a16f 100644
--- a/lib/src/barback.dart
+++ b/lib/src/barback.dart
@@ -15,6 +15,38 @@ import 'barback/server.dart';
 import 'barback/watch_sources.dart';
 import 'utils.dart';
 
+/// An identifier for a transformer and the configuration that will be passed to
+/// it.
+///
+/// It's possible that [asset] defines multiple transformers. If so,
+/// [configuration] will be passed to all of them.
+class TransformerId {
+  /// The asset containing the transformer.
+  final AssetId asset;
+
+  /// The configuration to pass to the transformer.
+  ///
+  /// This will be null if no configuration was provided.
+  final Map configuration;
+
+  TransformerId(this.asset, this.configuration) {
+    if (configuration == null) return;
+    for (var reserved in ['include', 'exclude']) {
+      if (!configuration.containsKey(reserved)) continue;
+      throw new FormatException('Configuration for transformer '
+          '${idToLibraryIdentifier(asset)} may not include reserved key '
+          '"$reserved".');
+    }
+  }
+
+  // TODO(nweiz): support deep equality on [configuration] as well.
+  bool operator==(other) => other is TransformerId &&
+      other.asset == asset &&
+      other.configuration == configuration;
+
+  int get hashCode => asset.hashCode ^ configuration.hashCode;
+}
+
 /// Creates a [BarbackServer] serving on [host] and [port].
 ///
 /// This transforms and serves all library and asset files in all packages in
diff --git a/lib/src/barback/load_all_transformers.dart b/lib/src/barback/load_all_transformers.dart
index 2f9414b6..e954da83 100644
--- a/lib/src/barback/load_all_transformers.dart
+++ b/lib/src/barback/load_all_transformers.dart
@@ -86,7 +86,7 @@ Future loadAllTransformers(BarbackServer server, PackageGraph graph) {
       var transformers = [[rewrite]];
       return Future.forEach(graph.packages[package].pubspec.transformers,
           (phase) {
-        return Future.wait(phase.where((id) => id.package == package)
+        return Future.wait(phase.where((id) => id.asset.package == package)
             .map(loader.load)).then((_) {
           transformers.add(unionAll(phase.map(
               (id) => loader.transformersFor(id))));
@@ -148,7 +148,7 @@ Map<String, Set<String>> _computeOrderingDeps(PackageGraph graph) {
 /// Returns the set of transformer dependencies for [package].
 Set<String> _transformerDeps(PackageGraph graph, String package) =>
   unionAll(graph.packages[package].pubspec.transformers)
-      .map((id) => id.package).toSet();
+      .map((id) => id.asset.package).toSet();
 
 /// Returns an [ApplicationException] describing an ordering dependency cycle
 /// detected in [graph].
@@ -167,7 +167,7 @@ ApplicationException _cycleError(PackageGraph graph, String dependee,
   return new ApplicationException("Transformer cycle detected:\n" +
       pairs(path).map((pair) {
     var transformers = unionAll(graph.packages[pair.first].pubspec.transformers)
-        .where((id) => id.package == pair.last)
+        .where((id) => id.asset.package == pair.last)
         .map(idToLibraryIdentifier).toList();
     if (transformers.isEmpty) {
       return "  ${pair.first} depends on ${pair.last}";
@@ -177,15 +177,16 @@ ApplicationException _cycleError(PackageGraph graph, String dependee,
   }).join("\n"));
 }
 
-/// Returns a map from each package name in [graph] to the asset ids of all
-/// transformers exposed by that package and used by other packages.
-Map<String, Set<AssetId>> _computePackageTransformers(PackageGraph graph) {
+/// Returns a map from each package name in [graph] to the transformer ids of
+/// all transformers exposed by that package and used by other packages.
+Map<String, Set<TransformerId>> _computePackageTransformers(
+    PackageGraph graph) {
   var packageTransformers = listToMap(graph.packages.values,
-      (package) => package.name, (_) => new Set<AssetId>());
+      (package) => package.name, (_) => new Set<TransformerId>());
   for (var package in graph.packages.values) {
     for (var phase in package.pubspec.transformers) {
       for (var id in phase) {
-        packageTransformers[id.package].add(id);
+        packageTransformers[id.asset.package].add(id);
       }
     }
   }
@@ -196,11 +197,11 @@ Map<String, Set<AssetId>> _computePackageTransformers(PackageGraph graph) {
 class _TransformerLoader {
   final BarbackServer _server;
 
-  /// The loaded transformers defined in the library identified by each asset
-  /// id.
-  final _transformers = new Map<AssetId, Set<Transformer>>();
+  /// The loaded transformers defined in the library identified by each
+  /// transformer id.
+  final _transformers = new Map<TransformerId, Set<Transformer>>();
 
-  /// The packages that use each transformer id.
+  /// The packages that use each transformer asset id.
   ///
   /// Used for error reporting.
   final _transformerUsers = new Map<AssetId, Set<String>>();
@@ -208,29 +209,35 @@ class _TransformerLoader {
   _TransformerLoader(this._server, PackageGraph graph) {
     for (var package in graph.packages.values) {
       for (var id in unionAll(package.pubspec.transformers)) {
-        _transformerUsers.putIfAbsent(id, () => new Set<String>())
+        _transformerUsers.putIfAbsent(id.asset, () => new Set<String>())
             .add(package.name);
       }
     }
   }
 
-  /// Loads the transformer(s) defined in the asset [id].
+  /// Loads the transformer(s) defined in [id].
   ///
   /// Once the returned future completes, these transformers can be retrieved
   /// using [transformersFor]. If [id] doesn't define any transformers, this
   /// will complete to an error.
-  Future load(AssetId id) {
+  Future load(TransformerId id) {
     if (_transformers.containsKey(id)) return new Future.value();
 
+    // TODO(nweiz): load multiple instances of the same transformer from the
+    // same isolate rather than spinning up a separate isolate for each one.
     return loadTransformers(_server, id).then((transformers) {
       if (!transformers.isEmpty) {
         _transformers[id] = transformers;
         return;
       }
 
+      var message = "No transformers";
+      if (id.configuration != null) {
+        message += " that accept configuration";
+      }
       throw new ApplicationException(
-          "No transformers were defined in ${idToPackageUri(id)},\n"
-          "required by ${ordered(_transformerUsers[id]).join(', ')}.");
+          "$message were defined in ${idToPackageUri(id.asset)},\n"
+          "required by ${ordered(_transformerUsers[id.asset]).join(', ')}.");
     });
   }
 
@@ -238,7 +245,7 @@ class _TransformerLoader {
   ///
   /// It's an error to call this before [load] is called with [id] and the
   /// future it returns has completed.
-  Set<Transformers> transformersFor(AssetId id) {
+  Set<Transformers> transformersFor(TransformerId id) {
     assert(_transformers.containsKey(id));
     return _transformers[id];
   }
diff --git a/lib/src/barback/load_transformers.dart b/lib/src/barback/load_transformers.dart
index 4d736c8f..b2c8464d 100644
--- a/lib/src/barback/load_transformers.dart
+++ b/lib/src/barback/load_transformers.dart
@@ -5,6 +5,7 @@
 library pub.load_transformers;
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:isolate';
 
 import 'package:barback/barback.dart';
@@ -21,15 +22,19 @@ import '../utils.dart';
 const _TRANSFORMER_ISOLATE = """
 import 'dart:async';
 import 'dart:isolate';
+import 'dart:convert';
 import 'dart:mirrors';
 
 import 'http://localhost:<<PORT>>/packages/barback/barback.dart';
 
 /// Sets up the initial communication with the host isolate.
 void main() {
-  port.receive((uri, replyTo) {
+  port.receive((args, replyTo) {
     _sendFuture(replyTo, new Future.sync(() {
-      return initialize(Uri.parse(uri)).map(_serializeTransformer).toList();
+      var library = Uri.parse(args['library']);
+      var configuration = JSON.decode(args['configuration']);
+      return initialize(library, configuration).
+          map(_serializeTransformer).toList();
     }));
   });
 }
@@ -37,8 +42,8 @@ void main() {
 /// Loads all the transformers defined in [uri] and adds them to [transformers].
 ///
 /// We then load the library, find any Transformer subclasses in it, instantiate
-/// them, and return them.
-Iterable<Transformer> initialize(Uri uri) {
+/// them (with [configuration] if it's non-null), and return them.
+Iterable<Transformer> initialize(Uri uri, Map configuration) {
   var mirrors = currentMirrorSystem();
   // TODO(nweiz): look this up by name once issue 5897 is fixed.
   var transformerUri = Uri.parse(
@@ -46,17 +51,30 @@ Iterable<Transformer> initialize(Uri uri) {
   var transformerClass = mirrors.libraries[transformerUri]
       .classes[const Symbol('Transformer')];
 
-  return mirrors.libraries[uri].classes.values.where((classMirror) {
-    if (classMirror.isPrivate) return false;
-    if (isAbstract(classMirror)) return false;
-    if (!classIsA(classMirror, transformerClass)) return false;
-    var constructor = classMirror.constructors[classMirror.simpleName];
-    if (constructor == null) return false;
-    if (!constructor.parameters.isEmpty) return false;
-    return true;
-  }).map((classMirror) {
-    return classMirror.newInstance(const Symbol(''), []).reflectee;
-  });
+  // TODO(nweiz): if no valid transformers are found, throw an error message
+  // describing candidates and why they were rejected.
+  return mirrors.libraries[uri].classes.values.map((classMirror) {
+    if (classMirror.isPrivate) return null;
+    if (isAbstract(classMirror)) return null;
+    if (!classIsA(classMirror, transformerClass)) return null;
+
+    var constructor = getConstructor(classMirror, 'asPlugin');
+    if (constructor == null) return null;
+    if (constructor.parameters.isEmpty) {
+      if (configuration != null) return null;
+      return classMirror.newInstance(const Symbol('asPlugin'), []).reflectee;
+    }
+    if (constructor.parameters.length != 1) return null;
+
+    // If the constructor expects configuration and none was passed, it defaults
+    // to an empty map.
+    if (configuration == null) configuration = {};
+
+    // TODO(nweiz): if the constructor accepts named parameters, automatically
+    // destructure the configuration map.
+    return classMirror.newInstance(const Symbol('asPlugin'), [configuration])
+        .reflectee;
+  }).where((classMirror) => classMirror != null);
 }
 
 /// A wrapper for a [Transform] that's in the host isolate.
@@ -107,6 +125,13 @@ ClassMirror get objectMirror {
 }
 ClassMirror _objectMirror;
 
+// TODO(nweiz): clean this up when issue 13248 is fixed.
+MethodMirror getConstructor(ClassMirror classMirror, String constructor) {
+  var name = new Symbol("\${MirrorSystem.getName(classMirror.simpleName)}"
+      ".\$constructor");
+  return classMirror.constructors[name];
+}
+
 // TODO(nweiz): get rid of this when issue 12439 is fixed.
 /// Returns whether or not [mirror] is a subtype of [superclass].
 ///
@@ -256,23 +281,28 @@ String getErrorMessage(error) {
 }
 """;
 
-/// Load and return all transformers from the library identified by [library].
+/// Load and return all transformers from the library identified by [id].
 ///
-/// [server] is used to serve [library] and any Dart files it imports.
+/// [server] is used to serve any Dart files needed to load the transformer.
 Future<Set<Transformer>> loadTransformers(BarbackServer server,
-    AssetId library) {
-  var path = library.path.replaceFirst('lib/', '');
+    TransformerId id) {
+  var path = id.asset.path.replaceFirst('lib/', '');
   // TODO(nweiz): load from a "package:" URI when issue 12474 is fixed.
-  var uri = 'http://localhost:${server.port}/packages/${library.package}/$path';
+  var uri = 'http://localhost:${server.port}/packages/${id.asset.package}/'
+      '$path';
   var code = 'import "$uri";' +
       _TRANSFORMER_ISOLATE.replaceAll('<<PORT>>', server.port.toString());
-  log.fine("Loading transformers from $library");
+  log.fine("Loading transformers from ${id.asset}");
   return dart.runInIsolate(code).then((sendPort) {
-    return _receiveFuture(sendPort.call(uri)).then((transformers) {
+    return _receiveFuture(sendPort.call({
+      'library': uri,
+      // TODO(nweiz): support non-JSON-encodable configuration maps.
+      'configuration': JSON.encode(id.configuration)
+    })).then((transformers) {
       transformers = transformers
           .map((transformer) => new _ForeignTransformer(transformer))
           .toSet();
-      log.fine("Transformers from $library: $transformers");
+      log.fine("Transformers from ${id.asset}: $transformers");
       return transformers;
     });
   }).catchError((error) {
@@ -287,7 +317,7 @@ Future<Set<Transformer>> loadTransformers(BarbackServer server,
     // If there was an IsolateSpawnException and the import that actually failed
     // was the one we were loading transformers from, throw an application
     // exception with a more user-friendly message.
-    fail('Transformer library "package:${library.package}/$path" not found.');
+    fail('Transformer library "package:${id.asset.package}/$path" not found.');
   });
 }
 
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index d5b162ed..0813315f 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -30,9 +30,8 @@ class Pubspec {
   /// The packages this package depends on when it is the root package.
   final List<PackageDep> devDependencies;
 
-  /// The ids of the libraries containing the transformers to use for this
-  /// package.
-  final List<Set<AssetId>> transformers;
+  /// The ids of the transformers to use for this package.
+  final List<Set<TransformerId>> transformers;
 
   /// The environment-related metadata.
   final PubspecEnvironment environment;
@@ -74,7 +73,7 @@ class Pubspec {
       dependencies = <PackageDep>[],
       devDependencies = <PackageDep>[],
       environment = new PubspecEnvironment(),
-      transformers = <Set<AssetId>>[],
+      transformers = <Set<TransformerId>>[],
       fields = {};
 
   /// Whether or not the pubspec has no contents.
@@ -190,19 +189,36 @@ Pubspec _parseMap(String filePath, Map map, SourceRegistry sources) {
     transformers = transformers.map((phase) {
       if (phase is! List) phase = [phase];
       return phase.map((transformer) {
-        if (transformer is! String) {
+        if (transformer is! String && transformer is! Map) {
           throw new FormatException(
-              'Transformer "$transformer" must be a string.');
+              'Transformer "$transformer" must be a string or map.');
+        }
+
+        var id;
+        var configuration;
+        if (transformer is String) {
+          id = libraryIdentifierToId(transformer);
+        } else {
+          if (transformer.length != 1) {
+            throw new FormatException('Transformer map "$transformer" must '
+                'have a single key: the library identifier.');
+          }
+
+          id = libraryIdentifierToId(transformer.keys.single);
+          configuration = transformer.values.single;
+          if (configuration is! Map) {
+            throw new FormatException('Configuration for transformer "$id" '
+                'must be a map, but was "$configuration".');
+          }
         }
 
-        var id = libraryIdentifierToId(transformer);
         if (id.package != name &&
             !dependencies.any((ref) => ref.name == id.package)) {
           throw new FormatException('Could not find package for transformer '
               '"$transformer".');
         }
 
-        return id;
+        return new TransformerId(id, configuration);
       }).toSet();
     }).toList();
   } else {
diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart
index 092611d3..44044619 100644
--- a/test/pubspec_test.dart
+++ b/test/pubspec_test.dart
@@ -214,6 +214,15 @@ version: not version
       expectFormatError('{author: abe, authors: ted}');
     });
 
+    test("throws if a transformer isn't a string or map", () {
+      expectFormatError('{transformers: 12}');
+      expectFormatError('{transformers: [12]}');
+    });
+
+    test("throws if a transformer's configuration isn't a map", () {
+      expectFormatError('{transformers: {pkg: 12}}');
+    });
+
     test("allows comment-only files", () {
       var pubspec = new Pubspec.parse(null, '''
 # No external dependencies yet
diff --git a/test/serve/configuration_defaults_to_empty_map_test.dart b/test/serve/configuration_defaults_to_empty_map_test.dart
new file mode 100644
index 00000000..bad8e520
--- /dev/null
+++ b/test/serve/configuration_defaults_to_empty_map_test.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'dart:convert';
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+import 'utils.dart';
+
+final transformer = """
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:barback/barback.dart';
+
+class ConfigTransformer extends Transformer {
+  final Map configuration;
+
+  ConfigTransformer.asPlugin(this.configuration);
+
+  String get allowedExtensions => '.txt';
+
+  Future apply(Transform transform) {
+    return transform.primaryInput.readAsString().then((contents) {
+      var id = transform.primaryInput.id.changeExtension(".json");
+      transform.addOutput(new Asset.fromString(id, JSON.encode(configuration)));
+    });
+  }
+}
+""";
+
+main() {
+  initConfig();
+  integration("configuration defaults to an empty map", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": ["myapp/src/transformer"]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", transformer)
+      ])]),
+      d.dir("web", [
+        d.file("foo.txt", "foo")
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    var server = startPubServe();
+    requestShouldSucceed("foo.json", JSON.encode({}));
+    endPubServe();
+  });
+}
diff --git a/test/serve/fails_to_load_a_pubspec_with_reserved_transformer_config_test.dart b/test/serve/fails_to_load_a_pubspec_with_reserved_transformer_config_test.dart
new file mode 100644
index 00000000..5ba03d83
--- /dev/null
+++ b/test/serve/fails_to_load_a_pubspec_with_reserved_transformer_config_test.dart
@@ -0,0 +1,36 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+import 'utils.dart';
+
+main() {
+  initConfig();
+
+  integration("fails to load a pubspec with reserved transformer config", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [{"myapp/src/transformer": {'include': 'something'}}]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", REWRITE_TRANSFORMER)
+      ])])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    var pub = startPub(args: ['serve', '--port=0']);
+    expect(pub.nextErrLine(), completion(startsWith('Could not parse ')));
+    expect(pub.nextErrLine(), completion(equals('Configuration for '
+        'transformer myapp/src/transformer may not include reserved key '
+        '"include".')));
+    pub.shouldExit(1);
+  });
+}
diff --git a/test/serve/fails_to_load_an_unconfigurable_transformer_when_config_is_passed_test.dart b/test/serve/fails_to_load_an_unconfigurable_transformer_when_config_is_passed_test.dart
new file mode 100644
index 00000000..04036416
--- /dev/null
+++ b/test/serve/fails_to_load_an_unconfigurable_transformer_when_config_is_passed_test.dart
@@ -0,0 +1,35 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+import 'utils.dart';
+
+main() {
+  initConfig();
+
+  integration("fails to load an unconfigurable transformer when config is "
+      "passed", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [{"myapp/src/transformer": {'foo': 'bar'}}]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", REWRITE_TRANSFORMER)
+      ])])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    var pub = startPub(args: ['serve', '--port=0']);
+    expect(pub.nextErrLine(), completion(startsWith('No transformers that '
+        'accept configuration were defined in ')));
+    pub.shouldExit(1);
+  });
+}
diff --git a/test/serve/passes_configuration_to_a_transformer_test.dart b/test/serve/passes_configuration_to_a_transformer_test.dart
new file mode 100644
index 00000000..e3f68e10
--- /dev/null
+++ b/test/serve/passes_configuration_to_a_transformer_test.dart
@@ -0,0 +1,61 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'dart:convert';
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+import 'utils.dart';
+
+final transformer = """
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:barback/barback.dart';
+
+class ConfigTransformer extends Transformer {
+  final Map configuration;
+
+  ConfigTransformer.asPlugin(this.configuration);
+
+  String get allowedExtensions => '.txt';
+
+  Future apply(Transform transform) {
+    return transform.primaryInput.readAsString().then((contents) {
+      var id = transform.primaryInput.id.changeExtension(".json");
+      transform.addOutput(new Asset.fromString(id, JSON.encode(configuration)));
+    });
+  }
+}
+""";
+
+main() {
+  initConfig();
+  integration("passes configuration to a transformer", () {
+    var configuration = {"param": ["list", "of", "values"]};
+
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [{"myapp/src/transformer": configuration}]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", transformer)
+      ])]),
+      d.dir("web", [
+        d.file("foo.txt", "foo")
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    var server = startPubServe();
+    requestShouldSucceed("foo.json", JSON.encode(configuration));
+    endPubServe();
+  });
+}
diff --git a/test/serve/prints_a_transform_error_in_apply_test.dart b/test/serve/prints_a_transform_error_in_apply_test.dart
index 93b24a90..2d90ee84 100644
--- a/test/serve/prints_a_transform_error_in_apply_test.dart
+++ b/test/serve/prints_a_transform_error_in_apply_test.dart
@@ -16,7 +16,7 @@ import 'dart:async';
 import 'package:barback/barback.dart';
 
 class RewriteTransformer extends Transformer {
-  RewriteTransformer();
+  RewriteTransformer.asPlugin();
 
   String get allowedExtensions => '.txt';
 
diff --git a/test/serve/prints_a_transform_interface_error_test.dart b/test/serve/prints_a_transform_interface_error_test.dart
index 47636b48..9e35a461 100644
--- a/test/serve/prints_a_transform_interface_error_test.dart
+++ b/test/serve/prints_a_transform_interface_error_test.dart
@@ -16,7 +16,7 @@ import 'dart:async';
 import 'package:barback/barback.dart';
 
 class RewriteTransformer extends Transformer {
-  RewriteTransformer();
+  RewriteTransformer.asPlugin();
 
   String get allowedExtensions => '.txt';
 }
diff --git a/test/serve/utils.dart b/test/serve/utils.dart
index 7025d9b9..46ace9de 100644
--- a/test/serve/utils.dart
+++ b/test/serve/utils.dart
@@ -26,7 +26,7 @@ import 'dart:async';
 import 'package:barback/barback.dart';
 
 class RewriteTransformer extends Transformer {
-  RewriteTransformer();
+  RewriteTransformer.asPlugin();
 
   String get allowedExtensions => '.txt';
 
@@ -69,7 +69,7 @@ const TOKEN = "$id";
 final _tokenRegExp = new RegExp(r'^const TOKEN = "(.*?)";\$', multiLine: true);
 
 class DartTransformer extends Transformer {
-  DartTransformer();
+  DartTransformer.asPlugin();
 
   String get allowedExtensions => '.dart';
 
diff --git a/test/serve/with_configuration_only_instantiates_configurable_transformers_test.dart b/test/serve/with_configuration_only_instantiates_configurable_transformers_test.dart
new file mode 100644
index 00000000..6c010bf5
--- /dev/null
+++ b/test/serve/with_configuration_only_instantiates_configurable_transformers_test.dart
@@ -0,0 +1,76 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'dart:convert';
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+import 'utils.dart';
+
+final transformer = """
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:barback/barback.dart';
+
+class ConfigTransformer extends Transformer {
+  final Map configuration;
+
+  ConfigTransformer.asPlugin(this.configuration);
+
+  String get allowedExtensions => '.txt';
+
+  Future apply(Transform transform) {
+    return transform.primaryInput.readAsString().then((contents) {
+      var id = transform.primaryInput.id.changeExtension(".json");
+      transform.addOutput(new Asset.fromString(id, JSON.encode(configuration)));
+    });
+  }
+}
+
+class RewriteTransformer extends Transformer {
+  RewriteTransformer.asPlugin();
+
+  String get allowedExtensions => '.txt';
+
+  Future apply(Transform transform) {
+    return transform.primaryInput.readAsString().then((contents) {
+      var id = transform.primaryInput.id.changeExtension(".out");
+      transform.addOutput(new Asset.fromString(id, "\$contents.out"));
+    });
+  }
+}
+""";
+
+main() {
+  initConfig();
+  integration("with configuration, only instantiates configurable transformers",
+      () {
+    var configuration = {"param": ["list", "of", "values"]};
+
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [{"myapp/src/transformer": configuration}]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", transformer)
+      ])]),
+      d.dir("web", [
+        d.file("foo.txt", "foo")
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    var server = startPubServe();
+    requestShouldSucceed("foo.json", JSON.encode(configuration));
+    requestShould404("foo.out");
+    endPubServe();
+  });
+}
-- 
GitLab