blob: 93c523cd42c59f95569f9f9046613a7aff7ccad3 [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS 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.load_all_transformers;
import 'dart:async';
import 'package:barback/barback.dart';
import 'dart2js_transformer.dart';
import 'load_transformers.dart';
import 'rewrite_import_transformer.dart';
import 'server.dart';
import '../barback.dart';
import '../package_graph.dart';
import '../utils.dart';
/// Loads all transformers depended on by packages in [graph].
///
/// This uses [server] to serve the Dart files from which transformers are
/// loaded, then adds the transformers to `server.barback`.
///
/// Any [builtInTransformers] that are provided will automatically be added to
/// the end of the root package's cascade.
Future loadAllTransformers(BarbackServer server, PackageGraph graph,
BarbackMode mode, Iterable<Transformer> builtInTransformers) {
// In order to determine in what order we should load transformers, we need to
// know which transformers depend on which others. This is different than
// normal package dependencies. Let's begin with some terminology:
//
// * If package A is transformed by package B, we say A has a "transformer
// dependency" on B.
// * If A imports B we say A has a "package dependency" on B.
// * If A needs B's transformers to be loaded in order to load A's
// transformers, we say A has an "ordering dependency" on B.
//
// In particular, an ordering dependency is defined as follows:
//
// * If A has a transformer dependency on B, A also has an ordering dependency
// on B.
// * If A has a transitive package dependency on B and B has a transformer
// dependency on C, A has an ordering dependency on C.
//
// The order that transformers are loaded is determined by each package's
// ordering dependencies. We treat the packages as a directed acyclic[1] graph
// where each package is a node and the ordering dependencies are the edges
// (that is, the packages form a partially ordered set). We then load[2]
// packages in a topological sort order of this graph.
//
// [1] TODO(nweiz): support cycles in some cases.
//
// [2] We use "loading a package" as a shorthand for loading that package's
// transformers.
// Add a rewrite transformer for each package, so that we can resolve
// "package:" imports while loading transformers.
var rewrite = new RewriteImportTransformer();
for (var package in graph.packages.values) {
server.barback.updateTransformers(package.name, [[rewrite]]);
}
var orderingDeps = _computeOrderingDeps(graph);
var packageTransformers = _computePackageTransformers(graph);
var loader = new _TransformerLoader(server, mode, graph);
// The packages on which no packages have ordering dependencies -- that is,
// the packages that don't need to be loaded before any other packages. These
// packages will be loaded last, since all of their ordering dependencies need
// to be loaded before they're loaded. However, they'll be traversed by
// [loadPackage] first.
var rootPackages = graph.packages.keys.toSet()
.difference(unionAll(orderingDeps.values));
// The Futures for packages that have been loaded or are being actively loaded
// by [loadPackage]. Once one of these Futures is complete, the transformers
// for that package will all be available from [loader].
var loadingPackages = new Map<String, Future>();
// A helper function that loads all the transformers that [package] uses, then
// all the transformers that [package] defines.
Future loadPackage(String package) {
if (loadingPackages.containsKey(package)) return loadingPackages[package];
// First, load each package upon which [package] has an ordering dependency.
var future = Future.wait(orderingDeps[package].map(loadPackage)).then((_) {
// Go through the transformers used by [package] phase-by-phase. If any
// phase uses a transformer defined in [package] itself, that transform
// should be loaded after running all previous phases.
var transformers = [[rewrite]];
return Future.forEach(graph.packages[package].pubspec.transformers,
(phase) {
return Future.wait(phase.where((id) => id.package == package)
.map(loader.load)).then((_) {
transformers.add(unionAll(phase.map(
(id) => loader.transformersFor(id))));
server.barback.updateTransformers(package, transformers);
});
}).then((_) {
// Now that we've applied all the transformers used by [package] via
// [Barback.updateTransformers], we load any transformers defined in
// [package] but used elsewhere.
return Future.wait(packageTransformers[package].map(loader.load));
});
});
loadingPackages[package] = future;
return future;
}
return Future.wait(rootPackages.map(loadPackage)).then((_) {
/// Reset the transformers for each package to get rid of [rewrite], which
/// is no longer needed.
for (var package in graph.packages.values) {
var phases = package.pubspec.transformers.map((phase) {
return unionAll(phase.map((id) => loader.transformersFor(id)));
}).toList();
// The built-in transformers are for dart2js and forwarding assets around
// dart2js, and those only apply to the entrypoints in the root package.
if (package.name == graph.entrypoint.root.name &&
builtInTransformers != null &&
builtInTransformers.isNotEmpty) {
phases.add(builtInTransformers);
}
server.barback.updateTransformers(package.name, phases);
}
});
}
/// Computes and returns the graph of ordering dependencies for [graph].
///
/// This graph is in the form of a map whose keys are packages and whose values
/// are those packages' ordering dependencies.
Map<String, Set<String>> _computeOrderingDeps(PackageGraph graph) {
var orderingDeps = new Map<String, Set<String>>();
// Iterate through the packages in a deterministic order so that if there are
// multiple cycles we choose which to print consistently.
var packages = ordered(graph.packages.values.map((package) => package.name));
for (var package in packages) {
// This package's transformer dependencies are also ordering dependencies.
var deps = _transformerDeps(graph, package);
deps.remove(package);
// The transformer dependencies of this package's transitive package
// dependencies are also ordering dependencies for this package.
var transitivePackageDeps = graph.transitiveDependencies(package)
.map((package) => package.name);
for (var packageDep in ordered(transitivePackageDeps)) {
var transformerDeps = _transformerDeps(graph, packageDep);
if (transformerDeps.contains(package)) {
throw _cycleError(graph, package, packageDep);
}
deps.addAll(transformerDeps);
}
orderingDeps[package] = deps;
}
return orderingDeps;
}
/// Returns the set of transformer dependencies for [package].
Set<String> _transformerDeps(PackageGraph graph, String package) =>
unionAll(graph.packages[package].pubspec.transformers)
.where((id) => !id.isBuiltInTransformer)
.map((id) => id.package).toSet();
/// Returns an [ApplicationException] describing an ordering dependency cycle
/// detected in [graph].
///
/// [dependee] and [depender] should be the names of two packages known to be in
/// the cycle. In addition, [depender] should have a transformer dependency on
/// [dependee].
ApplicationException _cycleError(PackageGraph graph, String dependee,
String depender) {
assert(_transformerDeps(graph, depender).contains(dependee));
var simpleGraph = mapMapValues(graph.packages,
(_, package) => package.dependencies.map((dep) => dep.name).toList());
var path = shortestPath(simpleGraph, dependee, depender);
path.add(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)
.map((id) => id.toString()).toList();
if (transformers.isEmpty) {
return " ${pair.first} depends on ${pair.last}";
} else {
return " ${pair.first} is transformed by ${toSentence(transformers)}";
}
}).join("\n"));
}
/// 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<TransformerId>());
for (var package in graph.packages.values) {
for (var phase in package.pubspec.transformers) {
for (var id in phase) {
if (id.isBuiltInTransformer) continue;
packageTransformers[id.package].add(id);
}
}
}
return packageTransformers;
}
/// A class that loads transformers defined in specific files.
class _TransformerLoader {
final PackageGraph _graph;
final BarbackServer _server;
/// The mode that pub is running barback in.
final BarbackMode _mode;
/// 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 asset id.
///
/// Used for error reporting.
final _transformerUsers = new Map<Pair<String, String>, Set<String>>();
_TransformerLoader(this._server, this._mode, this._graph) {
for (var package in _graph.packages.values) {
for (var id in unionAll(package.pubspec.transformers)) {
_transformerUsers.putIfAbsent(
new Pair(id.package, id.path), () => new Set<String>())
.add(package.name);
}
}
}
/// 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(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, _mode, id).then((transformers) {
if (!transformers.isEmpty) {
_transformers[id] = transformers;
return;
}
var message = "No transformers";
if (id.configuration.isNotEmpty) {
message += " that accept configuration";
}
var location;
if (id.path == null) {
location = 'package:${id.package}/transformer.dart or '
'package:${id.package}/${id.package}.dart';
} else {
location = 'package:$id.dart';
}
var pair = new Pair(id.package, id.path);
throw new ApplicationException(
"$message were defined in $location,\n"
"required by ${ordered(_transformerUsers[pair]).join(', ')}.");
});
}
/// Returns the set of transformers for [id].
///
/// It's an error to call this before [load] is called with [id] and the
/// future it returns has completed.
Set<Transformer> transformersFor(TransformerId id) {
if (_transformers.containsKey(id)) return _transformers[id];
assert(id.package == '\$dart2js');
var transformer;
try {
transformer = new Dart2JSTransformer.withSettings(
_graph, new BarbackSettings(id.configuration, _mode));
} on FormatException catch (error, stackTrace) {
fail(error.message, error, stackTrace);
}
_transformers[id] = new Set.from([transformer]);
return _transformers[id];
}
}