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); + }); + }); +}