Index: sdk/lib/_internal/pub/lib/src/barback/build_environment.dart |
diff --git a/sdk/lib/_internal/pub/lib/src/barback/build_environment.dart b/sdk/lib/_internal/pub/lib/src/barback/build_environment.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..9e1467b071a00811e382b5d860bd081a349ee377 |
--- /dev/null |
+++ b/sdk/lib/_internal/pub/lib/src/barback/build_environment.dart |
@@ -0,0 +1,395 @@ |
+// Copyright (c) 2014, 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.barback.build_environment; |
+ |
+import 'dart:async'; |
+ |
+import 'package:barback/barback.dart'; |
+import 'package:path/path.dart' as path; |
+import 'package:stack_trace/stack_trace.dart'; |
+import 'package:watcher/watcher.dart'; |
+ |
+import '../entrypoint.dart'; |
+import '../io.dart'; |
+import '../log.dart' as log; |
+import '../package.dart'; |
+import '../package_graph.dart'; |
+import '../utils.dart'; |
+import 'dart_forwarding_transformer.dart'; |
+import 'dart2js_transformer.dart'; |
+import 'load_all_transformers.dart'; |
+import 'pub_package_provider.dart'; |
+import 'server.dart'; |
+ |
+/// The entire "visible" state of the assets of a package and all of its |
+/// dependencies, taking into account the user's configuration when running pub. |
+/// |
+/// Where [PackageGraph] just describes the entrypoint's dependencies as |
+/// specified by pubspecs, this includes "transient" information like the mode |
+/// that the user is running pub in, or which directories they want to build. |
+class BuildEnvironment { |
+ /// Creates a new build environment for working with the assets used by |
+ /// [entrypoint] and its dependencies. |
+ /// |
+ /// Spawns an HTTP server on [hostname] and [port]. Loads all used |
+ /// transformers using [mode] (including dart2js if [useDart2JS] is true). |
+ /// |
+ /// Includes [buildDirectories] in the root package, as well as "lib" and |
+ /// "asset". |
+ /// |
+ /// If [watcherType] is not [WatcherType.NONE], watches source assets for |
+ /// modification. |
+ /// |
+ /// Returns a [Future] that completes to the environment once the inputs, |
+ /// transformers, and server are loaded and ready. |
+ static Future<BuildEnvironment> create(Entrypoint entrypoint, |
+ String hostname, int port, BarbackMode mode, WatcherType watcherType, |
+ Set<String> buildDirectories, |
+ {bool useDart2JS: true}) { |
+ return entrypoint.loadPackageGraph().then((graph) { |
+ var barback = new Barback(new PubPackageProvider(graph)); |
+ barback.log.listen(_log); |
+ |
+ return BarbackServer.bind(hostname, port, barback, |
+ graph.entrypoint.root.name).then((server) { |
+ var environment = new BuildEnvironment._(graph, server, mode, |
+ watcherType, buildDirectories); |
+ |
+ // If the entrypoint package manually configures the dart2js |
+ // transformer, don't include it in the built-in transformer list. |
+ // |
+ // TODO(nweiz): if/when we support more built-in transformers, make |
+ // this more general. |
+ var containsDart2JS = graph.entrypoint.root.pubspec.transformers |
+ .any((transformers) => transformers |
+ .any((id) => id.package == '\$dart2js')); |
+ |
+ if (!containsDart2JS && useDart2JS) { |
+ environment._builtInTransformers.addAll([ |
+ new Dart2JSTransformer(environment, mode), |
+ new DartForwardingTransformer(mode) |
+ ]); |
+ } |
+ |
+ return environment._load(barback).then((_) => environment); |
+ }); |
+ }); |
+ } |
+ |
+ /// The server serving this environment's assets. |
+ final BarbackServer server; |
+ |
+ /// The [Barback] instance used to process assets in this environment. |
+ Barback get barback => server.barback; |
+ |
+ /// The root package being built. |
+ Package get rootPackage => graph.entrypoint.root; |
+ |
+ /// The underlying [PackageGraph] being built. |
+ final PackageGraph graph; |
+ |
+ /// The mode to run the transformers in. |
+ final BarbackMode mode; |
+ |
+ /// The [Transformer]s that should be appended by default to the root |
+ /// package's transformer cascade. Will be empty if there are none. |
+ final _builtInTransformers = <Transformer>[]; |
+ |
+ /// How source files should be watched. |
+ final WatcherType _watcherType; |
+ |
+ /// The set of top-level directories in the entrypoint package that should be |
+ /// built. |
+ final Set<String> _buildDirectories; |
+ |
+ BuildEnvironment._(this.graph, this.server, this.mode, this._watcherType, |
+ this._buildDirectories); |
+ |
+ /// Gets the built-in [Transformer]s that should be added to [package]. |
+ /// |
+ /// Returns `null` if there are none. |
+ Iterable<Transformer> getBuiltInTransformers(Package package) { |
+ // Built-in transformers only apply to the root package. |
+ if (package.name != rootPackage.name) return null; |
+ |
+ // The built-in transformers are for dart2js and forwarding assets around |
+ // dart2js. |
+ if (_builtInTransformers.isEmpty) return null; |
+ |
+ return _builtInTransformers; |
+ } |
+ |
+ /// Creates a [BarbackServer] for this environment. |
+ /// |
+ /// This transforms and serves all library and asset files in all packages in |
+ /// the environment's package graph. It loads any transformer plugins defined |
+ /// in packages in [graph] and re-runs them as necessary when any input files |
+ /// change. |
+ /// |
+ /// Returns a [Future] that completes once all inputs and transformers are |
+ /// loaded. |
+ Future _load(Barback barback) { |
+ return _provideSources(barback).then((_) { |
+ var completer = new Completer(); |
+ |
+ // If any errors get emitted either by barback or by the server, |
+ // including non-programmatic barback errors, they should take down the |
+ // whole program. |
+ var subscriptions = [ |
+ server.barback.errors.listen((error) { |
+ if (error is TransformerException) error = error.error; |
+ if (!completer.isCompleted) { |
+ completer.completeError(error, new Chain.current()); |
+ } |
+ }), |
+ server.barback.results.listen((_) {}, onError: (error, stackTrace) { |
+ if (completer.isCompleted) return; |
+ completer.completeError(error, stackTrace); |
+ }), |
+ server.results.listen((_) {}, onError: (error, stackTrace) { |
+ if (completer.isCompleted) return; |
+ completer.completeError(error, stackTrace); |
+ }) |
+ ]; |
+ |
+ loadAllTransformers(this).then((_) { |
+ if (!completer.isCompleted) completer.complete(); |
+ }).catchError((error, stackTrace) { |
+ if (!completer.isCompleted) { |
+ completer.completeError(error, stackTrace); |
+ } |
+ }); |
+ |
+ return completer.future.whenComplete(() { |
+ for (var subscription in subscriptions) { |
+ subscription.cancel(); |
+ } |
+ }); |
+ }); |
+ } |
+ |
+ /// Provides all of the source assets in the environment to barback. |
+ /// |
+ /// If [watcherType] is not [WatcherType.NONE], enables watching on them. |
+ Future _provideSources(Barback barback) { |
+ if (_watcherType != WatcherType.NONE) { |
+ return _watchSources(barback); |
+ } |
+ |
+ return syncFuture(() { |
+ _loadSources(barback); |
+ }); |
+ } |
+ |
+ /// Provides all of the source assets in the environment to barback. |
+ void _loadSources(Barback barback) { |
+ for (var package in graph.packages.values) { |
+ barback.updateSources(_listAssets(graph.entrypoint, package)); |
+ } |
+ } |
+ |
+ /// Adds all of the source assets in this environment to barback and then |
+ /// watches the public directories for changes. |
+ /// |
+ /// Returns a Future that completes when the sources are loaded and the |
+ /// watchers are active. |
+ Future _watchSources(Barback barback) { |
+ return Future.wait(graph.packages.values.map((package) { |
+ // If this package comes from a cached source, its contents won't change |
+ // so we don't need to monitor it. `packageId` will be null for the |
+ // application package, since that's not locked. |
+ var packageId = graph.lockFile.packages[package.name]; |
+ if (packageId != null && |
+ graph.entrypoint.cache.sources[packageId.source].shouldCache) { |
+ barback.updateSources(_listAssets(graph.entrypoint, package)); |
+ return new Future.value(); |
+ } |
+ |
+ // Watch the visible package directories for changes. |
+ return Future.wait(_getPublicDirectories(graph.entrypoint, package) |
+ .map((name) { |
+ var subdirectory = path.join(package.dir, name); |
+ if (!dirExists(subdirectory)) return new Future.value(); |
+ |
+ // TODO(nweiz): close these watchers when [barback] is closed. |
+ var watcher = _watcherType.create(subdirectory); |
+ watcher.events.listen((event) { |
+ // Don't watch files symlinked into these directories. |
+ // TODO(rnystrom): If pub gets rid of symlinks, remove this. |
+ var parts = path.split(event.path); |
+ if (parts.contains("packages") || parts.contains("assets")) return; |
+ |
+ // Skip ".js" files that were (most likely) compiled from nearby |
+ // ".dart" files. These are created by the Editor's "Run as |
+ // JavaScript" command and are written directly into the package's |
+ // directory. When pub's dart2js transformer then tries to create the |
+ // same file name, we get a build error. To avoid that, just don't |
+ // consider that file to be a source. |
+ // TODO(rnystrom): Remove this when the Editor no longer generates |
+ // .js files. See #15859. |
+ if (event.path.endsWith(".dart.js")) return; |
+ |
+ var id = new AssetId(package.name, |
+ path.relative(event.path, from: package.dir)); |
+ if (event.type == ChangeType.REMOVE) { |
+ barback.removeSources([id]); |
+ } else { |
+ barback.updateSources([id]); |
+ } |
+ }); |
+ return watcher.ready; |
+ })).then((_) { |
+ barback.updateSources(_listAssets(graph.entrypoint, package)); |
+ }); |
+ })); |
+ } |
+ |
+ /// Lists all of the visible files in [package]. |
+ /// |
+ /// This is the recursive contents of the "asset" and "lib" directories (if |
+ /// present). If [package] is the entrypoint package, it also includes the |
+ /// contents of "web". |
+ List<AssetId> _listAssets(Entrypoint entrypoint, Package package) { |
+ var files = <AssetId>[]; |
+ |
+ for (var dirPath in _getPublicDirectories(entrypoint, package)) { |
+ var dir = path.join(package.dir, dirPath); |
+ if (!dirExists(dir)) continue; |
+ for (var entry in listDir(dir, recursive: true)) { |
+ // Ignore "packages" symlinks if there. |
+ if (path.split(entry).contains("packages")) continue; |
+ |
+ // Skip directories. |
+ if (!fileExists(entry)) continue; |
+ |
+ // Skip ".js" files that were (most likely) compiled from nearby ".dart" |
+ // files. These are created by the Editor's "Run as JavaScript" command |
+ // and are written directly into the package's directory. When pub's |
+ // dart2js transformer then tries to create the same file name, we get |
+ // a build error. To avoid that, just don't consider that file to be a |
+ // source. |
+ // TODO(rnystrom): Remove this when the Editor no longer generates .js |
+ // files. See #15859. |
+ if (entry.endsWith(".dart.js")) continue; |
+ |
+ var id = new AssetId(package.name, |
+ path.relative(entry, from: package.dir)); |
+ files.add(id); |
+ } |
+ } |
+ |
+ return files; |
+ } |
+ |
+ /// Gets the names of the top-level directories in [package] whose contents |
+ /// should be provided as source assets. |
+ Iterable<String> _getPublicDirectories(Entrypoint entrypoint, |
+ Package package) { |
+ var directories = ["asset", "lib"]; |
+ |
+ if (package.name == entrypoint.root.name) { |
+ directories.addAll(_buildDirectories); |
+ } |
+ |
+ return directories; |
+ } |
+} |
+ |
+/// Log [entry] using Pub's logging infrastructure. |
+/// |
+/// Since both [LogEntry] objects and the message itself often redundantly |
+/// show the same context like the file where an error occurred, this tries |
+/// to avoid showing redundant data in the entry. |
+void _log(LogEntry entry) { |
+ messageMentions(String text) { |
+ return entry.message.toLowerCase().contains(text.toLowerCase()); |
+ } |
+ |
+ var prefixParts = []; |
+ |
+ // Show the level (unless the message mentions it). |
+ if (!messageMentions(entry.level.name)) { |
+ prefixParts.add("${entry.level} from"); |
+ } |
+ |
+ // Show the transformer. |
+ prefixParts.add(entry.transform.transformer); |
+ |
+ // Mention the primary input of the transform unless the message seems to. |
+ if (!messageMentions(entry.transform.primaryId.path)) { |
+ prefixParts.add("on ${entry.transform.primaryId}"); |
+ } |
+ |
+ // If the relevant asset isn't the primary input, mention it unless the |
+ // message already does. |
+ if (entry.assetId != entry.transform.primaryId && |
+ !messageMentions(entry.assetId.path)) { |
+ prefixParts.add("with input ${entry.assetId}"); |
+ } |
+ |
+ var prefix = "[${prefixParts.join(' ')}]:"; |
+ var message = entry.message; |
+ if (entry.span != null) { |
+ message = entry.span.getLocationMessage(entry.message); |
+ } |
+ |
+ switch (entry.level) { |
+ case LogLevel.ERROR: |
+ log.error("${log.red(prefix)}\n$message"); |
+ break; |
+ |
+ case LogLevel.WARNING: |
+ log.warning("${log.yellow(prefix)}\n$message"); |
+ break; |
+ |
+ case LogLevel.INFO: |
+ log.message("${log.cyan(prefix)}\n$message"); |
+ break; |
+ } |
+} |
+ |
+/// An enum describing different modes of constructing a [DirectoryWatcher]. |
+abstract class WatcherType { |
+ /// A watcher that automatically chooses its type based on the operating |
+ /// system. |
+ static const AUTO = const _AutoWatcherType(); |
+ |
+ /// A watcher that always polls the filesystem for changes. |
+ static const POLLING = const _PollingWatcherType(); |
+ |
+ /// No directory watcher at all. |
+ static const NONE = const _NoneWatcherType(); |
+ |
+ /// Creates a new DirectoryWatcher. |
+ DirectoryWatcher create(String directory); |
+ |
+ String toString(); |
+} |
+ |
+class _AutoWatcherType implements WatcherType { |
+ const _AutoWatcherType(); |
+ |
+ DirectoryWatcher create(String directory) => |
+ new DirectoryWatcher(directory); |
+ |
+ String toString() => "auto"; |
+} |
+ |
+class _PollingWatcherType implements WatcherType { |
+ const _PollingWatcherType(); |
+ |
+ DirectoryWatcher create(String directory) => |
+ new PollingDirectoryWatcher(directory); |
+ |
+ String toString() => "polling"; |
+} |
+ |
+class _NoneWatcherType implements WatcherType { |
+ const _NoneWatcherType(); |
+ |
+ DirectoryWatcher create(String directory) => null; |
+ |
+ String toString() => "none"; |
+} |