| // 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]; |
| } |
| } |