Add package implementation.
See dart-lang/test#327
R=rnystrom@google.com
Review URL: https://codereview.chromium.org//2132443003 .
diff --git a/lib/package_resolver.dart b/lib/package_resolver.dart
new file mode 100644
index 0000000..c0984d3
--- /dev/null
+++ b/lib/package_resolver.dart
@@ -0,0 +1,6 @@
+// Copyright (c) 2016, 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.
+
+export 'src/package_resolver.dart';
+export 'src/sync_package_resolver.dart';
diff --git a/lib/src/async_package_resolver.dart b/lib/src/async_package_resolver.dart
new file mode 100644
index 0000000..06c61e7
--- /dev/null
+++ b/lib/src/async_package_resolver.dart
@@ -0,0 +1,31 @@
+// Copyright (c) 2016, 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.
+
+import 'dart:async';
+
+import 'package_resolver.dart';
+import 'sync_package_resolver.dart';
+
+/// An implementation of [PackageResolver] that wraps a [SyncPackageResolver].
+class AsyncPackageResolver implements PackageResolver {
+ /// The wrapped [SyncPackageResolver].
+ final SyncPackageResolver _inner;
+
+ AsyncPackageResolver(this._inner);
+
+ Future<Map<String, Uri>> get packageConfigMap async =>
+ _inner.packageConfigMap;
+
+ Future<Uri> get packageConfigUri async => _inner.packageConfigUri;
+ Future<Uri> get packageRoot async => _inner.packageRoot;
+ Future<SyncPackageResolver> get asSync async => _inner;
+ Future<String> get processArgument async => _inner.processArgument;
+
+ Future<Uri> resolveUri(packageUri) async => _inner.resolveUri(packageUri);
+ Future<Uri> urlFor(String package, [String path]) async =>
+ _inner.urlFor(package, path);
+ Future<Uri> packageUriFor(url) async => _inner.packageUriFor(url);
+ Future<String> packagePath(String package) async =>
+ _inner.packagePath(package);
+}
diff --git a/lib/src/current_isolate_resolver.dart b/lib/src/current_isolate_resolver.dart
new file mode 100644
index 0000000..eb4dc9b
--- /dev/null
+++ b/lib/src/current_isolate_resolver.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2016, 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.
+
+import 'dart:async';
+import 'dart:isolate';
+
+import 'package:path/path.dart' as p;
+
+import 'package_config_resolver.dart';
+import 'package_resolver.dart';
+import 'package_root_resolver.dart';
+import 'sync_package_resolver.dart';
+import 'utils.dart';
+
+/// The package resolution strategy used by the current isolate.
+class CurrentIsolateResolver implements PackageResolver {
+ Future<Map<String, Uri>> get packageConfigMap async {
+ if (_packageConfigMap != null) return _packageConfigMap;
+
+ var url = await Isolate.packageConfig;
+ if (url == null) return null;
+
+ return await loadConfigMap(url);
+ }
+ Map<String, Uri> _packageConfigMap;
+
+ Future<Uri> get packageConfigUri => Isolate.packageConfig;
+
+ Future<Uri> get packageRoot => Isolate.packageRoot;
+
+ Future<SyncPackageResolver> get asSync async {
+ var root = await packageRoot;
+ if (root != null) return new PackageRootResolver(root);
+
+ var map = await packageConfigMap;
+
+ // It's hard to imagine how there would be no package resolution strategy
+ // for an Isolate that can load the package_resolver package, but it's easy
+ // to handle that case so we do.
+ if (map == null) return SyncPackageResolver.none;
+
+ return new PackageConfigResolver(map, uri: await packageConfigUri);
+ }
+
+ Future<String> get processArgument async {
+ var configUri = await packageConfigUri;
+ if (configUri != null) return "--packages=$configUri";
+
+ var root = await packageRoot;
+ if (root != null) return "--package-root=$root";
+
+ return null;
+ }
+
+ Future<Uri> resolveUri(packageUri) =>
+ Isolate.resolvePackageUri(asPackageUri(packageUri, "packageUri"));
+
+ Future<Uri> urlFor(String package, [String path]) =>
+ Isolate.resolvePackageUri(Uri.parse("package:$package/${path ?? ''}"));
+
+ Future<Uri> packageUriFor(url) async => (await asSync).packageUriFor(url);
+
+ Future<String> packagePath(String package) async {
+ var root = await packageRoot;
+ if (root != null) return new PackageRootResolver(root).packagePath(package);
+
+ return p.dirname(p.fromUri(await urlFor(package)));
+ }
+}
diff --git a/lib/src/no_package_resolver.dart b/lib/src/no_package_resolver.dart
new file mode 100644
index 0000000..9d324d4
--- /dev/null
+++ b/lib/src/no_package_resolver.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2016, 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.
+
+import 'async_package_resolver.dart';
+import 'package_resolver.dart';
+import 'sync_package_resolver.dart';
+import 'utils.dart';
+
+/// A package resolution strategy that is unable to resolve any `package:` URIs.
+class NoPackageResolver implements SyncPackageResolver {
+ Map<String, Uri> get packageConfigMap => null;
+ Uri get packageConfigUri => null;
+ Uri get packageRoot => null;
+ String get processArgument => null;
+
+ PackageResolver get asAsync => new AsyncPackageResolver(this);
+
+ Uri resolveUri(packageUri) {
+ // Verify that the URI is valid.
+ asPackageUri(packageUri, "packageUri");
+ return null;
+ }
+
+ Uri urlFor(String package, [String path]) => null;
+
+ Uri packageUriFor(url) {
+ // Verify that the URI is a valid type.
+ asUri(url, "url");
+ return null;
+ }
+
+ String packagePath(String package) => null;
+}
diff --git a/lib/src/package_config_resolver.dart b/lib/src/package_config_resolver.dart
new file mode 100644
index 0000000..defd683
--- /dev/null
+++ b/lib/src/package_config_resolver.dart
@@ -0,0 +1,94 @@
+// Copyright (c) 2016, 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.
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart';
+import 'package:package_config/packages_file.dart' as packages_file;
+import 'package:path/path.dart' as p;
+
+import 'async_package_resolver.dart';
+import 'package_resolver.dart';
+import 'sync_package_resolver.dart';
+import 'utils.dart';
+
+/// A package resolution strategy based on a package config map.
+class PackageConfigResolver implements SyncPackageResolver {
+ final packageRoot = null;
+
+ final Map<String, Uri> packageConfigMap;
+
+ Uri get packageConfigUri {
+ if (_uri != null) return _uri;
+
+ var buffer = new StringBuffer();
+ packages_file.write(buffer, packageConfigMap, comment: "");
+ _uri = new UriData.fromString(buffer.toString(),
+ parameters: {"charset": "utf-8"})
+ .uri;
+ return _uri;
+ }
+ Uri _uri;
+
+ PackageResolver get asAsync => new AsyncPackageResolver(this);
+
+ String get processArgument => "--packages=$packageConfigUri";
+
+ PackageConfigResolver(Map<String, Uri> packageConfigMap, {uri})
+ : packageConfigMap = _normalizeMap(packageConfigMap),
+ _uri = uri == null ? null : asUri(uri, "uri");
+
+ /// Normalizes the URIs in [map] to ensure that they all end in a trailing
+ /// slash.
+ static Map<String, Uri> _normalizeMap(Map<String, Uri> map) =>
+ new UnmodifiableMapView(
+ mapMap(map, value: (_, uri) => ensureTrailingSlash(uri)));
+
+ Uri resolveUri(packageUri) {
+ var uri = asPackageUri(packageUri, "packageUri");
+
+ var baseUri = packageConfigMap[uri.pathSegments.first];
+ if (baseUri == null) return null;
+
+ var segments = baseUri.pathSegments.toList()
+ ..removeLast(); // Remove the trailing slash.
+
+ // Following [Isolate.resolvePackageUri], "package:foo" resolves to null.
+ if (uri.pathSegments.length == 1) return null;
+
+ segments.addAll(uri.pathSegments.skip(1));
+ return baseUri.replace(pathSegments: segments);
+ }
+
+ Uri urlFor(String package, [String path]) {
+ var baseUri = packageConfigMap[package];
+ if (baseUri == null) return null;
+ if (path == null) return baseUri;
+ return baseUri.resolve(path);
+ }
+
+ Uri packageUriFor(url) {
+ url = asUri(url, "url").toString();
+
+ // Make sure isWithin works if [url] is exactly the base.
+ var nested = p.url.join(url, "_");
+ for (var package in packageConfigMap.keys) {
+ var base = packageConfigMap[package].toString();
+ if (!p.url.isWithin(base, nested)) continue;
+
+ var relative = p.url.relative(url, from: base);
+ if (relative == '.') relative = '';
+ return Uri.parse("package:$package/$relative");
+ }
+
+ return null;
+ }
+
+ String packagePath(String package) {
+ var lib = packageConfigMap[package];
+ if (lib == null) return null;
+ if (lib.scheme != 'file') return null;
+ return p.dirname(p.fromUri(lib));
+ }
+}
diff --git a/lib/src/package_resolver.dart b/lib/src/package_resolver.dart
new file mode 100644
index 0000000..c72fc9d
--- /dev/null
+++ b/lib/src/package_resolver.dart
@@ -0,0 +1,161 @@
+// Copyright (c) 2016, 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.
+
+import 'dart:async';
+
+import 'package:http/http.dart' as http;
+
+import 'current_isolate_resolver.dart';
+import 'package_config_resolver.dart';
+import 'package_root_resolver.dart';
+import 'sync_package_resolver.dart';
+
+/// A class that defines how to resolve `package:` URIs.
+///
+/// This includes the information necessary to resolve `package:` URIs using
+/// either a package config or a package root. It can be used both as a standard
+/// cross-package representation of the user's configuration, and as a means of
+/// concretely locating packages and the assets they contain.
+///
+/// Unlike [SyncPackageResolver], this does not provide synchronous APIs. This
+/// is necessary when dealing with the current Isolate's package resolution
+/// strategy.
+///
+/// This class should not be implemented by user code.
+abstract class PackageResolver {
+ /// The map contained in the parsed package config.
+ ///
+ /// This maps package names to the base URIs for those packages. These are
+ /// already resolved relative to [packageConfigUri], so if they're relative
+ /// they should be considered relative to [Uri.base].
+ ///
+ /// [urlFor] should generally be used rather than looking up package URLs in
+ /// this map, to ensure that code works with a package root as well as a
+ /// package config.
+ ///
+ /// Note that for some implementations, loading the map may require IO
+ /// operations that could fail.
+ ///
+ /// Completes to `null` when using a [packageRoot] for resolution, or when no
+ /// package resolution is being used.
+ Future<Map<String, Uri>> get packageConfigMap;
+
+ /// The URI for the package config.
+ ///
+ /// This is the URI from which [packageConfigMap] was parsed, if that's
+ /// available. Otherwise, it's a `data:` URI containing a serialized
+ /// representation of [packageConfigMap]. This `data:` URI should be accepted
+ /// by all Dart tools.
+ ///
+ /// Note that if this is a `data:` URI, it may not be safe to pass as a
+ /// parameter to a Dart process due to length limits.
+ ///
+ /// Completes to `null` when using a [packageRoot] for resolution, or when no
+ /// package resolution is being used.
+ Future<Uri> get packageConfigUri;
+
+ /// The base URL for resolving `package:` URLs.
+ ///
+ /// Completes to `null` when using a [packageConfigMap] for resolution, or
+ /// when no package resolution is being used.
+ Future<Uri> get packageRoot;
+
+ /// Fetches the package resolution for [this] and returns an object that
+ /// provides synchronous access.
+ ///
+ /// This may throw exceptions if loading or parsing the package map fails.
+ Future<SyncPackageResolver> get asSync;
+
+ /// Returns the argument to pass to a subprocess to get it to use this package
+ /// resolution strategy when resolving `package:` URIs.
+ ///
+ /// This uses the `--package-root` or `--package` flags, which are the
+ /// conventions supported by the Dart VM and dart2js.
+ ///
+ /// Note that if [packageConfigUri] is a `data:` URI, it may be too large to
+ /// pass on the command line.
+ ///
+ /// Returns `null` if no package resolution is in use.
+ Future<String> get processArgument;
+
+ /// Returns package resolution strategy describing how the current isolate
+ /// resolves `package:` URIs.
+ static final PackageResolver current = new CurrentIsolateResolver();
+
+ /// Returns a package resolution strategy that is unable to resolve any
+ /// `package:` URIs.
+ static final PackageResolver none = SyncPackageResolver.none.asAsync;
+
+ /// Loads a package config file from [uri] and returns its package resolution
+ /// strategy.
+ ///
+ /// This supports `file:`, `http:`, `data:` and `package:` URIs. It throws an
+ /// [UnsupportedError] for any other schemes. If [client] is passed and an
+ /// HTTP request is needed, it's used to make that request; otherwise, a
+ /// default client is used.
+ ///
+ /// [uri] may be a [String] or a [Uri].
+ static Future<PackageResolver> loadConfig(uri, {http.Client client}) async {
+ var resolver = await SyncPackageResolver.loadConfig(uri, client: client);
+ return resolver.asAsync;
+ }
+
+ /// Returns the package resolution strategy for the given [packageConfigMap].
+ ///
+ /// If passed, [uri] specifies the URI from which [packageConfigMap] was
+ /// loaded. It may be a [String] or a [Uri].
+ ///
+ /// Whether or not [uri] is passed, [packageConfigMap] is expected to be
+ /// fully-resolved. That is, any relative URIs in the original package config
+ /// source should be resolved relative to its location.
+ factory PackageResolver.config(Map<String, Uri> packageConfigMap, {uri}) =>
+ new PackageConfigResolver(packageConfigMap, uri: uri).asAsync;
+
+ /// Returns the package resolution strategy for the given [packageRoot], which
+ /// may be a [String] or a [Uri].
+ factory PackageResolver.root(packageRoot) =>
+ new PackageRootResolver(packageRoot).asAsync;
+
+ /// Resolves [packageUri] according to this package resolution strategy.
+ ///
+ /// [packageUri] may be a [String] or a [Uri]. This throws a [FormatException]
+ /// if [packageUri] isn't a `package:` URI or doesn't have at least one path
+ /// segment.
+ ///
+ /// If [packageUri] refers to a package that's not in the package spec, this
+ /// returns `null`.
+ Future<Uri> resolveUri(packageUri);
+
+ /// Returns the resolved URL for [package] and [path].
+ ///
+ /// This is equivalent to `resolveUri("package:$package/")` or
+ /// `resolveUri("package:$package/$path")`, depending on whether [path] was
+ /// passed.
+ ///
+ /// If [package] refers to a package that's not in the package spec, this
+ /// returns `null`.
+ Future<Uri> urlFor(String package, [String path]);
+
+ /// Returns the `package:` URI for [uri].
+ ///
+ /// If [uri] can't be referred to using a `package:` URI, returns `null`.
+ ///
+ /// [uri] may be a [String] or a [Uri].
+ Future<Uri> packageUriFor(uri);
+
+ /// Returns the path on the local filesystem to the root of [package], or
+ /// `null` if the root cannot be found.
+ ///
+ /// **Note**: this assumes a pub-style package layout. In particular:
+ ///
+ /// * If a package root is being used, this assumes that it contains symlinks
+ /// to packages' lib/ directories.
+ ///
+ /// * If a package config is being used, this assumes that each entry points
+ /// to a package's lib/ directory.
+ ///
+ /// Returns `null` if the package root is not a `file:` URI, or if the package
+ /// config entry for [package] is not a `file:` URI.
+ Future<String> packagePath(String package);
+}
diff --git a/lib/src/package_root_resolver.dart b/lib/src/package_root_resolver.dart
new file mode 100644
index 0000000..c9a06ff
--- /dev/null
+++ b/lib/src/package_root_resolver.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2016, 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.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import 'async_package_resolver.dart';
+import 'package_resolver.dart';
+import 'sync_package_resolver.dart';
+import 'utils.dart';
+
+/// A package resolution strategy based on a package root URI.
+class PackageRootResolver implements SyncPackageResolver {
+ final packageConfigMap = null;
+ final packageConfigUri = null;
+
+ final Uri packageRoot;
+
+ PackageResolver get asAsync => new AsyncPackageResolver(this);
+
+ String get processArgument => "--package-root=$packageRoot";
+
+ PackageRootResolver(packageRoot)
+ : packageRoot = ensureTrailingSlash(asUri(packageRoot, "packageRoot"));
+
+ Uri resolveUri(packageUri) {
+ packageUri = asPackageUri(packageUri, "packageUri");
+
+ // Following [Isolate.resolvePackageUri], "package:foo" resolves to null.
+ if (packageUri.pathSegments.length == 1) return null;
+ return packageRoot.resolve(packageUri.path);
+ }
+
+ Uri urlFor(String package, [String path]) {
+ var result = packageRoot.resolve("$package/");
+ return path == null ? result : result.resolve(path);
+ }
+
+ Uri packageUriFor(url) {
+ var packageRootString = packageRoot.toString();
+ url = asUri(url, "url").toString();
+ if (!p.url.isWithin(packageRootString, url)) return null;
+
+ var relative = p.url.relative(url, from: packageRootString);
+ if (!relative.contains("/")) relative += "/";
+ return Uri.parse("package:$relative");
+ }
+
+ String packagePath(String package) {
+ if (packageRoot.scheme != 'file') return null;
+
+ var libLink = p.join(p.fromUri(packageRoot), package);
+ if (!new Link(libLink).existsSync()) return null;
+
+ return p.dirname(new Link(libLink).resolveSymbolicLinksSync());
+ }
+}
diff --git a/lib/src/sync_package_resolver.dart b/lib/src/sync_package_resolver.dart
new file mode 100644
index 0000000..1c71dfd
--- /dev/null
+++ b/lib/src/sync_package_resolver.dart
@@ -0,0 +1,170 @@
+// Copyright (c) 2016, 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.
+
+import 'dart:async';
+
+import 'package:http/http.dart' as http;
+
+import 'no_package_resolver.dart';
+import 'package_config_resolver.dart';
+import 'package_resolver.dart';
+import 'package_root_resolver.dart';
+import 'utils.dart';
+
+/// A class that defines how to resolve `package:` URIs.
+///
+/// This includes the information necessary to resolve `package:` URIs using
+/// either a package config or a package root. It can be used both as a standard
+/// cross-package representation of the user's configuration, and as a means of
+/// concretely locating packages and the assets they contain.
+///
+/// Unlike [PackageResolver], all members on this are synchronous, which may
+/// require that more data be loaded up front. This is useful when primarily
+/// dealing with user-created package resolution strategies, whereas
+/// [PackageInfo] is usually preferable when the current Isolate's package
+/// resolution logic may be used.
+///
+/// This class should not be implemented by user code.
+abstract class SyncPackageResolver {
+ /// The map contained in the parsed package config.
+ ///
+ /// This maps package names to the base URIs for those packages. These are
+ /// already resolved relative to [packageConfigUri], so if they're relative
+ /// they should be considered relative to [Uri.base]. They're normalized to
+ /// ensure that all URLs end with a trailing slash.
+ ///
+ /// [urlFor] should generally be used rather than looking up package URLs in
+ /// this map, to ensure that code works with a package root as well as a
+ /// package config.
+ ///
+ /// Returns `null` when using a [packageRoot] for resolution, or when no
+ /// package resolution is being used.
+ Map<String, Uri> get packageConfigMap;
+
+ /// The URI for the package config.
+ ///
+ /// This is the URI from which [packageConfigMap] was parsed, if that's
+ /// available. Otherwise, it's a `data:` URI containing a serialized
+ /// representation of [packageConfigMap]. This `data:` URI should be accepted
+ /// by all Dart tools.
+ ///
+ /// Note that if this is a `data:` URI, it's likely not safe to pass as a
+ /// parameter to a Dart process due to length limits.
+ ///
+ /// Returns `null` when using a [packageRoot] for resolution, or when no
+ /// package resolution is being used.
+ Uri get packageConfigUri;
+
+ /// The base URL for resolving `package:` URLs.
+ ///
+ /// This is normalized so that it always ends with a trailing slash.
+ ///
+ /// Returns `null` when using a [packageConfigMap] for resolution, or when no
+ /// package resolution is being used.
+ Uri get packageRoot;
+
+ /// Returns a wrapper for [this] that implements [PackageResolver].
+ PackageResolver get asAsync;
+
+ /// Returns the argument to pass to a subprocess to get it to use this package
+ /// resolution strategy when resolving `package:` URIs.
+ ///
+ /// This uses the `--package-root` or `--package` flags, which are the
+ /// convention supported by the Dart VM and dart2js.
+ ///
+ /// Note that if [packageConfigUri] is a `data:` URI, it may be too large to
+ /// pass on the command line.
+ ///
+ /// Returns `null` if no package resolution is in use.
+ String get processArgument;
+
+ /// Returns a package resolution strategy describing how the current isolate
+ /// resolves `package:` URIs.
+ ///
+ /// This may throw exceptions if loading or parsing the isolate's package map
+ /// fails.
+ static final Future<SyncPackageResolver> current =
+ PackageResolver.current.asSync;
+
+ /// Returns a package resolution strategy that is unable to resolve any
+ /// `package:` URIs.
+ static final SyncPackageResolver none = new NoPackageResolver();
+
+ /// Loads a package config file from [uri] and returns its package resolution
+ /// strategy.
+ ///
+ /// This supports `file:`, `http:`, `data:` and `package:` URIs. It throws an
+ /// [UnsupportedError] for any other schemes. If [client] is passed and an
+ /// HTTP request is needed, it's used to make that request; otherwise, a
+ /// default client is used.
+ ///
+ /// [uri] may be a [String] or a [Uri].
+ static Future<SyncPackageResolver> loadConfig(uri, {http.Client client})
+ async {
+ uri = asUri(uri, "uri");
+ return new SyncPackageResolver.config(
+ await loadConfigMap(uri, client: client),
+ uri: uri);
+ }
+
+ /// Returns the package resolution strategy for the given [packageConfigMap].
+ ///
+ /// If passed, [uri] specifies the URI from which [packageConfigMap] was
+ /// loaded. It may be a [String] or a [Uri].
+ ///
+ /// Whether or not [uri] is passed, [packageConfigMap] is expected to be
+ /// fully-resolved. That is, any relative URIs in the original package config
+ /// source should be resolved relative to its location.
+ factory SyncPackageResolver.config(Map<String, Uri> packageConfigMap, {uri}) =
+ PackageConfigResolver;
+
+ /// Returns the package resolution strategy for the given [packageRoot], which
+ /// may be a [String] or a [Uri].
+ factory SyncPackageResolver.root(packageRoot) = PackageRootResolver;
+
+ /// Resolves [packageUri] according to this package resolution strategy.
+ ///
+ /// [packageUri] may be a [String] or a [Uri]. This throws a [FormatException]
+ /// if [packageUri] isn't a `package:` URI or doesn't have at least one path
+ /// segment.
+ ///
+ /// If [packageUri] refers to a package that's not in the package spec, this
+ /// returns `null`.
+ Uri resolveUri(packageUri);
+
+ /// Returns the resolved URL for [package] and [path].
+ ///
+ /// This is equivalent to `resolveUri("package:$package/")` or
+ /// `resolveUri("package:$package/$path")`, depending on whether [path] was
+ /// passed.
+ ///
+ /// If [package] refers to a package that's not in the package spec, this
+ /// returns `null`.
+ Uri urlFor(String package, [String path]);
+
+ /// Returns the `package:` URI for [url].
+ ///
+ /// If [url] can't be referred to using a `package:` URI, returns `null`.
+ ///
+ /// [url] may be a [String] or a [Uri].
+ Uri packageUriFor(url);
+
+ /// Returns the path on the local filesystem to the root of [package], or
+ /// `null` if the root cannot be found.
+ ///
+ /// **Note**: this assumes a pub-style package layout. In particular:
+ ///
+ /// * If a package root is being used, this assumes that it contains symlinks
+ /// to packages' lib/ directories.
+ ///
+ /// * If a package config is being used, this assumes that each entry points
+ /// to a package's lib/ directory.
+ ///
+ /// If these assumptions are broken, this may return `null` or it may return
+ /// an invalid result.
+ ///
+ /// Returns `null` if the package root is not a `file:` URI, or if the package
+ /// config entry for [package] is not a `file:` URI.
+ String packagePath(String package);
+}
diff --git a/lib/src/test_package_config b/lib/src/test_package_config
new file mode 100644
index 0000000..31aaa5e
--- /dev/null
+++ b/lib/src/test_package_config
@@ -0,0 +1,3 @@
+# This needs to be in src/ so the tests can access it using a package: URI.
+foo:file:///foo/bar/
+bar:http://dartlang.org/bar/
\ No newline at end of file
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..77de03d
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2016, 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.
+
+// TODO(nweiz): Avoid importing dart:io directly when cross-platform libraries
+// exist.
+import 'dart:io';
+import 'dart:isolate';
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+import 'package:package_config/packages_file.dart' as packages_file;
+
+/// Loads the configuration map from [uri].
+///
+/// This supports `http`, `file`, `data`, and `package` URIs. If [client] is
+/// passed and an HTTP request is needed, it's used to make that request;
+/// otherwise, a default client is used.
+Future<Map<String, Uri>> loadConfigMap(Uri uri, {http.Client client}) async {
+ var resolved = Uri.base.resolveUri(uri);
+
+ var text;
+ if (resolved.scheme == 'http') {
+ text = await (client == null
+ ? http.read(resolved)
+ : client.read(resolved));
+ } else if (resolved.scheme == 'file') {
+ var path = resolved.toFilePath(windows: Platform.isWindows);
+ text = await new File(path).readAsString();
+ } else if (resolved.scheme == 'data') {
+ text = resolved.data.contentAsString();
+ } else if (resolved.scheme == 'package') {
+ return loadConfigMap(await Isolate.resolvePackageUri(uri),
+ client: client);
+ } else {
+ throw new UnsupportedError(
+ 'PackageInfo.loadConfig doesn\'t support URI scheme "${uri.scheme}:".');
+ }
+
+ return packages_file.parse(UTF8.encode(text), resolved);
+}
+
+/// Converts [uri] to a [Uri] and verifies that it's a valid `package:` URI.
+///
+/// Throws an [ArgumentError] if [uri] isn't a [String] or a [Uri]. [name] is
+/// used as the argument name in that error.
+///
+/// Throws a [FormatException] if [uri] isn't a `package:` URI or doesn't have
+/// at least one path segment.
+Uri asPackageUri(uri, String name) {
+ uri = asUri(uri, name);
+
+ if (uri.scheme != 'package') {
+ throw new FormatException("Can only resolve a package: URI.",
+ uri.toString(), 0);
+ } else if (uri.pathSegments.isEmpty) {
+ throw new FormatException("Expected package name.",
+ uri.toString(), "package:".length);
+ }
+
+ return uri;
+}
+
+/// Converts [uri] to a [Uri].
+///
+/// Throws an [ArgumentError] if [uri] isn't a [String] or a [Uri]. [name] is
+/// used as the argument name in that error.
+Uri asUri(uri, String name) {
+ if (uri is Uri) return uri;
+ if (uri is String) return Uri.parse(uri);
+
+ throw new ArgumentError.value(uri, name, "Must be a String or a Uri.");
+}
+
+/// Returns a copy of [uri] with a trailing slash.
+///
+/// If [uri] already ends in a slash, returns it as-is.
+Uri ensureTrailingSlash(Uri uri) {
+ if (uri.pathSegments.last.isEmpty) return uri;
+ return uri.replace(pathSegments: uri.pathSegments.toList()..add(""));
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 751eb44..d33cd97 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,5 +7,11 @@
environment:
sdk: '>=1.14.0 <2.0.0'
+dependencies:
+ collection: '^1.9.0'
+ http: '^0.11.0'
+ package_config: '>=0.1.0 <2.0.0'
+ path: '^1.0.0'
dev_dependencies:
+ shelf: '^0.6.0'
test: '^0.12.0'
diff --git a/test/current_isolate_info_test.dart b/test/current_isolate_info_test.dart
new file mode 100644
index 0000000..bb6f4f7
--- /dev/null
+++ b/test/current_isolate_info_test.dart
@@ -0,0 +1,233 @@
+// Copyright (c) 2016, 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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'package:package_resolver/package_resolver.dart';
+
+void main() {
+ // It's important to test these, because they use PackageConfig.current and
+ // they're used to verify the output of the inner isolate's
+ // PackageConfig.current.
+ test("_packageResolverLibUri is correct", () async {
+ var libPath = p.fromUri(await _packageResolverLibUri);
+ expect(new File(p.join(libPath, 'package_resolver.dart')).exists(),
+ completion(isTrue));
+ });
+
+ test("_pathLibUri is correct", () async {
+ var libPath = p.fromUri(await _pathLibUri);
+ expect(new File(p.join(libPath, 'path.dart')).exists(), completion(isTrue));
+ });
+
+ group("with a package config", () {
+ var resolver;
+ setUp(() async {
+ var map;
+ var currentIsolateMap = await PackageResolver.current.packageConfigMap;
+ if (currentIsolateMap != null) {
+ map = new Map.from(currentIsolateMap);
+ } else {
+ // If the isolate running this test isn't using package config, create
+ // one from scratch with the same resolution semantics.
+ var map = {};
+ var root = p.fromUri(await PackageResolver.current.packageRoot);
+ await for (var link in new Directory(root).list(followLinks: false)) {
+ assert(link is Link);
+ map[p.basename(link.path)] =
+ p.toUri(await link.resolveSymbolicLinks());
+ }
+ }
+
+ // Ensure that we have at least one URI that ends with "/" and one that
+ // doesn't. Both of these cases need to be tested.
+ expect(map["package_resolver"].path, endsWith("/"));
+ map["path"] = Uri.parse(p.url.normalize(map["path"].toString()));
+ expect(map["path"].path, isNot(endsWith("/")));
+
+ resolver = new PackageResolver.config(map);
+ });
+
+ test("exposes the config map", () async {
+ expect(await _spawn("""() async {
+ var serializable = {};
+ (await PackageResolver.current.packageConfigMap)
+ .forEach((package, uri) {
+ serializable[package] = uri.toString();
+ });
+ return serializable;
+ }()""", resolver),
+ containsPair("package_resolver", await _packageResolverLibUri));
+ });
+
+ test("exposes the config URI", () async {
+ expect(
+ await _spawn(
+ "(await PackageResolver.current.packageConfigUri).toString()",
+ resolver),
+ equals((await resolver.packageConfigUri).toString()));
+ });
+
+ test("exposes a null package root", () async {
+ expect(
+ // Use "== null" because if it *is* a URI, it'll crash the isolate
+ // when we try to send it over the port.
+ await _spawn(
+ "(await PackageResolver.current.packageRoot) == null", resolver),
+ isTrue);
+ });
+
+ test("processArgument uses --packages", () async {
+ expect(
+ await _spawn("PackageResolver.current.processArgument", resolver),
+ equals(await resolver.processArgument));
+ });
+
+ group("resolveUri", () {
+ test("with a matching package", () async {
+ expect(await _spawn("""() async {
+ var uri = await PackageResolver.current.resolveUri(
+ 'package:package_resolver/foo/bar.dart');
+ return uri.toString();
+ }()""", resolver),
+ equals(p.url.join(await _packageResolverLibUri, "foo/bar.dart")));
+
+ expect(await _spawn("""() async {
+ var uri = await PackageResolver.current.resolveUri(
+ 'package:path/foo/bar.dart');
+ return uri.toString();
+ }()""", resolver),
+ equals(p.url.join(await _pathLibUri, "foo/bar.dart")));
+ });
+
+ test("with a matching package with no path", () async {
+ expect(await _spawn("""() async {
+ var uri = await PackageResolver.current.resolveUri(
+ 'package:package_resolver');
+ return uri == null;
+ }()""", resolver), isTrue);
+
+ expect(await _spawn("""() async {
+ var uri = await PackageResolver.current.resolveUri('package:path');
+ return uri == null;
+ }()""", resolver), isTrue);
+ });
+
+ test("with a matching package with an empty path",
+ () async {
+ expect(await _spawn("""() async {
+ var uri = await PackageResolver.current.resolveUri(
+ 'package:package_resolver/');
+ return uri.toString();
+ }()""", resolver), (await _packageResolverLibUri).toString());
+
+ expect(await _spawn("""() async {
+ var uri = await PackageResolver.current.resolveUri('package:path/');
+ return uri.toString();
+ }()""", resolver), (await _pathLibUri).toString());
+ });
+
+ test("with a URI object", () async {
+ expect(await _spawn("""() async {
+ var uri = await PackageResolver.current.resolveUri(
+ Uri.parse('package:package_resolver/foo/bar.dart'));
+ return uri.toString();
+ }()""", resolver),
+ equals(p.url.join(await _packageResolverLibUri, 'foo/bar.dart')));
+ });
+
+ test("with a non-matching package", () async {
+ expect(await _spawn("""() async {
+ var uri = await PackageResolver.current.resolveUri(
+ Uri.parse('package:not-a-package/foo/bar.dart'));
+ return uri == null;
+ }()""", resolver), isTrue);
+ });
+
+ test("with an invalid argument type", () async {
+ expect(await _spawn("""() async {
+ try {
+ await PackageResolver.current.resolveUri(12);
+ return false;
+ } on ArgumentError catch (_) {
+ return true;
+ }
+ }()""", resolver), isTrue);
+ });
+
+ test("with a non-package URI", () async {
+ expect(await _spawn("""() async {
+ try {
+ await PackageResolver.current.resolveUri('file:///zip/zap');
+ return false;
+ } on FormatException catch (_) {
+ return true;
+ }
+ }()""", resolver), isTrue);
+ });
+
+ test("with an invalid package URI", () async {
+ expect(await _spawn("""() async {
+ try {
+ await PackageResolver.current.resolveUri("package:");
+ return false;
+ } on FormatException catch (_) {
+ return true;
+ }
+ }()""", resolver), isTrue);
+ });
+ });
+ });
+}
+
+Future<String> get _packageResolverLibUri async =>
+ (await PackageResolver.current.urlFor("package_resolver")).toString();
+
+Future<String> get _pathLibUri async =>
+ (await PackageResolver.current.urlFor("path")).toString();
+
+Future _spawn(String expression, PackageResolver packageResolver) async {
+ var data = new UriData.fromString("""
+ import 'dart:convert';
+ import 'dart:isolate';
+
+ import 'package:package_resolver/package_resolver.dart';
+
+ main(_, SendPort message) async {
+ message.send(await ($expression));
+ }
+ """, mimeType: "application/dart", parameters: {"charset": "utf-8"});
+
+ var receivePort = new ReceivePort();
+ var errorPort = new ReceivePort();
+ try {
+ var isolate = await Isolate.spawnUri(data.uri, [], receivePort.sendPort,
+ packageRoot: await packageResolver.packageRoot,
+ packageConfig: await packageResolver.packageConfigUri,
+ paused: true);
+
+ isolate.addErrorListener(errorPort.sendPort);
+ errorPort.listen((message) {
+ registerException(message[0],
+ message[1] == null ? null : new Trace.parse(message[1]));
+ errorPort.close();
+ receivePort.close();
+ });
+ isolate.resume(isolate.pauseCapability);
+
+ var value = await receivePort.first;
+ isolate.kill();
+ return value;
+ } finally {
+ errorPort.close();
+ receivePort.close();
+ }
+}
diff --git a/test/no_package_info_test.dart b/test/no_package_info_test.dart
new file mode 100644
index 0000000..fddb011
--- /dev/null
+++ b/test/no_package_info_test.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2016, 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.
+
+import 'package:test/test.dart';
+
+import 'package:package_resolver/package_resolver.dart';
+
+void main() {
+ var resolver;
+ setUp(() {
+ resolver = SyncPackageResolver.none;
+ });
+
+ test("exposes everything as null", () {
+ expect(resolver.packageConfigMap, isNull);
+ expect(resolver.packageConfigUri, isNull);
+ expect(resolver.packageRoot, isNull);
+ expect(resolver.processArgument, isNull);
+ expect(resolver.resolveUri("package:foo/bar.dart"), isNull);
+ expect(resolver.urlFor("foo"), isNull);
+ expect(resolver.urlFor("foo", "bar.dart"), isNull);
+ expect(resolver.packageUriFor("file:///foo/bar.dart"), isNull);
+ expect(resolver.packagePath("foo"), isNull);
+ });
+
+ group("resolveUri", () {
+ test("with an invalid argument type", () {
+ expect(() => resolver.resolveUri(12), throwsArgumentError);
+ });
+
+ test("with a non-package URI", () {
+ expect(() => resolver.resolveUri("file:///zip/zap"),
+ throwsFormatException);
+ });
+
+ test("with an invalid package URI", () {
+ expect(() => resolver.resolveUri("package:"), throwsFormatException);
+ });
+ });
+
+ test("packageUriFor with an invalid argument type", () {
+ expect(() => resolver.packageUriFor(12), throwsArgumentError);
+ });
+}
diff --git a/test/package_config_info_test.dart b/test/package_config_info_test.dart
new file mode 100644
index 0000000..5dcc417
--- /dev/null
+++ b/test/package_config_info_test.dart
@@ -0,0 +1,249 @@
+// Copyright (c) 2016, 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.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:test/test.dart';
+
+import 'package:package_resolver/package_resolver.dart';
+
+void main() {
+ var resolver;
+ setUp(() {
+ resolver = new SyncPackageResolver.config({
+ "foo": Uri.parse("file:///foo/bar/"),
+ "bar": Uri.parse("http://dartlang.org/bar")
+ }, uri: "file:///myapp/.packages");
+ });
+
+ group("constructor", () {
+ test("with a URI object", () {
+ var resolver = new SyncPackageResolver.config({},
+ uri: Uri.parse("file:///myapp/.packages"));
+ expect(resolver.packageConfigUri,
+ equals(Uri.parse("file:///myapp/.packages")));
+ });
+
+ test("with an invalid URI type", () {
+ expect(() => new SyncPackageResolver.config({}, uri: 12),
+ throwsArgumentError);
+ });
+ });
+
+ test("exposes the config map", () {
+ expect(resolver.packageConfigMap, equals({
+ "foo": Uri.parse("file:///foo/bar/"),
+ "bar": Uri.parse("http://dartlang.org/bar/")
+ }));
+ });
+
+ test("exposes the config URI if passed", () {
+ expect(resolver.packageConfigUri,
+ equals(Uri.parse("file:///myapp/.packages")));
+ });
+
+ test("exposes a data: config URI if none is passed", () {
+ resolver = new SyncPackageResolver.config(resolver.packageConfigMap);
+ expect(resolver.packageConfigUri, equals(Uri.parse(
+ "data:;charset=utf-8,"
+ "foo:file:///foo/bar/%0A"
+ "bar:http://dartlang.org/bar/%0A")));
+ });
+
+ test("exposes a null root", () {
+ expect(resolver.packageRoot, isNull);
+ });
+
+ test("processArgument uses --packages", () {
+ expect(resolver.processArgument,
+ equals("--packages=file:///myapp/.packages"));
+ });
+
+ group("resolveUri", () {
+ test("with a matching package", () {
+ expect(resolver.resolveUri("package:foo/bang/qux.dart"),
+ equals(Uri.parse("file:///foo/bar/bang/qux.dart")));
+ expect(resolver.resolveUri("package:bar/bang/qux.dart"),
+ equals(Uri.parse("http://dartlang.org/bar/bang/qux.dart")));
+ });
+
+ test("with a matching package with no path", () {
+ expect(resolver.resolveUri("package:foo"), isNull);
+ });
+
+ test("with a matching package with an empty path", () {
+ expect(resolver.resolveUri("package:bar/"),
+ equals(Uri.parse("http://dartlang.org/bar/")));
+ });
+
+ test("with a URI object", () {
+ expect(resolver.resolveUri(Uri.parse("package:foo/bang/qux.dart")),
+ equals(Uri.parse("file:///foo/bar/bang/qux.dart")));
+ });
+
+ test("with a non-matching package", () {
+ expect(resolver.resolveUri("package:zap/bang/qux.dart"), isNull);
+ });
+
+ test("with an invalid argument type", () {
+ expect(() => resolver.resolveUri(12), throwsArgumentError);
+ });
+
+ test("with a non-package URI", () {
+ expect(() => resolver.resolveUri("file:///zip/zap"),
+ throwsFormatException);
+ });
+
+ test("with an invalid package URI", () {
+ expect(() => resolver.resolveUri("package:"), throwsFormatException);
+ });
+ });
+
+ group("urlFor", () {
+ test("with a matching package and no path", () {
+ expect(resolver.urlFor("foo"), equals(Uri.parse("file:///foo/bar/")));
+ expect(resolver.urlFor("bar"),
+ equals(Uri.parse("http://dartlang.org/bar/")));
+ });
+
+ test("with a matching package and a path", () {
+ expect(resolver.urlFor("foo", "bang/qux.dart"),
+ equals(Uri.parse("file:///foo/bar/bang/qux.dart")));
+ expect(resolver.urlFor("bar", "bang/qux.dart"),
+ equals(Uri.parse("http://dartlang.org/bar/bang/qux.dart")));
+ });
+
+ test("with a non-matching package and no path", () {
+ expect(resolver.urlFor("zap"), isNull);
+ });
+ });
+
+ group("packageUriFor", () {
+ test("converts matching URIs to package:", () {
+ expect(resolver.packageUriFor("file:///foo/bar/bang/qux.dart"),
+ equals(Uri.parse("package:foo/bang/qux.dart")));
+ expect(resolver.packageUriFor("http://dartlang.org/bar/bang/qux.dart"),
+ equals(Uri.parse("package:bar/bang/qux.dart")));
+ });
+
+ test("converts URIs with no paths", () {
+ expect(resolver.packageUriFor("file:///foo/bar"),
+ equals(Uri.parse("package:foo/")));
+ expect(resolver.packageUriFor("http://dartlang.org/bar/"),
+ equals(Uri.parse("package:bar/")));
+ });
+
+ test("with a URI object", () {
+ expect(resolver.packageUriFor(Uri.parse("file:///foo/bar/bang/qux.dart")),
+ equals(Uri.parse("package:foo/bang/qux.dart")));
+ });
+
+ test("with an invalid argument type", () {
+ expect(() => resolver.packageUriFor(12), throwsArgumentError);
+ });
+ });
+
+ group("packagePath", () {
+ setUp(() {
+ resolver = new SyncPackageResolver.config({
+ "foo": p.toUri(p.join(p.current, 'lib')),
+ "bar": Uri.parse("http://dartlang.org/bar")
+ });
+ });
+
+ test("with a matching package", () {
+ expect(resolver.packagePath("foo"), equals(p.current));
+ });
+
+ test("with a package with a non-file scheme", () {
+ expect(resolver.packagePath("bar"), isNull);
+ });
+
+ test("with a non-matching", () {
+ expect(resolver.packagePath("baz"), isNull);
+ });
+ });
+
+ group("loadConfig", () {
+ var server;
+ var sandbox;
+ setUp(() async {
+ sandbox = (await Directory.systemTemp.createTemp("package_resolver_test"))
+ .path;
+ });
+
+ tearDown(() async {
+ if (server != null) await server.close();
+ await new Directory(sandbox).delete(recursive: true);
+ });
+
+ test("with an http: URI", () async {
+ server = await shelf_io.serve((request) {
+ return new shelf.Response.ok(
+ "foo:file:///foo/bar/\n"
+ "bar:http://dartlang.org/bar/");
+ }, 'localhost', 0);
+
+ var resolver = await SyncPackageResolver.loadConfig(
+ "http://localhost:${server.port}");
+
+ expect(resolver.packageConfigMap, equals({
+ "foo": Uri.parse("file:///foo/bar/"),
+ "bar": Uri.parse("http://dartlang.org/bar/")
+ }));
+ expect(resolver.packageConfigUri,
+ equals(Uri.parse("http://localhost:${server.port}")));
+ });
+
+ test("with a file: URI", () async {
+ var packagesPath = p.join(sandbox, ".packages");
+ new File(packagesPath).writeAsStringSync(
+ "foo:file:///foo/bar/\n"
+ "bar:http://dartlang.org/bar/");
+
+ var resolver =
+ await SyncPackageResolver.loadConfig(p.toUri(packagesPath));
+
+ expect(resolver.packageConfigMap, equals({
+ "foo": Uri.parse("file:///foo/bar/"),
+ "bar": Uri.parse("http://dartlang.org/bar/")
+ }));
+ expect(resolver.packageConfigUri, equals(p.toUri(packagesPath)));
+ });
+
+ test("with a data: URI", () async {
+ var data = Uri.parse(
+ "data:;charset=utf-8,"
+ "foo:file:///foo/bar/%0A"
+ "bar:http://dartlang.org/bar/%0A");
+ var resolver = await SyncPackageResolver.loadConfig(data);
+
+ expect(resolver.packageConfigMap, equals({
+ "foo": Uri.parse("file:///foo/bar/"),
+ "bar": Uri.parse("http://dartlang.org/bar/")
+ }));
+ expect(resolver.packageConfigUri, equals(data));
+ });
+
+ test("with a package: URI", () async {
+ var resolver = await SyncPackageResolver.loadConfig(
+ "package:package_resolver/src/test_package_config");
+
+ expect(resolver.packageConfigMap, equals({
+ "foo": Uri.parse("file:///foo/bar/"),
+ "bar": Uri.parse("http://dartlang.org/bar/")
+ }));
+ expect(resolver.packageConfigUri, equals(Uri.parse(
+ "package:package_resolver/src/test_package_config")));
+ });
+
+ test("with an unsupported scheme", () {
+ expect(SyncPackageResolver.loadConfig("asdf:foo/bar"),
+ throwsUnsupportedError);
+ });
+ });
+}
diff --git a/test/package_root_info_test.dart b/test/package_root_info_test.dart
new file mode 100644
index 0000000..c0edba6
--- /dev/null
+++ b/test/package_root_info_test.dart
@@ -0,0 +1,147 @@
+// Copyright (c) 2016, 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.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import 'package:package_resolver/package_resolver.dart';
+
+void main() {
+ var resolver;
+ setUp(() {
+ resolver = new SyncPackageResolver.root("file:///foo/bar");
+ });
+
+ group("constructor", () {
+ test("with a URI object", () {
+ var resolver =
+ new SyncPackageResolver.root(Uri.parse("file:///foo/bar/"));
+ expect(resolver.packageRoot, equals(Uri.parse("file:///foo/bar/")));
+ });
+
+ test("with an invalid URI type", () {
+ expect(() => new SyncPackageResolver.root(12), throwsArgumentError);
+ });
+ });
+
+ test("exposes a null config map", () {
+ expect(resolver.packageConfigMap, isNull);
+ });
+
+ test("exposes a null config URI", () {
+ expect(resolver.packageConfigUri, isNull);
+ });
+
+ test("exposes the root root", () {
+ expect(resolver.packageRoot, equals(Uri.parse("file:///foo/bar/")));
+ });
+
+ test("processArgument uses --package-root", () {
+ expect(resolver.processArgument, equals("--package-root=file:///foo/bar/"));
+ });
+
+ group("resolveUri", () {
+ test("with a package", () {
+ expect(resolver.resolveUri("package:baz/bang/qux.dart"),
+ equals(Uri.parse("file:///foo/bar/baz/bang/qux.dart")));
+ });
+
+ test("with a package with no path", () {
+ expect(resolver.resolveUri("package:baz"), isNull);
+ });
+
+ test("with a package with an empty path", () {
+ expect(resolver.resolveUri("package:baz/"),
+ equals(Uri.parse("file:///foo/bar/baz/")));
+ });
+
+ test("with a URI object", () {
+ expect(resolver.resolveUri(Uri.parse("package:baz/bang/qux.dart")),
+ equals(Uri.parse("file:///foo/bar/baz/bang/qux.dart")));
+ });
+
+ test("with an invalid argument type", () {
+ expect(() => resolver.resolveUri(12), throwsArgumentError);
+ });
+
+ test("with a non-package URI", () {
+ expect(() => resolver.resolveUri("file:///zip/zap"),
+ throwsFormatException);
+ });
+
+ test("with an invalid package URI", () {
+ expect(() => resolver.resolveUri("package:"), throwsFormatException);
+ });
+ });
+
+ group("urlFor", () {
+ test("with no path", () {
+ expect(resolver.urlFor("baz"), equals(Uri.parse("file:///foo/bar/baz/")));
+ });
+
+ test("with a path", () {
+ expect(resolver.urlFor("baz", "bang/qux.dart"),
+ equals(Uri.parse("file:///foo/bar/baz/bang/qux.dart")));
+ });
+ });
+
+ group("packageUriFor", () {
+ test("converts a matching URI to a package:", () {
+ expect(resolver.packageUriFor("file:///foo/bar/bang/qux.dart"),
+ equals(Uri.parse("package:bang/qux.dart")));
+ });
+
+ test("converts a matching URI with no path", () {
+ expect(resolver.packageUriFor("file:///foo/bar/baz"),
+ equals(Uri.parse("package:baz/")));
+ expect(resolver.packageUriFor("file:///foo/bar/baz/"),
+ equals(Uri.parse("package:baz/")));
+ });
+
+ test("with a URI object", () {
+ expect(resolver.packageUriFor(Uri.parse("file:///foo/bar/bang/qux.dart")),
+ equals(Uri.parse("package:bang/qux.dart")));
+ });
+
+ test("with an invalid argument type", () {
+ expect(() => resolver.packageUriFor(12), throwsArgumentError);
+ });
+ });
+
+ group("packagePath", () {
+ var sandbox;
+ setUp(() async {
+ sandbox = (await Directory.systemTemp.createTemp("package_resolver_test"))
+ .path;
+ });
+
+ tearDown(() async {
+ await new Directory(sandbox).delete(recursive: true);
+ });
+
+ test("with a file: scheme", () async {
+ var packageLib = p.join(sandbox, "foo/lib");
+ await new Directory(packageLib).create(recursive: true);
+
+ var packagesDir = p.join(sandbox, "packages");
+ var fooLink = p.join(packagesDir, "foo");
+ await new Link(fooLink).create(packageLib, recursive: true);
+
+ var packagesLink = p.join(sandbox, "foo/packages");
+ await new Link(packagesLink).create(packagesDir);
+
+ var resolver = new SyncPackageResolver.root(p.toUri(packagesLink));
+
+ expect(resolver.packagePath("foo"), equals(p.join(sandbox, "foo")));
+ expect(resolver.packagePath("bar"), isNull);
+ });
+
+ test("without a file: scheme", () {
+ var resolver = new SyncPackageResolver.root("http://dartlang.org/bar");
+ expect(resolver.packagePath("foo"), isNull);
+ });
+ });
+}