| // 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.asset_environment; |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| |
| import 'package:barback/barback.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:watcher/watcher.dart'; |
| |
| import '../cached_package.dart'; |
| import '../entrypoint.dart'; |
| import '../exceptions.dart'; |
| import '../io.dart'; |
| import '../log.dart' as log; |
| import '../package.dart'; |
| import '../package_graph.dart'; |
| import '../sdk.dart' as sdk; |
| import '../source/cached.dart'; |
| import '../utils.dart'; |
| import 'admin_server.dart'; |
| import 'barback_server.dart'; |
| import 'dart_forwarding_transformer.dart'; |
| import 'dart2js_transformer.dart'; |
| import 'load_all_transformers.dart'; |
| import 'pub_package_provider.dart'; |
| import 'source_directory.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 |
| /// transform. |
| class AssetEnvironment { |
| /// Creates a new build environment for working with the assets used by |
| /// [entrypoint] and its dependencies. |
| /// |
| /// HTTP servers that serve directories from this environment will be bound |
| /// to [hostname] and have ports based on [basePort]. If omitted, they |
| /// default to "localhost" and "0" (use ephemeral ports), respectively. |
| /// |
| /// Loads all used transformers using [mode] (including dart2js if |
| /// [useDart2JS] is true). |
| /// |
| /// This will only add the root package's "lib" directory to the environment. |
| /// Other directories can be added to the environment using [serveDirectory]. |
| /// |
| /// If [watcherType] is not [WatcherType.NONE] (the default), watches source |
| /// assets for modification. |
| /// |
| /// If [packages] is passed, only those packages' assets are loaded and |
| /// served. |
| /// |
| /// If [entrypoints] is passed, only transformers necessary to run those |
| /// entrypoints are loaded. Each entrypoint is expected to refer to a Dart |
| /// library. |
| /// |
| /// Returns a [Future] that completes to the environment once the inputs, |
| /// transformers, and server are loaded and ready. |
| static Future<AssetEnvironment> create(Entrypoint entrypoint, |
| BarbackMode mode, {WatcherType watcherType, String hostname, int basePort, |
| Iterable<String> packages, Iterable<AssetId> entrypoints, bool useDart2JS: |
| true}) { |
| if (watcherType == null) watcherType = WatcherType.NONE; |
| if (hostname == null) hostname = "localhost"; |
| if (basePort == null) basePort = 0; |
| |
| return log.progress("Loading asset environment", () { |
| final completer0 = new Completer(); |
| scheduleMicrotask(() { |
| try { |
| entrypoint.loadPackageGraph().then((x0) { |
| try { |
| var graph = x0; |
| graph = _adjustPackageGraph(graph, mode, packages); |
| var barback = new Barback(new PubPackageProvider(graph)); |
| barback.log.listen(_log); |
| var environment = |
| new AssetEnvironment._(graph, barback, mode, watcherType, hostname, basePort); |
| environment._load( |
| entrypoints: entrypoints, |
| useDart2JS: useDart2JS).then((x1) { |
| try { |
| x1; |
| completer0.complete(environment); |
| } catch (e0, s0) { |
| completer0.completeError(e0, s0); |
| } |
| }, onError: completer0.completeError); |
| } catch (e1, s1) { |
| completer0.completeError(e1, s1); |
| } |
| }, onError: completer0.completeError); |
| } catch (e, s) { |
| completer0.completeError(e, s); |
| } |
| }); |
| return completer0.future; |
| }, fine: true); |
| } |
| |
| /// Return a version of [graph] that's restricted to [packages] (if passed) |
| /// and loads cached packages (if [mode] is [BarbackMode.DEBUG]). |
| static PackageGraph _adjustPackageGraph(PackageGraph graph, BarbackMode mode, |
| Iterable<String> packages) { |
| if (mode != BarbackMode.DEBUG && packages == null) return graph; |
| packages = (packages == null ? graph.packages.keys : packages).toSet(); |
| |
| return new PackageGraph( |
| graph.entrypoint, |
| graph.lockFile, |
| new Map.fromIterable(packages, value: (packageName) { |
| var package = graph.packages[packageName]; |
| if (mode != BarbackMode.DEBUG) return package; |
| var cache = path.join('.pub/deps/debug', packageName); |
| if (!dirExists(cache)) return package; |
| return new CachedPackage(package, cache); |
| })); |
| } |
| |
| /// The server for the Web Socket API and admin interface. |
| AdminServer _adminServer; |
| |
| /// The public directories in the root package that are included in the asset |
| /// environment, keyed by their root directory. |
| final _directories = new Map<String, SourceDirectory>(); |
| |
| /// The [Barback] instance used to process assets in this environment. |
| final Barback barback; |
| |
| /// The root package being built. |
| Package get rootPackage => graph.entrypoint.root; |
| |
| /// The graph of packages whose assets and transformers are loaded in this |
| /// environment. |
| /// |
| /// This isn't necessarily identical to the graph that's passed in to the |
| /// environment. It may expose fewer packages if some packages' assets don't |
| /// need to be loaded, and it may expose some [CachedPackage]s. |
| 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 hostname that servers are bound to. |
| final String _hostname; |
| |
| /// The starting number for ports that servers will be bound to. |
| /// |
| /// Servers will be bound to ports starting at this number and then |
| /// incrementing from there. However, if this is zero, then ephemeral port |
| /// numbers will be selected for each server. |
| final int _basePort; |
| |
| /// The modified source assets that have not been sent to barback yet. |
| /// |
| /// The build environment can be paused (by calling [pauseUpdates]) and |
| /// resumed ([resumeUpdates]). While paused, all source asset updates that |
| /// come from watching or adding new directories are not sent to barback. |
| /// When resumed, all pending source updates are sent to barback. |
| /// |
| /// This lets pub serve and pub build create an environment and bind several |
| /// servers before barback starts building and producing results |
| /// asynchronously. |
| /// |
| /// If this is `null`, then the environment is "live" and all updates will |
| /// go to barback immediately. |
| Set<AssetId> _modifiedSources; |
| |
| AssetEnvironment._(this.graph, this.barback, this.mode, this._watcherType, |
| this._hostname, this._basePort); |
| |
| /// 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; |
| } |
| |
| /// Starts up the admin server on an appropriate port and returns it. |
| /// |
| /// This may only be called once on the build environment. |
| Future<AdminServer> startAdminServer([int port]) { |
| // Can only start once. |
| assert(_adminServer == null); |
| |
| // The admin server is bound to one before the base port by default, unless |
| // it's ephemeral in which case the admin port is too. |
| if (port == null) port = _basePort == 0 ? 0 : _basePort - 1; |
| |
| return AdminServer.bind(this, _hostname, port).then((server) => _adminServer = |
| server); |
| } |
| |
| /// Binds a new port to serve assets from within [rootDirectory] in the |
| /// entrypoint package. |
| /// |
| /// Adds and watches the sources within that directory. Returns a [Future] |
| /// that completes to the bound server. |
| /// |
| /// If [rootDirectory] is already being served, returns that existing server. |
| Future<BarbackServer> serveDirectory(String rootDirectory) { |
| // See if there is already a server bound to the directory. |
| var directory = _directories[rootDirectory]; |
| if (directory != null) { |
| return directory.server.then((server) { |
| log.fine('Already serving $rootDirectory on ${server.url}.'); |
| return server; |
| }); |
| } |
| |
| // See if the new directory overlaps any existing servers. |
| var overlapping = _directories.keys.where( |
| (directory) => |
| path.isWithin(directory, rootDirectory) || |
| path.isWithin(rootDirectory, directory)).toList(); |
| |
| if (overlapping.isNotEmpty) { |
| return new Future.error( |
| new OverlappingSourceDirectoryException(overlapping)); |
| } |
| |
| var port = _basePort; |
| |
| // If not using an ephemeral port, find the lowest-numbered available one. |
| if (port != 0) { |
| var boundPorts = |
| _directories.values.map((directory) => directory.port).toSet(); |
| while (boundPorts.contains(port)) { |
| port++; |
| } |
| } |
| |
| var sourceDirectory = |
| new SourceDirectory(this, rootDirectory, _hostname, port); |
| _directories[rootDirectory] = sourceDirectory; |
| |
| return _provideDirectorySources( |
| rootPackage, |
| rootDirectory).then((subscription) { |
| sourceDirectory.watchSubscription = subscription; |
| return sourceDirectory.serve(); |
| }); |
| } |
| |
| /// Binds a new port to serve assets from within the "bin" directory of |
| /// [package]. |
| /// |
| /// Adds the sources within that directory and then binds a server to it. |
| /// Unlike [serveDirectory], this works with packages that are not the |
| /// entrypoint. |
| /// |
| /// Returns a [Future] that completes to the bound server. |
| Future<BarbackServer> servePackageBinDirectory(String package) { |
| return _provideDirectorySources( |
| graph.packages[package], |
| "bin").then( |
| (_) => |
| BarbackServer.bind(this, _hostname, 0, package: package, rootDirectory: "bin")); |
| } |
| |
| /// Precompiles all of [packageName]'s executables to snapshots in |
| /// [directory]. |
| /// |
| /// If [executableIds] is passed, only those executables are precompiled. |
| /// |
| /// Returns a map from executable name to path for the snapshots that were |
| /// successfully precompiled. |
| Future<Map<String, String>> precompileExecutables(String packageName, |
| String directory, {Iterable<AssetId> executableIds}) { |
| final completer0 = new Completer(); |
| scheduleMicrotask(() { |
| try { |
| join0() { |
| log.fine("Executables for ${packageName}: ${executableIds}"); |
| join1() { |
| var package = graph.packages[packageName]; |
| servePackageBinDirectory(packageName).then((x0) { |
| try { |
| var server = x0; |
| join2() { |
| completer0.complete(); |
| } |
| finally0(cont0) { |
| server.close(); |
| cont0(); |
| } |
| catch0(e1, s1) { |
| finally0(() => completer0.completeError(e1, s1)); |
| } |
| try { |
| var precompiled = {}; |
| waitAndPrintErrors(executableIds.map(((id) { |
| final completer0 = new Completer(); |
| scheduleMicrotask(() { |
| try { |
| var basename = path.url.basename(id.path); |
| var snapshotPath = |
| path.join(directory, "${basename}.snapshot"); |
| runProcess( |
| Platform.executable, |
| [ |
| '--snapshot=${snapshotPath}', |
| server.url.resolve(basename).toString()]).then((x0) { |
| try { |
| var result = x0; |
| join0() { |
| completer0.complete(); |
| } |
| if (result.success) { |
| log.message( |
| "Precompiled ${_formatExecutable(id)}."); |
| precompiled[path.withoutExtension(basename)] = |
| snapshotPath; |
| join0(); |
| } else { |
| throw new ApplicationException( |
| log.yellow("Failed to precompile ${_formatExecutable(id)}:\n") + |
| result.stderr.join('\n')); |
| join0(); |
| } |
| } catch (e0, s0) { |
| completer0.completeError(e0, s0); |
| } |
| }, onError: completer0.completeError); |
| } catch (e, s) { |
| completer0.completeError(e, s); |
| } |
| }); |
| return completer0.future; |
| }))).then((x1) { |
| try { |
| x1; |
| final v0 = precompiled; |
| finally0(() { |
| completer0.complete(v0); |
| }); |
| } catch (e2, s2) { |
| catch0(e2, s2); |
| } |
| }, onError: catch0); |
| } catch (e3, s3) { |
| catch0(e3, s3); |
| } |
| } catch (e4, s4) { |
| completer0.completeError(e4, s4); |
| } |
| }, onError: completer0.completeError); |
| } |
| if (executableIds.isEmpty) { |
| completer0.complete([]); |
| } else { |
| join1(); |
| } |
| } |
| if (executableIds == null) { |
| executableIds = graph.packages[packageName].executableIds; |
| join0(); |
| } else { |
| join0(); |
| } |
| } catch (e, s) { |
| completer0.completeError(e, s); |
| } |
| }); |
| return completer0.future; |
| } |
| |
| /// Returns the executable name for [id]. |
| /// |
| /// [id] is assumed to be an executable in a bin directory. The return value |
| /// is intended for log output and may contain formatting. |
| String _formatExecutable(AssetId id) => |
| log.bold("${id.package}:${path.basenameWithoutExtension(id.path)}"); |
| |
| /// Stops the server bound to [rootDirectory]. |
| /// |
| /// Also removes any source files within that directory from barback. Returns |
| /// the URL of the unbound server, of `null` if [rootDirectory] was not |
| /// bound to a server. |
| Future<Uri> unserveDirectory(String rootDirectory) { |
| log.fine("Unserving $rootDirectory."); |
| var directory = _directories.remove(rootDirectory); |
| if (directory == null) return new Future.value(); |
| |
| return directory.server.then((server) { |
| var url = server.url; |
| return directory.close().then((_) { |
| _removeDirectorySources(rootDirectory); |
| return url; |
| }); |
| }); |
| } |
| |
| /// Gets the source directory that contains [assetPath] within the entrypoint |
| /// package. |
| /// |
| /// If [assetPath] is not contained within a source directory, this throws |
| /// an exception. |
| String getSourceDirectoryContaining(String assetPath) => |
| _directories.values.firstWhere( |
| (dir) => path.isWithin(dir.directory, assetPath)).directory; |
| |
| /// Return all URLs serving [assetPath] in this environment. |
| Future<List<Uri>> getUrlsForAssetPath(String assetPath) { |
| // Check the three (mutually-exclusive) places the path could be pointing. |
| return _lookUpPathInServerRoot(assetPath).then((urls) { |
| if (urls.isNotEmpty) return urls; |
| return _lookUpPathInPackagesDirectory(assetPath); |
| }).then((urls) { |
| if (urls.isNotEmpty) return urls; |
| return _lookUpPathInDependency(assetPath); |
| }); |
| } |
| |
| /// Look up [assetPath] in the root directories of servers running in the |
| /// entrypoint package. |
| Future<List<Uri>> _lookUpPathInServerRoot(String assetPath) { |
| // Find all of the servers whose root directories contain the asset and |
| // generate appropriate URLs for each. |
| return Future.wait( |
| _directories.values.where( |
| (dir) => path.isWithin(dir.directory, assetPath)).map((dir) { |
| var relativePath = path.relative(assetPath, from: dir.directory); |
| return dir.server.then( |
| (server) => server.url.resolveUri(path.toUri(relativePath))); |
| })); |
| } |
| |
| /// Look up [assetPath] in the "packages" directory in the entrypoint package. |
| Future<List<Uri>> _lookUpPathInPackagesDirectory(String assetPath) { |
| var components = path.split(path.relative(assetPath)); |
| if (components.first != "packages") return new Future.value([]); |
| if (!graph.packages.containsKey(components[1])) return new Future.value([]); |
| return Future.wait(_directories.values.map((dir) { |
| return dir.server.then( |
| (server) => server.url.resolveUri(path.toUri(assetPath))); |
| })); |
| } |
| |
| /// Look up [assetPath] in the "lib" or "asset" directory of a dependency |
| /// package. |
| Future<List<Uri>> _lookUpPathInDependency(String assetPath) { |
| for (var packageName in graph.packages.keys) { |
| var package = graph.packages[packageName]; |
| var libDir = package.path('lib'); |
| var assetDir = package.path('asset'); |
| |
| var uri; |
| if (path.isWithin(libDir, assetPath)) { |
| uri = path.toUri( |
| path.join('packages', package.name, path.relative(assetPath, from: libDir))); |
| } else if (path.isWithin(assetDir, assetPath)) { |
| uri = path.toUri( |
| path.join('assets', package.name, path.relative(assetPath, from: assetDir))); |
| } else { |
| continue; |
| } |
| |
| return Future.wait(_directories.values.map((dir) { |
| return dir.server.then((server) => server.url.resolveUri(uri)); |
| })); |
| } |
| |
| return new Future.value([]); |
| } |
| |
| /// Given a URL to an asset served by this environment, returns the ID of the |
| /// asset that would be accessed by that URL. |
| /// |
| /// If no server can serve [url], completes to `null`. |
| Future<AssetId> getAssetIdForUrl(Uri url) { |
| return Future.wait( |
| _directories.values.map((dir) => dir.server)).then((servers) { |
| var server = servers.firstWhere((server) { |
| if (server.port != url.port) return false; |
| return isLoopback(server.address.host) == isLoopback(url.host) || |
| server.address.host == url.host; |
| }, orElse: () => null); |
| if (server == null) return null; |
| return server.urlToId(url); |
| }); |
| } |
| |
| /// Determines if [sourcePath] is contained within any of the directories in |
| /// the root package that are visible to this build environment. |
| bool containsPath(String sourcePath) { |
| var directories = ["lib"]; |
| directories.addAll(_directories.keys); |
| return directories.any((dir) => path.isWithin(dir, sourcePath)); |
| } |
| |
| /// Pauses sending source asset updates to barback. |
| void pauseUpdates() { |
| // Cannot pause while already paused. |
| assert(_modifiedSources == null); |
| |
| _modifiedSources = new Set<AssetId>(); |
| } |
| |
| /// Sends any pending source updates to barback and begins the asynchronous |
| /// build process. |
| void resumeUpdates() { |
| // Cannot resume while not paused. |
| assert(_modifiedSources != null); |
| |
| barback.updateSources(_modifiedSources); |
| _modifiedSources = null; |
| } |
| |
| /// Loads the assets and transformers 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. |
| /// |
| /// If [useDart2JS] is `true`, then the [Dart2JSTransformer] is implicitly |
| /// added to end of the root package's transformer phases. |
| /// |
| /// If [entrypoints] is passed, only transformers necessary to run those |
| /// entrypoints will be loaded. |
| /// |
| /// Returns a [Future] that completes once all inputs and transformers are |
| /// loaded. |
| Future _load({Iterable<AssetId> entrypoints, bool useDart2JS}) { |
| return log.progress("Initializing barback", () { |
| final completer0 = new Completer(); |
| scheduleMicrotask(() { |
| try { |
| var containsDart2JS = |
| graph.entrypoint.root.pubspec.transformers.any(((transformers) { |
| return transformers.any( |
| (config) => config.id.package == '\$dart2js'); |
| })); |
| join0() { |
| BarbackServer.bind(this, _hostname, 0).then((x0) { |
| try { |
| var transformerServer = x0; |
| var errorStream = barback.errors.map(((error) { |
| if (error is! AssetLoadException) throw error; |
| log.error(log.red(error.message)); |
| log.fine(error.stackTrace.terse); |
| })); |
| _withStreamErrors((() { |
| return log.progress("Loading source assets", _provideSources); |
| }), [errorStream, barback.results]).then((x1) { |
| try { |
| x1; |
| log.fine("Provided sources."); |
| errorStream = barback.errors.map(((error) { |
| if (error is! TransformerException) throw error; |
| var message = error.error.toString(); |
| if (error.stackTrace != null) { |
| message += "\n" + error.stackTrace.terse.toString(); |
| } |
| _log( |
| new LogEntry( |
| error.transform, |
| error.transform.primaryId, |
| LogLevel.ERROR, |
| message, |
| null)); |
| })); |
| _withStreamErrors((() { |
| final completer0 = new Completer(); |
| scheduleMicrotask(() { |
| try { |
| completer0.complete( |
| log.progress("Loading transformers", (() { |
| final completer0 = new Completer(); |
| scheduleMicrotask(() { |
| try { |
| loadAllTransformers( |
| this, |
| transformerServer, |
| entrypoints: entrypoints).then((x0) { |
| try { |
| x0; |
| transformerServer.close(); |
| completer0.complete(); |
| } catch (e0, s0) { |
| completer0.completeError(e0, s0); |
| } |
| }, onError: completer0.completeError); |
| } catch (e, s) { |
| completer0.completeError(e, s); |
| } |
| }); |
| return completer0.future; |
| }), fine: true)); |
| } catch (e, s) { |
| completer0.completeError(e, s); |
| } |
| }); |
| return completer0.future; |
| }), |
| [errorStream, barback.results, transformerServer.results]).then((x2) { |
| try { |
| x2; |
| completer0.complete(); |
| } catch (e0, s0) { |
| completer0.completeError(e0, s0); |
| } |
| }, onError: completer0.completeError); |
| } catch (e1, s1) { |
| completer0.completeError(e1, s1); |
| } |
| }, onError: completer0.completeError); |
| } catch (e2, s2) { |
| completer0.completeError(e2, s2); |
| } |
| }, onError: completer0.completeError); |
| } |
| if (!containsDart2JS && useDart2JS) { |
| _builtInTransformers.addAll( |
| [new Dart2JSTransformer(this, mode), new DartForwardingTransformer(mode)]); |
| join0(); |
| } else { |
| join0(); |
| } |
| } catch (e, s) { |
| completer0.completeError(e, s); |
| } |
| }); |
| return completer0.future; |
| }, fine: true); |
| } |
| |
| /// Provides the public source assets in the environment to barback. |
| /// |
| /// If [watcherType] is not [WatcherType.NONE], enables watching on them. |
| Future _provideSources() { |
| final completer0 = new Completer(); |
| scheduleMicrotask(() { |
| try { |
| Future.wait(graph.packages.values.map(((package) { |
| final completer0 = new Completer(); |
| scheduleMicrotask(() { |
| try { |
| join0() { |
| _provideDirectorySources(package, "lib").then((x0) { |
| try { |
| x0; |
| completer0.complete(); |
| } catch (e0, s0) { |
| completer0.completeError(e0, s0); |
| } |
| }, onError: completer0.completeError); |
| } |
| if (graph.isPackageStatic(package.name)) { |
| completer0.complete(null); |
| } else { |
| join0(); |
| } |
| } catch (e, s) { |
| completer0.completeError(e, s); |
| } |
| }); |
| return completer0.future; |
| }))).then((x0) { |
| try { |
| x0; |
| completer0.complete(); |
| } catch (e0, s0) { |
| completer0.completeError(e0, s0); |
| } |
| }, onError: completer0.completeError); |
| } catch (e, s) { |
| completer0.completeError(e, s); |
| } |
| }); |
| return completer0.future; |
| } |
| |
| /// Provides all of the source assets within [dir] in [package] to barback. |
| /// |
| /// If [watcherType] is not [WatcherType.NONE], enables watching on them. |
| /// Returns the subscription to the watcher, or `null` if none was created. |
| Future<StreamSubscription<WatchEvent>> |
| _provideDirectorySources(Package package, String dir) { |
| log.fine("Providing sources for ${package.name}|$dir."); |
| // TODO(rnystrom): Handle overlapping directories. If two served |
| // directories overlap like so: |
| // |
| // $ pub serve example example/subdir |
| // |
| // Then the sources of the subdirectory will be updated and watched twice. |
| // See: #17454 |
| if (_watcherType == WatcherType.NONE) { |
| _updateDirectorySources(package, dir); |
| return new Future.value(); |
| } |
| |
| // Watch the directory before listing is so we don't miss files that |
| // are added between the initial list and registering the watcher. |
| return _watchDirectorySources(package, dir).then((_) { |
| _updateDirectorySources(package, dir); |
| }); |
| } |
| |
| /// Updates barback with all of the files in [dir] inside [package]. |
| void _updateDirectorySources(Package package, String dir) { |
| var ids = _listDirectorySources(package, dir); |
| if (_modifiedSources == null) { |
| barback.updateSources(ids); |
| } else { |
| _modifiedSources.addAll(ids); |
| } |
| } |
| |
| /// Removes all of the files in [dir] in the root package from barback. |
| void _removeDirectorySources(String dir) { |
| var ids = _listDirectorySources(rootPackage, dir); |
| if (_modifiedSources == null) { |
| barback.removeSources(ids); |
| } else { |
| _modifiedSources.removeAll(ids); |
| } |
| } |
| |
| /// Lists all of the source assets in [dir] inside [package]. |
| /// |
| /// For large packages, listing the contents is a performance bottleneck, so |
| /// this is optimized for our needs in here instead of using the more general |
| /// but slower [listDir]. |
| Iterable<AssetId> _listDirectorySources(Package package, String dir) { |
| // This is used in some performance-sensitive paths and can list many, many |
| // files. As such, it leans more havily towards optimization as opposed to |
| // readability than most code in pub. In particular, it avoids using the |
| // path package, since re-parsing a path is very expensive relative to |
| // string operations. |
| return package.listFiles(beneath: dir).map((file) { |
| // From profiling, path.relative here is just as fast as a raw substring |
| // and is correct in the case where package.dir has a trailing slash. |
| var relative = package.relative(file); |
| |
| if (Platform.operatingSystem == 'windows') { |
| relative = relative.replaceAll("\\", "/"); |
| } |
| |
| var uri = new Uri(pathSegments: relative.split("/")); |
| return new AssetId(package.name, uri.toString()); |
| }); |
| } |
| |
| /// Adds a file watcher for [dir] within [package], if the directory exists |
| /// and the package needs watching. |
| Future<StreamSubscription<WatchEvent>> _watchDirectorySources(Package package, |
| String dir) { |
| // 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] is CachedSource) { |
| return new Future.value(); |
| } |
| |
| var subdirectory = package.path(dir); |
| if (!dirExists(subdirectory)) return new Future.value(); |
| |
| // TODO(nweiz): close this watcher when [barback] is closed. |
| var watcher = _watcherType.create(subdirectory); |
| var subscription = 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")) return; |
| |
| // Skip 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 these when the Editor no longer generates |
| // .js files and users have had enough time that they no longer have |
| // these files laying around. See #15859. |
| if (event.path.endsWith(".dart.js")) return; |
| if (event.path.endsWith(".dart.js.map")) return; |
| if (event.path.endsWith(".dart.precompiled.js")) return; |
| |
| var idPath = package.relative(event.path); |
| var id = new AssetId(package.name, path.toUri(idPath).toString()); |
| if (event.type == ChangeType.REMOVE) { |
| if (_modifiedSources != null) { |
| _modifiedSources.remove(id); |
| } else { |
| barback.removeSources([id]); |
| } |
| } else if (_modifiedSources != null) { |
| _modifiedSources.add(id); |
| } else { |
| barback.updateSources([id]); |
| } |
| }); |
| |
| return watcher.ready.then((_) => subscription); |
| } |
| |
| /// Returns the result of [futureCallback] unless any stream in [streams] |
| /// emits an error before it's done. |
| /// |
| /// If a stream does emit an error, that error is thrown instead. |
| /// [futureCallback] is a callback rather than a plain future to ensure that |
| /// [streams] are listened to before any code that might cause an error starts |
| /// running. |
| Future _withStreamErrors(Future futureCallback(), List<Stream> streams) { |
| var completer = new Completer.sync(); |
| var subscriptions = streams.map( |
| (stream) => stream.listen((_) {}, onError: completer.completeError)).toList(); |
| |
| new Future.sync(futureCallback).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(); |
| } |
| }); |
| } |
| } |
| |
| /// 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(text) => |
| entry.message.toLowerCase().contains(text.toLowerCase()); |
| |
| messageMentionsAsset(id) => |
| messageMentions(id.toString()) || |
| messageMentions(path.fromUri(entry.assetId.path)); |
| |
| 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 (!messageMentionsAsset(entry.transform.primaryId)) { |
| 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 && |
| !messageMentionsAsset(entry.assetId)) { |
| prefixParts.add("with input ${entry.assetId}"); |
| } |
| |
| var prefix = "[${prefixParts.join(' ')}]:"; |
| var message = entry.message; |
| if (entry.span != null) { |
| message = entry.span.message(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; |
| |
| case LogLevel.FINE: |
| log.fine("${log.gray(prefix)}\n$message"); |
| break; |
| } |
| } |
| |
| /// Exception thrown when trying to serve a new directory that overlaps one or |
| /// more directories already being served. |
| class OverlappingSourceDirectoryException implements Exception { |
| /// The relative paths of the directories that overlap the one that could not |
| /// be served. |
| final List<String> overlappingDirectories; |
| |
| OverlappingSourceDirectoryException(this.overlappingDirectories); |
| } |
| |
| /// 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"; |
| } |