| // 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.barback; |
| |
| import 'dart:async'; |
| |
| import 'package:barback/barback.dart'; |
| import 'package:path/path.dart' as path; |
| |
| import 'utils.dart'; |
| import 'version.dart'; |
| |
| /// The currently supported versions of the Barback package that this version of |
| /// pub works with. |
| /// |
| /// Pub implicitly constrains barback to these versions. |
| /// |
| /// Barback is in a unique position. Pub imports it, so a copy of Barback is |
| /// physically included in the SDK. Packages also depend on Barback (from |
| /// pub.dartlang.org) when they implement their own transformers. Pub's plug-in |
| /// API dynamically loads transformers into their own isolate. |
| /// |
| /// This includes a string literal of Dart code ([_TRANSFORMER_ISOLATE] in |
| /// load_transformers.dart). That code imports "package:barback/barback.dart". |
| /// This string is included in the SDK, but that import is resolved using the |
| /// application’s version of Barback. That means it must tightly control which |
| /// version of Barback the application is using so that it's one that pub |
| /// supports. |
| /// |
| /// Whenever a new non-patch version of barback is published, this *must* be |
| /// incremented to synchronize with that. |
| final supportedVersions = new VersionConstraint.parse(">=0.13.0-dev <0.14.0"); |
| |
| /// A list of the names of all built-in transformers that pub exposes. |
| const _BUILT_IN_TRANSFORMERS = const ['\$dart2js']; |
| |
| /// An identifier for a transformer and the configuration that will be passed to |
| /// it. |
| /// |
| /// It's possible that the library identified by [this] defines multiple |
| /// transformers. If so, [configuration] will be passed to all of them. |
| class TransformerId { |
| /// The package containing the library where the transformer is defined. |
| final String package; |
| |
| /// The `/`-separated path to the library that contains this transformer. |
| /// |
| /// This is relative to the `lib/` directory in [package], and doesn't end in |
| /// `.dart`. |
| /// |
| /// This can be null; if so, it indicates that the transformer(s) should be |
| /// loaded from `lib/transformer.dart` if that exists, and `lib/$package.dart` |
| /// otherwise. |
| final String path; |
| |
| /// The configuration to pass to the transformer. |
| /// |
| /// Any pub-specific configuration (i.e. keys starting with "$") will have |
| /// been stripped out of this and handled separately. This will be an empty |
| /// map if no configuration was provided. |
| final Map configuration; |
| |
| /// The primary input inclusions. |
| /// |
| /// Each inclusion is an asset path. If this set is non-empty, than *only* |
| /// matching assets are allowed as a primary input by this transformer. If |
| /// `null`, all assets are included. |
| /// |
| /// This is processed before [excludes]. If a transformer has both includes |
| /// and excludes, then the set of included assets is determined and assets |
| /// are excluded from that resulting set. |
| final Set<String> includes; |
| |
| /// The primary input exclusions. |
| /// |
| /// Any asset whose pach is in this is not allowed as a primary input by |
| /// this transformer. |
| /// |
| /// This is processed after [includes]. If a transformer has both includes |
| /// and excludes, then the set of included assets is determined and assets |
| /// are excluded from that resulting set. |
| final Set<String> excludes; |
| |
| /// Whether this ID points to a built-in transformer exposed by pub. |
| bool get isBuiltInTransformer => package.startsWith('\$'); |
| |
| /// Parses a transformer identifier. |
| /// |
| /// A transformer identifier is a string of the form "package_name" or |
| /// "package_name/path/to/library". It does not have a trailing extension. If |
| /// it just has a package name, it expands to lib/transformer.dart if that |
| /// exists, or lib/${package}.dart otherwise. Otherwise, it expands to |
| /// lib/${path}.dart. In either case it's located in the given package. |
| factory TransformerId.parse(String identifier, Map configuration) { |
| if (identifier.isEmpty) { |
| throw new FormatException('Invalid library identifier: "".'); |
| } |
| |
| var parts = split1(identifier, "/"); |
| if (parts.length == 1) { |
| return new TransformerId(parts.single, null, configuration); |
| } |
| |
| return new TransformerId(parts.first, parts.last, configuration); |
| } |
| |
| factory TransformerId(String package, String path, Map configuration) { |
| parseField(key) { |
| if (!configuration.containsKey(key)) return null; |
| var field = configuration.remove(key); |
| |
| if (field is String) return new Set<String>.from([field]); |
| |
| if (field is List) { |
| var nonstrings = field |
| .where((element) => element is! String) |
| .map((element) => '"$element"'); |
| |
| if (nonstrings.isNotEmpty) { |
| throw new FormatException( |
| '"$key" list field may only contain strings, but contained ' |
| '${toSentence(nonstrings)}.'); |
| } |
| |
| return new Set<String>.from(field); |
| } else { |
| throw new FormatException( |
| '"$key" field must be a string or list, but was "$field".'); |
| } |
| } |
| |
| var includes = null; |
| var excludes = null; |
| |
| if (configuration == null) { |
| configuration = {}; |
| } else { |
| // Pull out the exclusions/inclusions. |
| includes = parseField("\$include"); |
| excludes = parseField("\$exclude"); |
| |
| // All other keys starting with "$" are unexpected. |
| var reservedKeys = configuration.keys |
| .where((key) => key is String && key.startsWith(r'$')) |
| .map((key) => '"$key"'); |
| |
| if (reservedKeys.isNotEmpty) { |
| throw new FormatException( |
| 'Unknown reserved ${pluralize('field', reservedKeys.length)} ' |
| '${toSentence(reservedKeys)}.'); |
| } |
| } |
| |
| return new TransformerId._(package, path, configuration, |
| includes, excludes); |
| } |
| |
| TransformerId._(this.package, this.path, this.configuration, |
| this.includes, this.excludes) { |
| if (!package.startsWith('\$')) return; |
| if (_BUILT_IN_TRANSFORMERS.contains(package)) return; |
| throw new FormatException('Unsupported built-in transformer $package.'); |
| } |
| |
| // TODO(nweiz): support deep equality on [configuration] as well. |
| bool operator==(other) => other is TransformerId && |
| other.package == package && |
| other.path == path && |
| other.configuration == configuration; |
| |
| int get hashCode => package.hashCode ^ path.hashCode ^ configuration.hashCode; |
| |
| String toString() => path == null ? package : '$package/$path'; |
| |
| /// Returns the asset id for the library identified by this transformer id. |
| /// |
| /// If `path` is null, this will determine which library to load. |
| Future<AssetId> getAssetId(Barback barback) { |
| if (path != null) { |
| return new Future.value(new AssetId(package, 'lib/$path.dart')); |
| } |
| |
| var transformerAsset = new AssetId(package, 'lib/transformer.dart'); |
| return barback.getAssetById(transformerAsset).then((_) => transformerAsset) |
| .catchError((e) => new AssetId(package, 'lib/$package.dart'), |
| test: (e) => e is AssetNotFoundException); |
| } |
| } |
| |
| /// Converts [id] to a "package:" URI. |
| /// |
| /// This will throw an [ArgumentError] if [id] doesn't represent a library in |
| /// `lib/`. |
| Uri idToPackageUri(AssetId id) { |
| if (!id.path.startsWith('lib/')) { |
| throw new ArgumentError("Asset id $id doesn't identify a library."); |
| } |
| |
| return new Uri(scheme: 'package', path: id.path.replaceFirst('lib/', '')); |
| } |
| |
| /// Converts [uri] into an [AssetId] if its path is within "packages" or |
| /// "assets". |
| /// |
| /// While "packages" can appear anywhere in the path, "assets" is only allowed |
| /// as the top-level directory. (This throws a [FormatException] if "assets" |
| /// appears anywhere else.) |
| /// |
| /// If the URL contains a special directory, but lacks a following package name, |
| /// throws a [FormatException]. |
| /// |
| /// If the URI doesn't contain one of those special directories, returns null. |
| AssetId specialUrlToId(Uri url) { |
| var parts = path.url.split(url.path); |
| |
| // Strip the leading "/" from the URL. |
| if (parts.isNotEmpty && parts.first == "/") parts = parts.skip(1).toList(); |
| |
| if (parts.isEmpty) return null; |
| |
| // Check for "assets" at the beginning of the URL. |
| if (parts.first == "assets") { |
| if (parts.length <= 1) { |
| throw new FormatException( |
| 'Invalid URL path "${url.path}". Expected package name after ' |
| '"assets".'); |
| } |
| |
| var package = parts[1]; |
| var assetPath = path.url.join("asset", path.url.joinAll(parts.skip(2))); |
| return new AssetId(package, assetPath); |
| } |
| |
| // Check for "packages" anywhere in the URL. |
| // TODO(rnystrom): If we rewrite "package:" imports to relative imports that |
| // point to a canonical "packages" directory, we can limit "packages" to the |
| // root of the URL as well. See: #16649. |
| var index = parts.indexOf("packages"); |
| if (index == -1) return null; |
| |
| // There should be a package name after "packages". |
| if (parts.length <= index + 1) { |
| throw new FormatException( |
| 'Invalid URL path "${url.path}". Expected package name ' |
| 'after "packages".'); |
| } |
| |
| var package = parts[index + 1]; |
| var assetPath = path.url.join("lib", path.url.joinAll(parts.skip(index + 2))); |
| return new AssetId(package, assetPath); |
| } |