Version 2.0.0 supporting package_config.json (#56)

Giving it the package name package_config_2 to avoid conflict with the existing package_config which is used by the `test` package.

This is the initial version. It will be updated to allow overlapping packages, as long as no file in the package URI root can be considered as belonging to a different package.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50f1ede..84384fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 2.0.0
+ - Based on new JSON file format with more content.
+
 ## 1.2.0
  - Added support for writing default-package entries.
  - Fixed bug when writing `Uri`s containing a fragment.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6f5e0ea..8423ff9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,7 +23,7 @@
 ### File headers
 All files in the project must start with the following header.
 
-    // Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+    // Copyright (c) 2019, 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.
 
diff --git a/LICENSE b/LICENSE
index de31e1a..f75d7c2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2015, the Dart project authors. All rights reserved.
+Copyright 2019, the Dart project authors. All rights reserved.
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
 met:
diff --git a/lib/discovery.dart b/lib/discovery.dart
deleted file mode 100644
index 57584b6..0000000
--- a/lib/discovery.dart
+++ /dev/null
@@ -1,230 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library package_config.discovery;
-
-import "dart:async";
-import "dart:io";
-import "dart:typed_data" show Uint8List;
-
-import "package:path/path.dart" as path;
-
-import "packages.dart";
-import "packages_file.dart" as pkgfile show parse;
-import "src/packages_impl.dart";
-import "src/packages_io_impl.dart";
-
-/// Reads a package resolution file and creates a [Packages] object from it.
-///
-/// The [packagesFile] must exist and be loadable.
-/// Currently that means the URI must have a `file`, `http` or `https` scheme,
-/// and that the file can be loaded and its contents parsed correctly.
-///
-/// If the [loader] is provided, it is used to fetch non-`file` URIs, and
-/// it can support other schemes or set up more complex HTTP requests.
-///
-/// This function can be used to load an explicitly configured package
-/// resolution file, for example one specified using a `--packages`
-/// command-line parameter.
-Future<Packages> loadPackagesFile(Uri packagesFile,
-    {Future<List<int>> loader(Uri uri)}) async {
-  Packages parseBytes(List<int> bytes) {
-    Map<String, Uri> packageMap = pkgfile.parse(bytes, packagesFile);
-    return new MapPackages(packageMap);
-  }
-
-  if (packagesFile.scheme == "file") {
-    File file = new File.fromUri(packagesFile);
-    return parseBytes(await file.readAsBytes());
-  }
-  if (loader == null) {
-    return parseBytes(await _httpGet(packagesFile));
-  }
-  return parseBytes(await loader(packagesFile));
-}
-
-/// Create a [Packages] object for a package directory.
-///
-/// The [packagesDir] URI should refer to a directory.
-/// Package names are resolved as relative to sub-directories of the
-/// package directory.
-///
-/// This function can be used for explicitly configured package directories,
-/// for example one specified using a `--package-root` comand-line parameter.
-Packages getPackagesDirectory(Uri packagesDir) {
-  if (packagesDir.scheme == "file") {
-    Directory directory = new Directory.fromUri(packagesDir);
-    return new FilePackagesDirectoryPackages(directory);
-  }
-  if (!packagesDir.path.endsWith('/')) {
-    packagesDir = packagesDir.replace(path: packagesDir.path + '/');
-  }
-  return new NonFilePackagesDirectoryPackages(packagesDir);
-}
-
-/// Discover the package configuration for a Dart script.
-///
-/// The [baseUri] points to either the Dart script or its directory.
-/// A package resolution strategy is found by going through the following steps,
-/// and stopping when something is found.
-///
-/// * Check if a `.packages` file exists in the same directory.
-/// * If `baseUri`'s scheme is not `file`, then assume a `packages` directory
-///   in the same directory, and resolve packages relative to that.
-/// * If `baseUri`'s scheme *is* `file`:
-///   * Check if a `packages` directory exists.
-///   * Otherwise check each successive parent directory of `baseUri` for a
-///     `.packages` file.
-///
-/// If any of these tests succeed, a `Packages` class is returned.
-/// Returns the constant [noPackages] if no resolution strategy is found.
-///
-/// This function currently only supports `file`, `http` and `https` URIs.
-/// It needs to be able to load a `.packages` file from the URI, so only
-/// recognized schemes are accepted.
-///
-/// To support other schemes, or more complex HTTP requests,
-/// an optional [loader] function can be supplied.
-/// It's called to load the `.packages` file for a non-`file` scheme.
-/// The loader function returns the *contents* of the file
-/// identified by the URI it's given.
-/// The content should be a UTF-8 encoded `.packages` file, and must return an
-/// error future if loading fails for any reason.
-Future<Packages> findPackages(Uri baseUri,
-    {Future<List<int>> loader(Uri unsupportedUri)}) {
-  if (baseUri.scheme == "file") {
-    return new Future<Packages>.sync(() => findPackagesFromFile(baseUri));
-  } else if (loader != null) {
-    return findPackagesFromNonFile(baseUri, loader: loader);
-  } else if (baseUri.scheme == "http" || baseUri.scheme == "https") {
-    return findPackagesFromNonFile(baseUri, loader: _httpGet);
-  } else {
-    return new Future<Packages>.value(Packages.noPackages);
-  }
-}
-
-/// Find the location of the package resolution file/directory for a Dart file.
-///
-/// Checks for a `.packages` file in the [workingDirectory].
-/// If not found, checks for a `packages` directory in the same directory.
-/// If still not found, starts checking parent directories for
-/// `.packages` until reaching the root directory.
-///
-/// Returns a [File] object of a `.packages` file if one is found, or a
-/// [Directory] object for the `packages/` directory if that is found.
-FileSystemEntity _findPackagesFile(String workingDirectory) {
-  var dir = new Directory(workingDirectory);
-  if (!dir.isAbsolute) dir = dir.absolute;
-  if (!dir.existsSync()) {
-    throw new ArgumentError.value(
-        workingDirectory, "workingDirectory", "Directory does not exist.");
-  }
-  File checkForConfigFile(Directory directory) {
-    assert(directory.isAbsolute);
-    var file = new File(path.join(directory.path, ".packages"));
-    if (file.existsSync()) return file;
-    return null;
-  }
-
-  // Check for $cwd/.packages
-  var packagesCfgFile = checkForConfigFile(dir);
-  if (packagesCfgFile != null) return packagesCfgFile;
-  // Check for $cwd/packages/
-  var packagesDir = new Directory(path.join(dir.path, "packages"));
-  if (packagesDir.existsSync()) return packagesDir;
-  // Check for cwd(/..)+/.packages
-  var parentDir = dir.parent;
-  while (parentDir.path != dir.path) {
-    packagesCfgFile = checkForConfigFile(parentDir);
-    if (packagesCfgFile != null) break;
-    dir = parentDir;
-    parentDir = dir.parent;
-  }
-  return packagesCfgFile;
-}
-
-/// Finds a package resolution strategy for a local Dart script.
-///
-/// The [fileBaseUri] points to either a Dart script or the directory of the
-/// script. The `fileBaseUri` must be a `file:` URI.
-///
-/// This function first tries to locate a `.packages` file in the `fileBaseUri`
-/// directory. If that is not found, it instead checks for the presence of
-/// a `packages/` directory in the same place.
-/// If that also fails, it starts checking parent directories for a `.packages`
-/// file, and stops if it finds it.
-/// Otherwise it gives up and returns [Packages.noPackages].
-Packages findPackagesFromFile(Uri fileBaseUri) {
-  Uri baseDirectoryUri = fileBaseUri;
-  if (!fileBaseUri.path.endsWith('/')) {
-    baseDirectoryUri = baseDirectoryUri.resolve(".");
-  }
-  String baseDirectoryPath = baseDirectoryUri.toFilePath();
-  FileSystemEntity location = _findPackagesFile(baseDirectoryPath);
-  if (location == null) return Packages.noPackages;
-  if (location is File) {
-    List<int> fileBytes = location.readAsBytesSync();
-    Map<String, Uri> map =
-        pkgfile.parse(fileBytes, new Uri.file(location.path));
-    return new MapPackages(map);
-  }
-  assert(location is Directory);
-  return new FilePackagesDirectoryPackages(location);
-}
-
-/// Finds a package resolution strategy for a Dart script.
-///
-/// The [nonFileUri] points to either a Dart script or the directory of the
-/// script.
-/// The [nonFileUri] should not be a `file:` URI since the algorithm for
-/// finding a package resolution strategy is more elaborate for `file:` URIs.
-/// In that case, use [findPackagesFromFile].
-///
-/// This function first tries to locate a `.packages` file in the [nonFileUri]
-/// directory. If that is not found, it instead assumes a `packages/` directory
-/// in the same place.
-///
-/// By default, this function only works for `http:` and `https:` URIs.
-/// To support other schemes, a loader must be provided, which is used to
-/// try to load the `.packages` file. The loader should return the contents
-/// of the requested `.packages` file as bytes, which will be assumed to be
-/// UTF-8 encoded.
-Future<Packages> findPackagesFromNonFile(Uri nonFileUri,
-    {Future<List<int>> loader(Uri name)}) async {
-  if (loader == null) loader = _httpGet;
-  Uri packagesFileUri = nonFileUri.resolve(".packages");
-
-  try {
-    List<int> fileBytes = await loader(packagesFileUri);
-    Map<String, Uri> map = pkgfile.parse(fileBytes, packagesFileUri);
-    return new MapPackages(map);
-  } catch (_) {
-    // Didn't manage to load ".packages". Assume a "packages/" directory.
-    Uri packagesDirectoryUri = nonFileUri.resolve("packages/");
-    return new NonFilePackagesDirectoryPackages(packagesDirectoryUri);
-  }
-}
-
-/// Fetches a file over http.
-Future<List<int>> _httpGet(Uri uri) async {
-  HttpClient client = new HttpClient();
-  HttpClientRequest request = await client.getUrl(uri);
-  HttpClientResponse response = await request.close();
-  if (response.statusCode != HttpStatus.ok) {
-    throw new HttpException('${response.statusCode} ${response.reasonPhrase}',
-        uri: uri);
-  }
-  List<List<int>> splitContent = await response.toList();
-  int totalLength = 0;
-  for (var list in splitContent) {
-    totalLength += list.length;
-  }
-  Uint8List result = new Uint8List(totalLength);
-  int offset = 0;
-  for (List<int> contentPart in splitContent) {
-    result.setRange(offset, offset + contentPart.length, contentPart);
-    offset += contentPart.length;
-  }
-  return result;
-}
diff --git a/lib/discovery_analysis.dart b/lib/discovery_analysis.dart
deleted file mode 100644
index d623303..0000000
--- a/lib/discovery_analysis.dart
+++ /dev/null
@@ -1,167 +0,0 @@
-// Copyright (c) 2015, 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.
-
-/// Analyse a directory structure and find packages resolvers for each
-/// sub-directory.
-///
-/// The resolvers are generally the same that would be found by using
-/// the `discovery.dart` library on each sub-directory in turn,
-/// but more efficiently and with some heuristics for directories that
-/// wouldn't otherwise have a package resolution strategy, or that are
-/// determined to be "package directories" themselves.
-library package_config.discovery_analysis;
-
-import "dart:collection" show HashMap;
-import "dart:io" show File, Directory;
-
-import "package:path/path.dart" as path;
-
-import "packages.dart";
-import "packages_file.dart" as pkgfile;
-import "src/packages_impl.dart";
-import "src/packages_io_impl.dart";
-
-/// Associates a [Packages] package resolution strategy with a directory.
-///
-/// The package resolution applies to the directory and any sub-directory
-/// that doesn't have its own overriding child [PackageContext].
-abstract class PackageContext {
-  /// The directory that introduced the [packages] resolver.
-  Directory get directory;
-
-  /// A [Packages] resolver that applies to the directory.
-  ///
-  /// Introduced either by a `.packages` file or a `packages/` directory.
-  Packages get packages;
-
-  /// Child contexts that apply to sub-directories of [directory].
-  List<PackageContext> get children;
-
-  /// Look up the [PackageContext] that applies to a specific directory.
-  ///
-  /// The directory must be inside [directory].
-  PackageContext operator [](Directory directory);
-
-  /// A map from directory to package resolver.
-  ///
-  /// Has an entry for this package context and for each child context
-  /// contained in this one.
-  Map<Directory, Packages> asMap();
-
-  /// Analyze [directory] and sub-directories for package resolution strategies.
-  ///
-  /// Returns a mapping from sub-directories to [Packages] objects.
-  ///
-  /// The analysis assumes that there are no `.packages` files in a parent
-  /// directory of `directory`. If there is, its corresponding `Packages` object
-  /// should be provided as `root`.
-  static PackageContext findAll(Directory directory,
-      {Packages root: Packages.noPackages}) {
-    if (!directory.existsSync()) {
-      throw ArgumentError("Directory not found: $directory");
-    }
-    var contexts = <PackageContext>[];
-    void findRoots(Directory directory) {
-      Packages packages;
-      List<PackageContext> oldContexts;
-      File packagesFile = File(path.join(directory.path, ".packages"));
-      if (packagesFile.existsSync()) {
-        packages = _loadPackagesFile(packagesFile);
-        oldContexts = contexts;
-        contexts = [];
-      } else {
-        Directory packagesDir =
-            Directory(path.join(directory.path, "packages"));
-        if (packagesDir.existsSync()) {
-          packages = FilePackagesDirectoryPackages(packagesDir);
-          oldContexts = contexts;
-          contexts = [];
-        }
-      }
-      for (var entry in directory.listSync()) {
-        if (entry is Directory) {
-          if (packages == null || !entry.path.endsWith("/packages")) {
-            findRoots(entry);
-          }
-        }
-      }
-      if (packages != null) {
-        oldContexts.add(_PackageContext(directory, packages, contexts));
-        contexts = oldContexts;
-      }
-    }
-
-    findRoots(directory);
-    // If the root is not itself context root, add a the wrapper context.
-    if (contexts.length == 1 && contexts[0].directory == directory) {
-      return contexts[0];
-    }
-    return _PackageContext(directory, root, contexts);
-  }
-}
-
-class _PackageContext implements PackageContext {
-  final Directory directory;
-  final Packages packages;
-  final List<PackageContext> children;
-  _PackageContext(this.directory, this.packages, List<PackageContext> children)
-      : children = List<PackageContext>.unmodifiable(children);
-
-  Map<Directory, Packages> asMap() {
-    var result = HashMap<Directory, Packages>();
-    recurse(_PackageContext current) {
-      result[current.directory] = current.packages;
-      for (var child in current.children) {
-        recurse(child);
-      }
-    }
-
-    recurse(this);
-    return result;
-  }
-
-  PackageContext operator [](Directory directory) {
-    String path = directory.path;
-    if (!path.startsWith(this.directory.path)) {
-      throw ArgumentError("Not inside $path: $directory");
-    }
-    _PackageContext current = this;
-    // The current path is know to agree with directory until deltaIndex.
-    int deltaIndex = current.directory.path.length;
-    List children = current.children;
-    int i = 0;
-    while (i < children.length) {
-      // TODO(lrn): Sort children and use binary search.
-      _PackageContext child = children[i];
-      String childPath = child.directory.path;
-      if (_stringsAgree(path, childPath, deltaIndex, childPath.length)) {
-        deltaIndex = childPath.length;
-        if (deltaIndex == path.length) {
-          return child;
-        }
-        current = child;
-        children = current.children;
-        i = 0;
-        continue;
-      }
-      i++;
-    }
-    return current;
-  }
-
-  static bool _stringsAgree(String a, String b, int start, int end) {
-    if (a.length < end || b.length < end) return false;
-    for (int i = start; i < end; i++) {
-      if (a.codeUnitAt(i) != b.codeUnitAt(i)) return false;
-    }
-    return true;
-  }
-}
-
-Packages _loadPackagesFile(File file) {
-  var uri = Uri.file(file.path);
-  var bytes = file.readAsBytesSync();
-  var map = pkgfile.parse(bytes, uri);
-  return MapPackages(map);
-}
diff --git a/lib/package_config.dart b/lib/package_config.dart
new file mode 100644
index 0000000..4d6f346
--- /dev/null
+++ b/lib/package_config.dart
@@ -0,0 +1,141 @@
+// Copyright (c) 2019, 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.
+
+/// A package configuration is a way to assign file paths to package URIs,
+/// and vice-versa,
+library package_config.package_config;
+
+import "dart:io" show File, Directory;
+import "dart:typed_data" show Uint8List;
+import "src/discovery.dart" as discover;
+import "src/package_config.dart";
+import "src/package_config_json.dart";
+
+export "src/package_config.dart" show PackageConfig, Package;
+export "src/errors.dart" show PackageConfigError;
+
+/// Reads a specific package configuration file.
+///
+/// The file must exist and be readable.
+/// It must be either a valid `package_config.json` file
+/// or a valid `.packages` file.
+/// It is considered a `package_config.json` file if its first character
+/// is a `{`.
+///
+/// If the file is a `.packages` file, also checks if there is a
+/// `.dart_tool/package_config.json` file next to the original file,
+/// and if so, loads that instead.
+Future<PackageConfig> loadPackageConfig(File file) => readAnyConfigFile(file);
+
+/// Reads a specific package configuration URI.
+///
+/// The file of the URI must exist and be readable.
+/// It must be either a valid `package_config.json` file
+/// or a valid `.packages` file.
+/// It is considered a `package_config.json` file if its first
+/// non-whitespace character is a `{`.
+///
+/// If the file is a `.packages` file, first checks if there is a
+/// `.dart_tool/package_config.json` file next to the original file,
+/// and if so, loads that instead.
+/// The [file] *must not* be a `package:` URI.
+///
+/// If [loader] is provided, URIs are loaded using that function.
+/// The future returned by the loader must complete with a [Uint8List]
+/// containing the entire file content,
+/// or with `null` if the file does not exist.
+/// The loader may throw at its own discretion, for situations where
+/// it determines that an error might be need user attention,
+/// but it is always allowed to return `null`.
+/// This function makes no attempt to catch such errors.
+///
+/// If no [loader] is supplied, a default loader is used which
+/// only accepts `file:`,  `http:` and `https:` URIs,
+/// and which uses the platform file system and HTTP requests to
+/// fetch file content. The default loader never throws because
+/// of an I/O issue, as long as the location URIs are valid.
+/// As such, it does not distinguish between a file not existing,
+/// and it being temporarily locked or unreachable.
+Future<PackageConfig> loadPackageConfigUri(Uri file,
+        {Future<Uint8List /*?*/ > loader(Uri uri) /*?*/}) =>
+    readAnyConfigFileUri(file, loader);
+
+/// Finds a package configuration relative to [directory].
+///
+/// If [directory] contains a package configuration,
+/// either a `.dart_tool/package_config.json` file or,
+/// if not, a `.packages`, then that file is loaded.
+///
+/// If no file is found in the current directory,
+/// then the parent directories are checked recursively,
+/// all the way to the root directory, to check if those contains
+/// a package configuration.
+/// If [recurse] is set to [false], this parent directory check is not
+/// performed.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig> findPackageConfig(Directory directory,
+        {bool recurse = true}) =>
+    discover.findPackageConfig(directory, recurse);
+
+/// Finds a package configuration relative to [location].
+///
+/// If [location] contains a package configuration,
+/// either a `.dart_tool/package_config.json` file or,
+/// if not, a `.packages`, then that file is loaded.
+/// The [location] URI *must not* be a `package:` URI.
+/// It should be a hierarchical URI which is supported
+/// by [loader].
+///
+/// If no file is found in the current directory,
+/// then the parent directories are checked recursively,
+/// all the way to the root directory, to check if those contains
+/// a package configuration.
+/// If [recurse] is set to [false], this parent directory check is not
+/// performed.
+///
+/// If [loader] is provided, URIs are loaded using that function.
+/// The future returned by the loader must complete with a [Uint8List]
+/// containing the entire file content,
+/// or with `null` if the file does not exist.
+/// The loader may throw at its own discretion, for situations where
+/// it determines that an error might be need user attention,
+/// but it is always allowed to return `null`.
+/// This function makes no attempt to catch such errors.
+///
+/// If no [loader] is supplied, a default loader is used which
+/// only accepts `file:`,  `http:` and `https:` URIs,
+/// and which uses the platform file system and HTTP requests to
+/// fetch file content. The default loader never throws because
+/// of an I/O issue, as long as the location URIs are valid.
+/// As such, it does not distinguish between a file not existing,
+/// and it being temporarily locked or unreachable.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig> findPackageConfigUri(Uri location,
+        {bool recurse = true, Future<Uint8List /*?*/ > loader(Uri uri)}) =>
+    discover.findPackageConfigUri(location, loader, recurse);
+
+/// Writes a package configuration to the provided directory.
+///
+/// Writes `.dart_tool/package_config.json` relative to [directory].
+/// If the `.dart_tool/` directory does not exist, it is created.
+/// If it cannot be created, this operation fails.
+///
+/// If [extraData] contains any entries, they are added to the JSON
+/// written to the `package_config.json` file. Entries with the names
+/// `"configVersion"` or `"packages"` are ignored, all other entries
+/// are added verbatim.
+/// This is intended for, e.g., the
+/// `"generator"`, `"generated"` and `"generatorVersion"`
+/// properties.
+///
+/// Also writes a `.packages` file in [directory].
+/// This will stop happening eventually as the `.packages` file becomes
+/// discontinued.
+/// A comment is generated if `[PackageConfig.extraData]` contains a
+/// `"generator"` entry.
+Future<void> savePackageConfig(
+        PackageConfig configuration, Directory directory) =>
+    writePackageConfigJson(configuration, directory);
diff --git a/lib/packages.dart b/lib/packages.dart
deleted file mode 100644
index 886fbc8..0000000
--- a/lib/packages.dart
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library package_config.packages;
-
-import "src/packages_impl.dart";
-
-/// A package resolution strategy.
-///
-/// Allows converting a `package:` URI to a different kind of URI.
-///
-/// May also allow listing the available packages and converting
-/// to a `Map<String, Uri>` that gives the base location of each available
-/// package. In some cases there is no way to find the available packages,
-/// in which case [packages] and [asMap] will throw if used.
-/// One such case is if the packages are resolved relative to a
-/// `packages/` directory available over HTTP.
-abstract class Packages {
-  /// A [Packages] resolver containing no packages.
-  ///
-  /// This constant object is returned by [find] above if no
-  /// package resolution strategy is found.
-  static const Packages noPackages = const NoPackages();
-
-  /// Resolve a package URI into a non-package URI.
-  ///
-  /// Translates a `package:` URI, according to the package resolution
-  /// strategy, into a URI that can be loaded.
-  /// By default, only `file`, `http` and `https` URIs are returned.
-  /// Custom `Packages` objects may return other URIs.
-  ///
-  /// If resolution fails because a package with the requested package name
-  /// is not available, the [notFound] function is called.
-  /// If no `notFound` function is provided, it defaults to throwing an error.
-  ///
-  /// The [packageUri] must be a valid package URI.
-  Uri resolve(Uri packageUri, {Uri notFound(Uri packageUri)});
-
-  /// Return the names of the available packages.
-  ///
-  /// Returns an iterable that allows iterating the names of available packages.
-  ///
-  /// Some `Packages` objects are unable to find the package names,
-  /// and getting `packages` from such a `Packages` object will throw.
-  Iterable<String> get packages;
-
-  /// Retrieve metadata associated with a package.
-  ///
-  /// Metadata have string keys and values, and are looked up by key.
-  ///
-  /// Returns `null` if the argument is not a valid package name,
-  /// or if the package is not one of the packages configured by
-  /// this packages object, or if the package does not have associated
-  /// metadata with the provided [key].
-  ///
-  /// Not all `Packages` objects can support metadata.
-  /// Those will always return `null`.
-  String packageMetadata(String packageName, String key);
-
-  /// Retrieve metadata associated with a library.
-  ///
-  /// If [libraryUri] is a `package:` URI, the returned value
-  /// is the same that would be returned by [packageMetadata] with
-  /// the package's name and the same key.
-  ///
-  /// If [libraryUri] is not a `package:` URI, and this [Packages]
-  /// object has a [defaultPackageName], then the [key] is looked
-  /// up on the default package instead.
-  ///
-  /// Otherwise the result is `null`.
-  String libraryMetadata(Uri libraryUri, String key);
-
-  /// Return the names-to-base-URI mapping of the available packages.
-  ///
-  /// Returns a map from package name to a base URI.
-  /// The [resolve] method will resolve a package URI with a specific package
-  /// name to a path extending the base URI that this map gives for that
-  /// package name.
-  ///
-  /// Some `Packages` objects are unable to find the package names,
-  /// and calling `asMap` on such a `Packages` object will throw.
-  Map<String, Uri> asMap();
-
-  /// The name of the "default package".
-  ///
-  /// A default package is a package that *non-package* libraries
-  /// may be considered part of for some purposes.
-  ///
-  /// The value is `null` if there is no default package.
-  /// Not all implementations of [Packages] supports a default package,
-  /// and will always have a `null` value for those.
-  String get defaultPackageName;
-}
diff --git a/lib/packages_file.dart b/lib/packages_file.dart
deleted file mode 100644
index 1c35d5b..0000000
--- a/lib/packages_file.dart
+++ /dev/null
@@ -1,232 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library package_config.packages_file;
-
-import "package:charcode/ascii.dart";
-
-import "src/util.dart" show isValidPackageName;
-
-/// Parses a `.packages` file into a map from package name to base URI.
-///
-/// The [source] is the byte content of a `.packages` file, assumed to be
-/// UTF-8 encoded. In practice, all significant parts of the file must be ASCII,
-/// so Latin-1 or Windows-1252 encoding will also work fine.
-///
-/// If the file content is available as a string, its [String.codeUnits] can
-/// be used as the `source` argument of this function.
-///
-/// The [baseLocation] is used as a base URI to resolve all relative
-/// URI references against.
-/// If the content was read from a file, `baseLocation` should be the
-/// location of that file.
-///
-/// If [allowDefaultPackage] is set to true, an entry with an empty package name
-/// is accepted. This entry does not correspond to a package, but instead
-/// represents a *default package* which non-package libraries may be considered
-/// part of in some cases. The value of that entry must be a valid package name.
-///
-/// Returns a simple mapping from package name to package location.
-/// If default package is allowed, the map maps the empty string to the default package's name.
-Map<String, Uri> parse(List<int> source, Uri baseLocation,
-    {bool allowDefaultPackage = false}) {
-  int index = 0;
-  Map<String, Uri> result = <String, Uri>{};
-  while (index < source.length) {
-    bool isComment = false;
-    int start = index;
-    int separatorIndex = -1;
-    int end = source.length;
-    int char = source[index++];
-    if (char == $cr || char == $lf) {
-      continue;
-    }
-    if (char == $colon) {
-      if (!allowDefaultPackage) {
-        throw FormatException("Missing package name", source, index - 1);
-      }
-      separatorIndex = index - 1;
-    }
-    isComment = char == $hash;
-    while (index < source.length) {
-      char = source[index++];
-      if (char == $colon && separatorIndex < 0) {
-        separatorIndex = index - 1;
-      } else if (char == $cr || char == $lf) {
-        end = index - 1;
-        break;
-      }
-    }
-    if (isComment) continue;
-    if (separatorIndex < 0) {
-      throw FormatException("No ':' on line", source, index - 1);
-    }
-    var packageName = new String.fromCharCodes(source, start, separatorIndex);
-    if (packageName.isEmpty
-        ? !allowDefaultPackage
-        : !isValidPackageName(packageName)) {
-      throw FormatException("Not a valid package name", packageName, 0);
-    }
-    var packageValue =
-        new String.fromCharCodes(source, separatorIndex + 1, end);
-    Uri packageLocation;
-    if (packageName.isEmpty) {
-      if (!isValidPackageName(packageValue)) {
-        throw FormatException(
-            "Default package entry value is not a valid package name");
-      }
-      packageLocation = Uri(path: packageValue);
-    } else {
-      packageLocation = baseLocation.resolve(packageValue);
-      if (!packageLocation.path.endsWith('/')) {
-        packageLocation =
-            packageLocation.replace(path: packageLocation.path + "/");
-      }
-    }
-    if (result.containsKey(packageName)) {
-      if (packageName.isEmpty) {
-        throw FormatException(
-            "More than one default package entry", source, start);
-      }
-      throw FormatException("Same package name occured twice", source, start);
-    }
-    result[packageName] = packageLocation;
-  }
-  return result;
-}
-
-/// Writes the mapping to a [StringSink].
-///
-/// If [comment] is provided, the output will contain this comment
-/// with `# ` in front of each line.
-/// Lines are defined as ending in line feed (`'\n'`). If the final
-/// line of the comment doesn't end in a line feed, one will be added.
-///
-/// If [baseUri] is provided, package locations will be made relative
-/// to the base URI, if possible, before writing.
-///
-/// If [allowDefaultPackage] is `true`, the [packageMapping] may contain an
-/// empty string mapping to the _default package name_.
-///
-/// All the keys of [packageMapping] must be valid package names,
-/// and the values must be URIs that do not have the `package:` scheme.
-void write(StringSink output, Map<String, Uri> packageMapping,
-    {Uri baseUri, String comment, bool allowDefaultPackage = false}) {
-  ArgumentError.checkNotNull(allowDefaultPackage, 'allowDefaultPackage');
-
-  if (baseUri != null && !baseUri.isAbsolute) {
-    throw new ArgumentError.value(baseUri, "baseUri", "Must be absolute");
-  }
-
-  if (comment != null) {
-    var lines = comment.split('\n');
-    if (lines.last.isEmpty) lines.removeLast();
-    for (var commentLine in lines) {
-      output.write('# ');
-      output.writeln(commentLine);
-    }
-  } else {
-    output.write("# generated by package:package_config at ");
-    output.write(new DateTime.now());
-    output.writeln();
-  }
-
-  packageMapping.forEach((String packageName, Uri uri) {
-    // If [packageName] is empty then [uri] is the _default package name_.
-    if (allowDefaultPackage && packageName.isEmpty) {
-      final defaultPackageName = uri.toString();
-      if (!isValidPackageName(defaultPackageName)) {
-        throw ArgumentError.value(
-          defaultPackageName,
-          'defaultPackageName',
-          '"$defaultPackageName" is not a valid package name',
-        );
-      }
-      output.write(':');
-      output.write(defaultPackageName);
-      output.writeln();
-      return;
-    }
-    // Validate packageName.
-    if (!isValidPackageName(packageName)) {
-      throw new ArgumentError('"$packageName" is not a valid package name');
-    }
-    if (uri.scheme == "package") {
-      throw new ArgumentError.value(
-          "Package location must not be a package: URI", uri.toString());
-    }
-    output.write(packageName);
-    output.write(':');
-    // If baseUri provided, make uri relative.
-    if (baseUri != null) {
-      uri = _relativize(uri, baseUri);
-    }
-    if (!uri.path.endsWith('/')) {
-      uri = uri.replace(path: uri.path + '/');
-    }
-    output.write(uri);
-    output.writeln();
-  });
-}
-
-/// Attempts to return a relative URI for [uri].
-///
-/// The result URI satisfies `baseUri.resolveUri(result) == uri`,
-/// but may be relative.
-/// The `baseUri` must be absolute.
-Uri _relativize(Uri uri, Uri baseUri) {
-  assert(baseUri.isAbsolute);
-  if (uri.hasQuery || uri.hasFragment) {
-    uri = new Uri(
-        scheme: uri.scheme,
-        userInfo: uri.hasAuthority ? uri.userInfo : null,
-        host: uri.hasAuthority ? uri.host : null,
-        port: uri.hasAuthority ? uri.port : null,
-        path: uri.path);
-  }
-
-  // Already relative. We assume the caller knows what they are doing.
-  if (!uri.isAbsolute) return uri;
-
-  if (baseUri.scheme != uri.scheme) {
-    return uri;
-  }
-
-  // If authority differs, we could remove the scheme, but it's not worth it.
-  if (uri.hasAuthority != baseUri.hasAuthority) return uri;
-  if (uri.hasAuthority) {
-    if (uri.userInfo != baseUri.userInfo ||
-        uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
-        uri.port != baseUri.port) {
-      return uri;
-    }
-  }
-
-  baseUri = baseUri.normalizePath();
-  List<String> base = baseUri.pathSegments.toList();
-  if (base.isNotEmpty) {
-    base = new List<String>.from(base)..removeLast();
-  }
-  uri = uri.normalizePath();
-  List<String> target = uri.pathSegments.toList();
-  if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
-  int index = 0;
-  while (index < base.length && index < target.length) {
-    if (base[index] != target[index]) {
-      break;
-    }
-    index++;
-  }
-  if (index == base.length) {
-    if (index == target.length) {
-      return new Uri(path: "./");
-    }
-    return new Uri(path: target.skip(index).join('/'));
-  } else if (index > 0) {
-    return new Uri(
-        path: '../' * (base.length - index) + target.skip(index).join('/'));
-  } else {
-    return uri;
-  }
-}
diff --git a/lib/src/discovery.dart b/lib/src/discovery.dart
new file mode 100644
index 0000000..6b0fc8f
--- /dev/null
+++ b/lib/src/discovery.dart
@@ -0,0 +1,132 @@
+// Copyright (c) 2019, 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 'dart:typed_data';
+
+import "package:path/path.dart" as path;
+
+import "errors.dart";
+import "package_config_impl.dart";
+import "package_config_json.dart";
+import "packages_file.dart" as packages_file;
+import "util.dart" show defaultLoader;
+
+final Uri packageConfigJsonPath = Uri(path: ".dart_tool/package_config.json");
+final Uri dotPackagesPath = Uri(path: ".packages");
+final Uri currentPath = Uri(path: ".");
+final Uri parentPath = Uri(path: "..");
+
+/// Discover the package configuration for a Dart script.
+///
+/// The [baseDirectory] points to the directory of the Dart script.
+/// A package resolution strategy is found by going through the following steps,
+/// and stopping when something is found.
+///
+/// * Check if a `.dart_tool/package_config.json` file exists in the directory.
+/// * Check if a `.packages` file exists in the directory.
+/// * Repeat these checks for the parent directories until reaching the
+///   root directory if [recursive] is true.
+///
+/// If any of these tests succeed, a `PackageConfig` class is returned.
+/// Returns `null` if no configuration was found. If a configuration
+/// is needed, then the caller can supply [PackageConfig.empty].
+Future<PackageConfig /*?*/ > findPackageConfig(
+    Directory baseDirectory, bool recursive) async {
+  var directory = baseDirectory;
+  if (!directory.isAbsolute) directory = directory.absolute;
+  if (!await directory.exists()) {
+    return null;
+  }
+  do {
+    // Check for $cwd/.packages
+    var packageConfig = await findPackagConfigInDirectory(directory);
+    if (packageConfig != null) return packageConfig;
+    if (!recursive) break;
+    // Check in parent directories.
+    var parentDirectory = directory.parent;
+    if (parentDirectory.path == directory.path) break;
+    directory = parentDirectory;
+  } while (true);
+  return null;
+}
+
+/// Similar to [findPackageConfig] but based on a URI.
+Future<PackageConfig /*?*/ > findPackageConfigUri(Uri location,
+    Future<Uint8List /*?*/ > loader(Uri uri) /*?*/, bool recursive) async {
+  if (location.isScheme("package")) {
+    throw PackageConfigArgumentError(
+        location, "location", "Must not be a package: URI");
+  }
+  if (loader == null) {
+    if (location.isScheme("file")) {
+      return findPackageConfig(
+          Directory.fromUri(location.resolveUri(currentPath)), recursive);
+    }
+    loader = defaultLoader;
+  }
+  if (!location.path.endsWith("/")) location = location.resolveUri(currentPath);
+  while (true) {
+    var file = location.resolveUri(packageConfigJsonPath);
+    var bytes = await loader(file);
+    if (bytes != null) {
+      return parsePackageConfigBytes(bytes, file);
+    }
+    file = location.resolveUri(dotPackagesPath);
+    bytes = await loader(file);
+    if (bytes != null) {
+      return packages_file.parse(bytes, file);
+    }
+    if (!recursive) break;
+    var parent = location.resolveUri(parentPath);
+    if (parent == location) break;
+    location = parent;
+  }
+  return null;
+}
+
+/// Finds a `.packages` or `.dart_tool/package_config.json` file in [directory].
+///
+/// Loads the file, if it is there, and returns the resulting [PackageConfig].
+/// Returns `null` if the file isn't there.
+/// Throws [FormatException] if a file is there but is not valid.
+///
+/// If [extraData] is supplied and the `package_config.json` contains extra
+/// entries in the top JSON object, those extra entries are stored into
+/// [extraData].
+Future<PackageConfig /*?*/ > findPackagConfigInDirectory(
+    Directory directory) async {
+  var packageConfigFile = await checkForPackageConfigJsonFile(directory);
+  if (packageConfigFile != null) {
+    return await readPackageConfigJsonFile(packageConfigFile);
+  }
+  packageConfigFile = await checkForDotPackagesFile(directory);
+  if (packageConfigFile != null) {
+    return await readDotPackagesFile(packageConfigFile);
+  }
+  return null;
+}
+
+Future<File> /*?*/ checkForPackageConfigJsonFile(Directory directory) async {
+  assert(directory.isAbsolute);
+  var file =
+      File(path.join(directory.path, ".dart_tool", "package_config.json"));
+  if (await file.exists()) return file;
+  return null;
+}
+
+Future<File /*?*/ > checkForDotPackagesFile(Directory directory) async {
+  var file = File(path.join(directory.path, ".packages"));
+  if (await file.exists()) return file;
+  return null;
+}
+
+Future<Uint8List /*?*/ > _loadFile(File file) async {
+  Uint8List bytes;
+  try {
+    return await file.readAsBytes();
+  } catch (_) {
+    return null;
+  }
+}
diff --git a/lib/src/errors.dart b/lib/src/errors.dart
new file mode 100644
index 0000000..6c31cce
--- /dev/null
+++ b/lib/src/errors.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2019, 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.

+

+/// General superclass of most errors and exceptions thrown by this package.

+///

+/// Only covers errors thrown while parsing package configuration files.

+/// Programming errors and I/O exceptions are not covered.

+abstract class PackageConfigError {

+  PackageConfigError._();

+}

+

+class PackageConfigArgumentError extends ArgumentError

+    implements PackageConfigError {

+  PackageConfigArgumentError(Object /*?*/ value, String name, String message)

+      : super.value(value, name, message);

+}

+

+class PackageConfigFormatException extends FormatException

+    implements PackageConfigError {

+  PackageConfigFormatException(String message, Object /*?*/ value,

+      [int /*?*/ index])

+      : super(message, value, index);

+}

diff --git a/lib/src/package_config.dart b/lib/src/package_config.dart
new file mode 100644
index 0000000..f7b96b8
--- /dev/null
+++ b/lib/src/package_config.dart
@@ -0,0 +1,176 @@
+// Copyright (c) 2019, 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_config_impl.dart";
+
+/// A package configuration.
+///
+/// Associates configuration data to packages and files in packages.
+///
+/// More members may be added to this class in the future,
+/// so classes outside of this package must not implement [PackageConfig]
+/// or any subclass of it.
+abstract class PackageConfig {
+  /// The largest configuration version currently recognized.
+  static const int maxVersion = 2;
+
+  /// An empty package configuration.
+  ///
+  /// A package configuration with no available packages.
+  /// Is used as a default value where a package configuration
+  /// is expected, but none have been specified or found.
+  static const PackageConfig empty = const SimplePackageConfig.empty();
+
+  /// Creats a package configuration with the provided available [packages].
+  ///
+  /// The packages must be valid packages (valid package name, valid
+  /// absolute directory URIs, valid language version, if any),
+  /// and there must not be two packages with the same name or with
+  /// overlapping root directories.
+  ///
+  /// If supplied, the [extraData] will be available as the
+  /// [PackageConfig.extraData] of the created configuration.
+  ///
+  /// The version of the resulting configuration is always [maxVersion].
+  factory PackageConfig(Iterable<Package> packages, {dynamic extraData}) =>
+      SimplePackageConfig(maxVersion, packages);
+
+  /// The configuration version number.
+  ///
+  /// Currently this is 1 or 2, where
+  /// * Version one is the `.packages` file format and
+  /// * Version two is the first `package_config.json` format.
+  ///
+  /// Instances of this class supports both, and the version
+  /// is only useful for detecting which kind of file the configuration
+  /// was read from.
+  int get version;
+
+  /// All the available packages of this configuration.
+  ///
+  /// No two of these packages have the same name,
+  /// and no two [Package.root] directories overlap.
+  Iterable<Package> get packages;
+
+  /// Look up a package by name.
+  ///
+  /// Returns the [Package] fron [packages] with [packageName] as
+  /// [Package.name]. Returns `null` if the package is not available in the
+  /// current configuration.
+  Package /*?*/ operator [](String packageName);
+
+  /// Provides the associated package for a specific [file] (or directory).
+  ///
+  /// Returns a [Package] which contains the [file]'s path, if any.
+  /// That is, the [Package.rootUri] directory is a parent directory
+  /// of the [file]'s location.
+  ///
+  /// Returns `null` if the file does not belong to any package.
+  Package /*?*/ packageOf(Uri file);
+
+  /// Resolves a `package:` URI to a non-package URI
+  ///
+  /// The [packageUri] must be a valid package URI. That means:
+  /// * A URI with `package` as scheme,
+  /// * with no authority part (`package://...`),
+  /// * with a path starting with a valid package name followed by a slash, and
+  /// * with no query or fragment part.
+  ///
+  /// Throws an [ArgumentError] (which also implements [PackageConfigError])
+  /// if the package URI is not valid.
+  ///
+  /// Returns `null` if the package name of [packageUri] is not available
+  /// in this package configuration.
+  /// Returns the remaining path of the package URI resolved relative to the
+  /// [Package.packageUriRoot] of the corresponding package.
+  Uri /*?*/ resolve(Uri packageUri);
+
+  /// The package URI which resolves to [nonPackageUri].
+  ///
+  /// The [nonPackageUri] must not have any query or fragment part,
+  /// and it must not have `package` as scheme.
+  /// Throws an [ArgumentError] (which also implements [PackageConfigError])
+  /// if the non-package URI is not valid.
+  ///
+  /// Returns a package URI which [resolve] will convert to [nonPackageUri],
+  /// if any such URI exists. Returns `null` if no such package URI exists.
+  Uri /*?*/ toPackageUri(Uri nonPackageUri);
+
+  /// Extra data associated with the package configuration.
+  ///
+  /// The data may be in any format, depending on who introduced it.
+  /// The standard `packjage_config.json` file storage will only store
+  /// JSON-like list/map data structures.
+  dynamic get extraData;
+}
+
+/// Configuration data for a single package.
+abstract class Package {
+  /// Creates a package with the provided properties.
+  ///
+  /// The [name] must be a valid package name.
+  /// The [root] must be an absolute directory URI, meaning an absolute URI
+  /// with no query or fragment path and a path starting and ending with `/`.
+  /// The [packageUriRoot], if provided, must be either an absolute
+  /// directory URI or a relative URI reference which is then resolved
+  /// relative to [root]. It must then also be a subdirectory of [root],
+  /// or the same directory.
+  /// If [languageVersion] is supplied, it must be a valid Dart language
+  /// version, which means two decimal integer literals separated by a `.`,
+  /// where the integer literals have no leading zeros unless they are
+  /// a single zero digit.
+  /// If [extraData] is supplied, it will be available as the
+  /// [Package.extraData] of the created package.
+  factory Package(String name, Uri root,
+          {Uri /*?*/ packageUriRoot,
+          String /*?*/ languageVersion,
+          dynamic extraData}) =>
+      SimplePackage(name, root, packageUriRoot, languageVersion, extraData);
+
+  /// The package-name of the package.
+  String get name;
+
+  /// The location of the root of the package.
+  ///
+  /// Is always an absolute URI with no query or fragment parts,
+  /// and with a path ending in `/`.
+  ///
+  /// All files in the [rootUri] directory are considered
+  /// part of the package for purposes where that that matters.
+  Uri get root;
+
+  /// The root of the files available through `package:` URIs.
+  ///
+  /// A `package:` URI with [name] as the package name is
+  /// resolved relative to this location.
+  ///
+  /// Is always an absolute URI with no query or fragment part
+  /// with a path ending in `/`,
+  /// and with a location which is a subdirectory
+  /// of the [root], or the same as the [root].
+  Uri get packageUriRoot;
+
+  /// The default language version associated with this package.
+  ///
+  /// Each package may have a default language version associated,
+  /// which is the language version used to parse and compile
+  /// Dart files in the package.
+  /// A package version is always of the form:
+  ///
+  /// * A numeral consisting of one or more decimal digits,
+  ///   with no leading zero unless the entire numeral is a single zero digit.
+  /// * Followed by a `.` character.
+  /// * Followed by another numeral of the same form.
+  ///
+  /// There is no whitespace allowed around the numerals.
+  /// Valid version numbers include `2.5`, `3.0`, and `1234.5678`.
+  String /*?*/ get languageVersion;
+
+  /// Extra data associated with the specific package.
+  ///
+  /// The data may be in any format, depending on who introduced it.
+  /// The standard `packjage_config.json` file storage will only store
+  /// JSON-like list/map data structures.
+  dynamic get extraData;
+}
diff --git a/lib/src/package_config_impl.dart b/lib/src/package_config_impl.dart
new file mode 100644
index 0000000..0bbe18f
--- /dev/null
+++ b/lib/src/package_config_impl.dart
@@ -0,0 +1,181 @@
+// Copyright (c) 2019, 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 'errors.dart';
+import "package_config.dart";
+export "package_config.dart";
+import "util.dart";
+
+class SimplePackageConfig implements PackageConfig {
+  final int version;
+  final Map<String, Package> _packages;
+  final dynamic extraData;
+
+  SimplePackageConfig(int version, Iterable<Package> packages, [this.extraData])
+      : version = _validateVersion(version),
+        _packages = _validatePackages(packages);
+
+  SimplePackageConfig._(
+      int version, Iterable<SimplePackage> packages, this.extraData)
+      : version = _validateVersion(version),
+        _packages = {for (var package in packages) package.name: package};
+
+  /// Creates empty configuration.
+  ///
+  /// The empty configuration can be used in cases where no configuration is
+  /// found, but code expects a non-null configuration.
+  const SimplePackageConfig.empty()
+      : version = 1,
+        _packages = const <String, Package>{},
+        extraData = null;
+
+  static int _validateVersion(int version) {
+    if (version < 0 || version > PackageConfig.maxVersion) {
+      throw PackageConfigArgumentError(version, "version",
+          "Must be in the range 1 to ${PackageConfig.maxVersion}");
+    }
+    return version;
+  }
+
+  static Map<String, Package> _validatePackages(Iterable<Package> packages) {
+    Map<String, Package> result = {};
+    for (var package in packages) {
+      if (package is! SimplePackage) {
+        // SimplePackage validates these properties.
+        try {
+          _validatePackageData(package.name, package.root,
+              package.packageUriRoot, package.languageVersion);
+        } catch (e) {
+          throw PackageConfigArgumentError(
+              packages, "packages", "Package ${package.name}: ${e.message}");
+        }
+      }
+      var name = package.name;
+      if (result.containsKey(name)) {
+        throw PackageConfigArgumentError(
+            name, "packages", "Duplicate package name");
+      }
+      result[name] = package;
+    }
+
+    // Check that no root URI is a prefix of another.
+    if (result.length > 1) {
+      // Uris cache their toString, so this is not as bad as it looks.
+      var rootUris = [...result.values]
+        ..sort((a, b) => a.root.toString().compareTo(b.root.toString()));
+      var prev = rootUris[0];
+      var prevRoot = prev.root.toString();
+      for (int i = 1; i < rootUris.length; i++) {
+        var next = rootUris[i];
+        var nextRoot = next.root.toString();
+        // If one string is a prefix of another,
+        // the former sorts just before the latter.
+        if (nextRoot.startsWith(prevRoot)) {
+          throw PackageConfigArgumentError(
+              packages,
+              "packages",
+              "Package ${next.name} root overlaps "
+                  "package ${prev.name} root.\n"
+                  "${prev.name} root: $prevRoot\n"
+                  "${next.name} root: $nextRoot\n");
+        }
+        prev = next;
+      }
+    }
+    return result;
+  }
+
+  Iterable<Package> get packages => _packages.values;
+
+  Package /*?*/ operator [](String packageName) => _packages[packageName];
+
+  /// Provides the associated package for a specific [file] (or directory).
+  ///
+  /// Returns a [Package] which contains the [file]'s path.
+  /// That is, the [Package.rootUri] directory is a parent directory
+  /// of the [file]'s location.
+  /// Returns `null` if the file does not belong to any package.
+  Package /*?*/ packageOf(Uri file) {
+    String path = file.toString();
+    for (var package in _packages.values) {
+      var rootPath = package.root.toString();
+      if (path.startsWith(rootPath)) return package;
+    }
+    return null;
+  }
+
+  Uri /*?*/ resolve(Uri packageUri) {
+    String packageName = checkValidPackageUri(packageUri, "packageUri");
+    return _packages[packageName]?.packageUriRoot?.resolveUri(
+        Uri(path: packageUri.path.substring(packageName.length + 1)));
+  }
+
+  Uri /*?*/ toPackageUri(Uri nonPackageUri) {
+    if (nonPackageUri.isScheme("package")) {
+      throw PackageConfigArgumentError(
+          nonPackageUri, "nonPackageUri", "Must not be a package URI");
+    }
+    if (nonPackageUri.hasQuery || nonPackageUri.hasFragment) {
+      throw PackageConfigArgumentError(nonPackageUri, "nonPackageUri",
+          "Must not have query or fragment part");
+    }
+    for (var package in _packages.values) {
+      var root = package.packageUriRoot;
+      if (isUriPrefix(root, nonPackageUri)) {
+        var rest = nonPackageUri.toString().substring(root.toString().length);
+        return Uri(scheme: "package", path: "${package.name}/$rest");
+      }
+    }
+    return null;
+  }
+}
+
+/// Configuration data for a single package.
+class SimplePackage implements Package {
+  final String name;
+  final Uri root;
+  final Uri packageUriRoot;
+  final String /*?*/ languageVersion;
+  final dynamic extraData;
+
+  SimplePackage._(this.name, this.root, this.packageUriRoot,
+      this.languageVersion, this.extraData);
+
+  factory SimplePackage(String name, Uri root, Uri packageUriRoot,
+      String /*?*/ languageVersion, dynamic extraData) {
+    _validatePackageData(name, root, packageUriRoot, languageVersion);
+    return SimplePackage._(
+        name, root, packageUriRoot, languageVersion, extraData);
+  }
+}
+
+void _validatePackageData(
+    String name, Uri root, Uri packageUriRoot, String /*?*/ languageVersion) {
+  if (!isValidPackageName(name)) {
+    throw PackageConfigArgumentError(name, "name", "Not a valid package name");
+  }
+  if (!isAbsoluteDirectoryUri(root)) {
+    throw PackageConfigArgumentError(
+        "$root",
+        "root",
+        "Not an absolute URI with no query or fragment "
+            "with a path ending in /");
+  }
+  if (!isAbsoluteDirectoryUri(packageUriRoot)) {
+    throw PackageConfigArgumentError(
+        packageUriRoot,
+        "packageUriRoot",
+        "Not an absolute URI with no query or fragment "
+            "with a path ending in /");
+  }
+  if (!isUriPrefix(root, packageUriRoot)) {
+    throw PackageConfigArgumentError(packageUriRoot, "packageUriRoot",
+        "The package URI root is not below the package root");
+  }
+  if (languageVersion != null &&
+      checkValidVersionNumber(languageVersion) >= 0) {
+    throw PackageConfigArgumentError(
+        languageVersion, "languageVersion", "Invalid language version format");
+  }
+}
diff --git a/lib/src/package_config_json.dart b/lib/src/package_config_json.dart
new file mode 100644
index 0000000..8a6014c
--- /dev/null
+++ b/lib/src/package_config_json.dart
@@ -0,0 +1,327 @@
+// Copyright (c) 2019, 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:convert";
+import "dart:io";
+import "dart:typed_data";
+
+import 'package:charcode/ascii.dart';
+import "package:path/path.dart" as path;
+
+import "discovery.dart" show packageConfigJsonPath;
+import "errors.dart";
+import "package_config_impl.dart";
+import "packages_file.dart" as packages_file;
+import "util.dart";
+
+const String _configVersionKey = "configVersion";
+const String _packagesKey = "packages";
+const List<String> _topNames = [_configVersionKey, _packagesKey];
+const String _nameKey = "name";
+const String _rootUriKey = "rootUri";
+const String _packageUriKey = "packageUri";
+const String _languageVersionKey = "languageVersion";
+const List<String> _packageNames = [
+  _nameKey,
+  _rootUriKey,
+  _packageUriKey,
+  _languageVersionKey
+];
+
+const String _generatedKey = "generated";
+const String _generatorKey = "generator";
+const String _generatorVersionKey = "generatorVersion";
+
+/// Reads a package configuration file.
+///
+/// Detects whether the [file] is a version one `.packages` file or
+/// a version two `package_config.json` file.
+///
+/// If the [file] is a `.packages` file, first checks whether there is an
+/// adjacent `.dart_tool/package_config.json` file, and if so,
+/// reads that instead.
+///
+/// The file must exist and be a normal file.
+Future<PackageConfig> readAnyConfigFile(File file) async {
+  var bytes = await file.readAsBytes();
+  int firstChar = firstNonWhitespaceChar(bytes);
+  if (firstChar != $lbrace) {
+    // Definitely not a JSON object, probably a .packages.
+    var alternateFile = File(path.join(
+        path.dirname(file.path), ".dart_tool", "package_config.json"));
+    if (!alternateFile.existsSync()) {
+      return packages_file.parse(bytes, file.uri);
+    }
+    file = alternateFile;
+    bytes = await alternateFile.readAsBytes();
+  }
+  return parsePackageConfigBytes(bytes, file.uri);
+}
+
+/// Like [readAnyConfigFile] but uses a URI and an optional loader.
+Future<PackageConfig> readAnyConfigFileUri(
+    Uri file, Future<Uint8List /*?*/ > loader(Uri uri) /*?*/) async {
+  if (file.isScheme("package")) {
+    throw PackageConfigArgumentError(
+        file, "file", "Must not be a package: URI");
+  }
+  if (loader == null) {
+    if (file.isScheme("file")) return readAnyConfigFile(File.fromUri(file));
+    loader = defaultLoader;
+  }
+  var bytes = await loader(file);
+  if (bytes == null) {
+    throw PackageConfigArgumentError(
+        file.toString(), "file", "File cannot be read");
+  }
+  int firstChar = firstNonWhitespaceChar(bytes);
+  if (firstChar != $lbrace) {
+    // Definitely not a JSON object, probably a .packages.
+    var alternateFile = file.resolveUri(packageConfigJsonPath);
+    var alternateBytes = await loader(alternateFile);
+    if (alternateBytes == null) {
+      return packages_file.parse(bytes, file);
+    }
+    bytes = alternateBytes;
+    file = alternateFile;
+  }
+  return parsePackageConfigBytes(bytes, file);
+}
+
+Future<PackageConfig> readPackageConfigJsonFile(File file) async {
+  Uint8List bytes;
+  try {
+    bytes = await file.readAsBytes();
+  } catch (_) {
+    return null;
+  }
+  return parsePackageConfigBytes(bytes, file.uri);
+}
+
+Future<PackageConfig> readDotPackagesFile(File file) async {
+  Uint8List bytes;
+  try {
+    bytes = await file.readAsBytes();
+  } catch (_) {
+    return null;
+  }
+  return packages_file.parse(bytes, file.uri);
+}
+
+PackageConfig parsePackageConfigBytes(Uint8List bytes, Uri file) {
+  // TODO(lrn): Make this simpler. Maybe parse directly from bytes.
+  return parsePackageConfigJson(json.fuse(utf8).decode(bytes), file);
+}
+
+/// Creates a [PackageConfig] from a parsed JSON-like object structure.
+///
+/// The [json] argument must be a JSON object (`Map<String, dynamic>`)
+/// containing a `"configVersion"` entry with an integer value in the range
+/// 1 to [PackageConfig.maxVersion],
+/// and with a `"packages"` entry which is a JSON array (`List<dynamic>`)
+/// containing JSON objects which each has the following properties:
+///
+/// * `"name"`: The package name as a string.
+/// * `"rootUri"`: The root of the package as a URI stored as a string.
+/// * `"packageUri"`: Optionally the root of for `package:` URI resolution
+///     for the package, as a relative URI below the root URI
+///     stored as a string.
+/// * `"languageVersion"`: Optionally a language version string which is a
+///     an integer numeral, a decimal point (`.`) and another integer numeral,
+///     where the integer numeral cannot have a sign, and can only have a
+///     leading zero if the entire numeral is a single zero.
+///
+/// All other properties are stored in [extraData].
+///
+/// The [baseLocation] is used as base URI to resolve the "rootUri"
+/// URI referencestring.
+PackageConfig parsePackageConfigJson(dynamic json, Uri baseLocation) {
+  if (!baseLocation.hasScheme || baseLocation.isScheme("package")) {
+    throw PackageConfigArgumentError(baseLocation.toString(), "baseLocation",
+        "Must be an absolute non-package: URI");
+  }
+
+  if (!baseLocation.path.endsWith("/")) {
+    baseLocation = baseLocation.resolveUri(Uri(path: "."));
+  }
+
+  String typeName<T>() {
+    if (0 is T) return "int";
+    if ("" is T) return "string";
+    if (const [] is T) return "array";
+    return "object";
+  }
+
+  T checkType<T>(dynamic value, String name, [String /*?*/ packageName]) {
+    if (value is T) return value;
+    // The only types we are called with are [int], [String], [List<dynamic>]
+    // and Map<String, dynamic>. Recognize which to give a better error message.
+    var message =
+        "$name${packageName != null ? " of package $packageName" : ""}"
+        " is not a JSON ${typeName<T>()}";
+    throw PackageConfigFormatException(message, value);
+  }
+
+  Package parsePackage(Map<String, dynamic> entry) {
+    String /*?*/ name;
+    String /*?*/ rootUri;
+    String /*?*/ packageUri;
+    String /*?*/ languageVersion;
+    Map<String, dynamic> /*?*/ extraData;
+    entry.forEach((key, value) {
+      switch (key) {
+        case _nameKey:
+          name = checkType<String>(value, _nameKey);
+          break;
+        case _rootUriKey:
+          rootUri = checkType<String>(value, _rootUriKey, name);
+          break;
+        case _packageUriKey:
+          packageUri = checkType<String>(value, _packageUriKey, name);
+          break;
+        case _languageVersionKey:
+          languageVersion = checkType<String>(value, _languageVersionKey, name);
+          break;
+        default:
+          (extraData ??= {})[key] = value;
+          break;
+      }
+    });
+    if (name == null) {
+      throw PackageConfigFormatException("Missing name entry", entry);
+    }
+    if (rootUri == null) {
+      throw PackageConfigFormatException("Missing rootUri entry", entry);
+    }
+    Uri root = baseLocation.resolve(rootUri);
+    Uri /*?*/ packageRoot = root;
+    if (packageUri != null) packageRoot = root.resolve(packageUri);
+    try {
+      return SimplePackage(name, root, packageRoot, languageVersion, extraData);
+    } on ArgumentError catch (e) {
+      throw PackageConfigFormatException(e.message, e.invalidValue);
+    }
+  }
+
+  var map = checkType<Map<String, dynamic>>(json, "value");
+  Map<String, dynamic> /*?*/ extraData = null;
+  List<Package> /*?*/ packageList;
+  int /*?*/ configVersion;
+  map.forEach((key, value) {
+    switch (key) {
+      case _configVersionKey:
+        configVersion = checkType<int>(value, _configVersionKey);
+        break;
+      case _packagesKey:
+        var packageArray = checkType<List<dynamic>>(value, _packagesKey);
+        var packages = <Package>[];
+        for (var package in packageArray) {
+          packages.add(parsePackage(
+              checkType<Map<String, dynamic>>(package, "package entry")));
+        }
+        packageList = packages;
+        break;
+      default:
+        (extraData ??= {})[key] = value;
+        break;
+    }
+  });
+  if (configVersion == null) {
+    throw PackageConfigFormatException("Missing configVersion entry", json);
+  }
+  if (packageList == null)
+    throw PackageConfigFormatException("Missing packages list", json);
+  try {
+    return SimplePackageConfig(configVersion, packageList, extraData);
+  } on ArgumentError catch (e) {
+    throw PackageConfigFormatException(e.message, e.invalidValue);
+  }
+}
+
+Future<void> writePackageConfigJson(
+    PackageConfig config, Directory targetDirectory) async {
+  // Write .dart_tool/package_config.json first.
+  var file = File(
+      path.join(targetDirectory.path, ".dart_tool", "package_config.json"));
+  var baseUri = file.uri;
+  var extraData = config.extraData;
+  var data = <String, dynamic>{
+    _configVersionKey: PackageConfig.maxVersion,
+    _packagesKey: [
+      for (var package in config.packages)
+        <String, dynamic>{
+          _nameKey: package.name,
+          _rootUriKey: relativizeUri(package.root, baseUri),
+          if (package.root != package.packageUriRoot)
+            _packageUriKey: relativizeUri(package.packageUriRoot, package.root),
+          if (package.languageVersion != null)
+            _languageVersionKey: package.languageVersion,
+          ...?_extractExtraData(package.extraData, _packageNames),
+        }
+    ],
+    ...?_extractExtraData(config.extraData, _topNames),
+  };
+
+  // Write .packages too.
+  String /*?*/ comment;
+  if (extraData != null) {
+    String /*?*/ generator = extraData[_generatorKey];
+    if (generator != null) {
+      String /*?*/ generated = extraData[_generatedKey];
+      String /*?*/ generatorVersion = extraData[_generatorVersionKey];
+      comment = "Generated by $generator"
+          "${generatorVersion != null ? " $generatorVersion" : ""}"
+          "${generated != null ? " on $generated" : ""}.";
+    }
+  }
+  file = File(path.join(targetDirectory.path, ".packages"));
+  baseUri = file.uri;
+  var buffer = StringBuffer();
+  packages_file.write(buffer, config, baseUri: baseUri, comment: comment);
+
+  await Future.wait([
+    file.writeAsString(JsonEncoder.withIndent("  ").convert(data)),
+    file.writeAsString(buffer.toString()),
+  ]);
+}
+
+/// If "extraData" is a JSON map, then return it, otherwise return null.
+///
+/// If the value contains any of the [reservedNames] for the current context,
+/// entries with that name in the extra data are dropped.
+Map<String, dynamic> /*?*/ _extractExtraData(
+    dynamic data, Iterable<String> reservedNames) {
+  if (data is Map<String, dynamic>) {
+    if (data.isEmpty) return null;
+    for (var name in reservedNames) {
+      if (data.containsKey(name)) {
+        data = {
+          for (var key in data.keys)
+            if (!reservedNames.contains(key)) key: data[key]
+        };
+        if (data.isEmpty) return null;
+        for (var value in data.values) {
+          if (!_validateJson(value)) return null;
+        }
+      }
+    }
+    return data;
+  }
+  return null;
+}
+
+/// Checks that the object is a valid JSON-like data structure.
+bool _validateJson(dynamic object) {
+  if (object == null || object == true || object == false) return true;
+  if (object is num || object is String) return true;
+  if (object is List<dynamic>) {
+    for (var element in object) if (!_validateJson(element)) return false;
+    return true;
+  }
+  if (object is Map<String, dynamic>) {
+    for (var value in object.values) if (!_validateJson(value)) return false;
+    return true;
+  }
+  return false;
+}
diff --git a/lib/src/packages_file.dart b/lib/src/packages_file.dart
new file mode 100644
index 0000000..ac57b4f
--- /dev/null
+++ b/lib/src/packages_file.dart
@@ -0,0 +1,150 @@
+// Copyright (c) 2019, 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_config_impl.dart";
+import "package:charcode/ascii.dart";
+
+import "util.dart" show isValidPackageName, relativizeUri;
+import "errors.dart";
+
+/// Parses a `.packages` file into a [PackageConfig].
+///
+/// The [source] is the byte content of a `.packages` file, assumed to be
+/// UTF-8 encoded. In practice, all significant parts of the file must be ASCII,
+/// so Latin-1 or Windows-1252 encoding will also work fine.
+///
+/// If the file content is available as a string, its [String.codeUnits] can
+/// be used as the `source` argument of this function.
+///
+/// The [baseLocation] is used as a base URI to resolve all relative
+/// URI references against.
+/// If the content was read from a file, `baseLocation` should be the
+/// location of that file.
+///
+/// Returns a simple package configuration where each package's
+/// [Package.packageUriRoot] is the same as its [Package.root]
+/// and it has no [Package.languageVersion].
+PackageConfig parse(List<int> source, Uri baseLocation) {
+  if (baseLocation.isScheme("package")) {
+    throw PackageConfigArgumentError(
+        baseLocation, "baseLocation", "Must not be a package: URI");
+  }
+  int index = 0;
+  List<Package> packages = [];
+  Set<String> packageNames = {};
+  while (index < source.length) {
+    bool isComment = false;
+    int start = index;
+    int separatorIndex = -1;
+    int end = source.length;
+    int char = source[index++];
+    if (char == $cr || char == $lf) {
+      continue;
+    }
+    if (char == $colon) {
+      throw PackageConfigFormatException(
+          "Missing package name", source, index - 1);
+    }
+    isComment = char == $hash;
+    while (index < source.length) {
+      char = source[index++];
+      if (char == $colon && separatorIndex < 0) {
+        separatorIndex = index - 1;
+      } else if (char == $cr || char == $lf) {
+        end = index - 1;
+        break;
+      }
+    }
+    if (isComment) continue;
+    if (separatorIndex < 0) {
+      throw PackageConfigFormatException("No ':' on line", source, index - 1);
+    }
+    var packageName = String.fromCharCodes(source, start, separatorIndex);
+    if (!isValidPackageName(packageName)) {
+      throw PackageConfigFormatException(
+          "Not a valid package name", packageName, 0);
+    }
+    var packageValue = String.fromCharCodes(source, separatorIndex + 1, end);
+    Uri packageLocation = baseLocation.resolve(packageValue);
+    if (packageLocation.isScheme("package")) {
+      throw PackageConfigFormatException(
+          "Package URI as location for package", source, separatorIndex + 1);
+    }
+    if (packageLocation.hasQuery || packageLocation.hasFragment) {
+      throw PackageConfigFormatException(
+          "Location URI must not have query or fragment", source, start);
+    }
+    if (!packageLocation.path.endsWith('/')) {
+      packageLocation =
+          packageLocation.replace(path: packageLocation.path + "/");
+    }
+    if (packageNames.contains(packageName)) {
+      throw PackageConfigFormatException(
+          "Same package name occured more than once", source, start);
+    }
+    packages.add(SimplePackage(
+        packageName, packageLocation, packageLocation, null, null));
+    packageNames.add(packageName);
+  }
+  return SimplePackageConfig(1, packages, null);
+}
+
+/// Writes the configuration to a [StringSink].
+///
+/// If [comment] is provided, the output will contain this comment
+/// with `# ` in front of each line.
+/// Lines are defined as ending in line feed (`'\n'`). If the final
+/// line of the comment doesn't end in a line feed, one will be added.
+///
+/// If [baseUri] is provided, package locations will be made relative
+/// to the base URI, if possible, before writing.
+///
+/// If [allowDefaultPackage] is `true`, the [packageMapping] may contain an
+/// empty string mapping to the _default package name_.
+///
+/// All the keys of [packageMapping] must be valid package names,
+/// and the values must be URIs that do not have the `package:` scheme.
+void write(StringSink output, PackageConfig config,
+    {Uri baseUri, String comment}) {
+  if (baseUri != null && !baseUri.isAbsolute) {
+    throw PackageConfigArgumentError(baseUri, "baseUri", "Must be absolute");
+  }
+
+  if (comment != null) {
+    var lines = comment.split('\n');
+    if (lines.last.isEmpty) lines.removeLast();
+    for (var commentLine in lines) {
+      output.write('# ');
+      output.writeln(commentLine);
+    }
+  } else {
+    output.write("# generated by package:package_config at ");
+    output.write(DateTime.now());
+    output.writeln();
+  }
+  for (var package in config.packages) {
+    var packageName = package.name;
+    var uri = package.packageUriRoot;
+    // Validate packageName.
+    if (!isValidPackageName(packageName)) {
+      throw PackageConfigArgumentError(
+          config, "config", '"$packageName" is not a valid package name');
+    }
+    if (uri.scheme == "package") {
+      throw PackageConfigArgumentError(
+          config, "config", "Package location must not be a package URI: $uri");
+    }
+    output.write(packageName);
+    output.write(':');
+    // If baseUri provided, make uri relative.
+    if (baseUri != null) {
+      uri = relativizeUri(uri, baseUri);
+    }
+    if (!uri.path.endsWith('/')) {
+      uri = uri.replace(path: uri.path + '/');
+    }
+    output.write(uri);
+    output.writeln();
+  }
+}
diff --git a/lib/src/packages_impl.dart b/lib/src/packages_impl.dart
deleted file mode 100644
index 817002f..0000000
--- a/lib/src/packages_impl.dart
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (c) 2015, 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.
-
-/// Implementations of [Packages] that may be used in either server or browser
-/// based applications. For implementations that can only run in the browser,
-/// see [package_config.packages_io_impl].
-library package_config.packages_impl;
-
-import "dart:collection" show UnmodifiableMapView;
-
-import "../packages.dart";
-import "util.dart" show checkValidPackageUri;
-
-/// A [Packages] null-object.
-class NoPackages implements Packages {
-  const NoPackages();
-
-  Uri resolve(Uri packageUri, {Uri notFound(Uri packageUri)}) {
-    String packageName = checkValidPackageUri(packageUri);
-    if (notFound != null) return notFound(packageUri);
-    throw new ArgumentError.value(
-        packageUri, "packageUri", 'No package named "$packageName"');
-  }
-
-  Iterable<String> get packages => new Iterable<String>.empty();
-
-  Map<String, Uri> asMap() => const <String, Uri>{};
-
-  String get defaultPackageName => null;
-
-  String packageMetadata(String packageName, String key) => null;
-
-  String libraryMetadata(Uri libraryUri, String key) => null;
-}
-
-/// Base class for [Packages] implementations.
-///
-/// This class implements the [resolve] method in terms of a private
-/// member
-abstract class PackagesBase implements Packages {
-  Uri resolve(Uri packageUri, {Uri notFound(Uri packageUri)}) {
-    packageUri = packageUri.normalizePath();
-    String packageName = checkValidPackageUri(packageUri);
-    Uri packageBase = getBase(packageName);
-    if (packageBase == null) {
-      if (notFound != null) return notFound(packageUri);
-      throw new ArgumentError.value(
-          packageUri, "packageUri", 'No package named "$packageName"');
-    }
-    String packagePath = packageUri.path.substring(packageName.length + 1);
-    return packageBase.resolve(packagePath);
-  }
-
-  /// Find a base location for a package name.
-  ///
-  /// Returns `null` if no package exists with that name, and that can be
-  /// determined.
-  Uri getBase(String packageName);
-
-  String get defaultPackageName => null;
-
-  String packageMetadata(String packageName, String key) => null;
-
-  String libraryMetadata(Uri libraryUri, String key) => null;
-}
-
-/// A [Packages] implementation based on an existing map.
-class MapPackages extends PackagesBase {
-  final Map<String, Uri> _mapping;
-  MapPackages(this._mapping);
-
-  Uri getBase(String packageName) =>
-      packageName.isEmpty ? null : _mapping[packageName];
-
-  Iterable<String> get packages => _mapping.keys;
-
-  Map<String, Uri> asMap() => new UnmodifiableMapView<String, Uri>(_mapping);
-
-  String get defaultPackageName => _mapping[""]?.toString();
-
-  String packageMetadata(String packageName, String key) {
-    if (packageName.isEmpty) return null;
-    Uri uri = _mapping[packageName];
-    if (uri == null || !uri.hasFragment) return null;
-    // This can be optimized, either by caching the map or by
-    // parsing incrementally instead of parsing the entire fragment.
-    return Uri.splitQueryString(uri.fragment)[key];
-  }
-
-  String libraryMetadata(Uri libraryUri, String key) {
-    if (libraryUri.isScheme("package")) {
-      return packageMetadata(libraryUri.pathSegments.first, key);
-    }
-    var defaultPackageNameUri = _mapping[""];
-    if (defaultPackageNameUri != null) {
-      return packageMetadata(defaultPackageNameUri.toString(), key);
-    }
-    return null;
-  }
-}
-
-/// A [Packages] implementation based on a remote (e.g., HTTP) directory.
-///
-/// There is no way to detect which packages exist short of trying to use
-/// them. You can't necessarily check whether a directory exists,
-/// except by checking for a know file in the directory.
-class NonFilePackagesDirectoryPackages extends PackagesBase {
-  final Uri _packageBase;
-  NonFilePackagesDirectoryPackages(this._packageBase);
-
-  Uri getBase(String packageName) => _packageBase.resolve("$packageName/");
-
-  Error _failListingPackages() {
-    return new UnsupportedError(
-        "Cannot list packages for a ${_packageBase.scheme}: "
-        "based package root");
-  }
-
-  Iterable<String> get packages {
-    throw _failListingPackages();
-  }
-
-  Map<String, Uri> asMap() {
-    throw _failListingPackages();
-  }
-}
diff --git a/lib/src/packages_io_impl.dart b/lib/src/packages_io_impl.dart
deleted file mode 100644
index 9eba9ce..0000000
--- a/lib/src/packages_io_impl.dart
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) 2015, 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.
-
-/// Implementations of [Packages] that can only be used in server based
-/// applications.
-library package_config.packages_io_impl;
-
-import "dart:collection" show UnmodifiableMapView;
-import "dart:io" show Directory;
-
-import "package:path/path.dart" as path;
-
-import "packages_impl.dart";
-
-/// A [Packages] implementation based on a local directory.
-class FilePackagesDirectoryPackages extends PackagesBase {
-  final Directory _packageDir;
-  final Map<String, Uri> _packageToBaseUriMap = <String, Uri>{};
-
-  FilePackagesDirectoryPackages(this._packageDir);
-
-  Uri getBase(String packageName) {
-    return _packageToBaseUriMap.putIfAbsent(packageName, () {
-      return new Uri.file(path.join(_packageDir.path, packageName, '.'));
-    });
-  }
-
-  Iterable<String> _listPackageNames() {
-    return _packageDir
-        .listSync()
-        .where((e) => e is Directory)
-        .map((e) => path.basename(e.path));
-  }
-
-  Iterable<String> get packages => _listPackageNames();
-
-  Map<String, Uri> asMap() {
-    var result = <String, Uri>{};
-    for (var packageName in _listPackageNames()) {
-      result[packageName] = getBase(packageName);
-    }
-    return new UnmodifiableMapView<String, Uri>(result);
-  }
-}
diff --git a/lib/src/util.dart b/lib/src/util.dart
index f1e1afd..25d7b89 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -1,12 +1,17 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// Copyright (c) 2019, 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.
 
 /// Utility methods used by more than one library in the package.
 library package_config.util;
 
+import 'dart:io';
+import 'dart:typed_data';
+
 import "package:charcode/ascii.dart";
 
+import "errors.dart";
+
 // All ASCII characters that are valid in a package name, with space
 // for all the invalid ones (including space).
 const String _validPackageNameCharacters =
@@ -15,7 +20,7 @@
 
 /// Tests whether something is a valid Dart package name.
 bool isValidPackageName(String string) {
-  return _findInvalidCharacter(string) < 0;
+  return checkPackageName(string) < 0;
 }
 
 /// Check if a string is a valid package name.
@@ -26,7 +31,7 @@
 /// Returns `-1` if the string is valid.
 /// Otherwise returns the index of the first invalid character,
 /// or `string.length` if the string contains no non-'.' character.
-int _findInvalidCharacter(String string) {
+int checkPackageName(String string) {
   // Becomes non-zero if any non-'.' character is encountered.
   int nonDot = 0;
   for (int i = 0; i < string.length; i++) {
@@ -40,47 +45,46 @@
   return -1;
 }
 
-/// Validate that a Uri is a valid package:URI.
-String checkValidPackageUri(Uri packageUri) {
+/// Validate that a [Uri] is a valid `package:` URI.
+String checkValidPackageUri(Uri packageUri, String name) {
   if (packageUri.scheme != "package") {
-    throw new ArgumentError.value(
-        packageUri, "packageUri", "Not a package: URI");
+    throw PackageConfigArgumentError(packageUri, name, "Not a package: URI");
   }
   if (packageUri.hasAuthority) {
-    throw new ArgumentError.value(
-        packageUri, "packageUri", "Package URIs must not have a host part");
+    throw PackageConfigArgumentError(
+        packageUri, name, "Package URIs must not have a host part");
   }
   if (packageUri.hasQuery) {
     // A query makes no sense if resolved to a file: URI.
-    throw new ArgumentError.value(
-        packageUri, "packageUri", "Package URIs must not have a query part");
+    throw PackageConfigArgumentError(
+        packageUri, name, "Package URIs must not have a query part");
   }
   if (packageUri.hasFragment) {
     // We could leave the fragment after the URL when resolving,
     // but it would be odd if "package:foo/foo.dart#1" and
     // "package:foo/foo.dart#2" were considered different libraries.
     // Keep the syntax open in case we ever get multiple libraries in one file.
-    throw new ArgumentError.value(
-        packageUri, "packageUri", "Package URIs must not have a fragment part");
+    throw PackageConfigArgumentError(
+        packageUri, name, "Package URIs must not have a fragment part");
   }
   if (packageUri.path.startsWith('/')) {
-    throw new ArgumentError.value(
-        packageUri, "packageUri", "Package URIs must not start with a '/'");
+    throw PackageConfigArgumentError(
+        packageUri, name, "Package URIs must not start with a '/'");
   }
   int firstSlash = packageUri.path.indexOf('/');
   if (firstSlash == -1) {
-    throw new ArgumentError.value(packageUri, "packageUri",
+    throw PackageConfigArgumentError(packageUri, name,
         "Package URIs must start with the package name followed by a '/'");
   }
   String packageName = packageUri.path.substring(0, firstSlash);
-  int badIndex = _findInvalidCharacter(packageName);
+  int badIndex = checkPackageName(packageName);
   if (badIndex >= 0) {
     if (packageName.isEmpty) {
-      throw new ArgumentError.value(
-          packageUri, "packageUri", "Package names mus be non-empty");
+      throw PackageConfigArgumentError(
+          packageUri, name, "Package names mus be non-empty");
     }
     if (badIndex == packageName.length) {
-      throw new ArgumentError.value(packageUri, "packageUri",
+      throw PackageConfigArgumentError(packageUri, name,
           "Package names must contain at least one non-'.' character");
     }
     assert(badIndex < packageName.length);
@@ -90,8 +94,211 @@
       // Printable character.
       badChar = "'${packageName[badIndex]}' ($badChar)";
     }
-    throw new ArgumentError.value(
-        packageUri, "packageUri", "Package names must not contain $badChar");
+    throw PackageConfigArgumentError(
+        packageUri, name, "Package names must not contain $badChar");
   }
   return packageName;
 }
+
+/// Checks whether [version] is a valid Dart language version string.
+///
+/// The format is (as RegExp) `^(0|[1-9]\d+)\.(0|[1-9]\d+)$`.
+///
+/// Returns the position of the first invalid character, or -1 if
+/// the string is valid.
+/// If the string is terminated early, the result is the length of the string.
+int checkValidVersionNumber(String version) {
+  if (version == null) {
+    return 0;
+  }
+  int index = 0;
+  int dotsSeen = 0;
+  outer:
+  for (;;) {
+    // Check for numeral.
+    if (index == version.length) return index;
+    int char = version.codeUnitAt(index++);
+    int digit = char ^ 0x30;
+    if (digit != 0) {
+      if (digit < 9) {
+        while (index < version.length) {
+          char = version.codeUnitAt(index++);
+          digit = char ^ 0x30;
+          if (digit < 9) continue;
+          if (char == 0x2e /*.*/) {
+            if (dotsSeen > 0) return index - 1;
+            dotsSeen = 1;
+            continue outer;
+          }
+          return index - 1;
+        }
+        if (dotsSeen > 0) return -1;
+        return index;
+      }
+      return index - 1;
+    }
+    // Leading zero means numeral is over.
+    if (index >= version.length) {
+      if (dotsSeen > 0) return -1;
+      return index;
+    }
+    if (dotsSeen > 0) return index;
+    char = version.codeUnitAt(index++);
+    if (char != 0x2e /*.*/) return index - 1;
+  }
+}
+
+/// Checks whether URI is just an absolute directory.
+///
+/// * It must have a scheme.
+/// * It must not have a query or fragment.
+/// * The path must start and end with `/`.
+bool isAbsoluteDirectoryUri(Uri uri) {
+  if (uri.hasQuery) return false;
+  if (uri.hasFragment) return false;
+  if (!uri.hasScheme) return false;
+  var path = uri.path;
+  if (!path.startsWith("/")) return false;
+  if (!path.endsWith("/")) return false;
+  return true;
+}
+
+/// Whether the former URI is a prefix of the latter.
+bool isUriPrefix(Uri prefix, Uri path) {
+  assert(!prefix.hasFragment);
+  assert(!prefix.hasQuery);
+  assert(!path.hasQuery);
+  assert(!path.hasFragment);
+  assert(prefix.path.endsWith('/'));
+  return path.toString().startsWith(prefix.toString());
+}
+
+/// Finds the first non-JSON-whitespace character in a file.
+///
+/// Used to heuristically detect whether a file is a JSON file or an .ini file.
+int firstNonWhitespaceChar(List<int> bytes) {
+  for (int i = 0; i < bytes.length; i++) {
+    var char = bytes[i];
+    if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) {
+      return char;
+    }
+  }
+  return -1;
+}
+
+/// Attempts to return a relative path-only URI for [uri].
+///
+/// First removes any query or fragment part from [uri].
+///
+/// If [uri] is already relative (has no scheme), it's returned as-is.
+/// If that is not desired, the caller can pass `baseUri.resolveUri(uri)`
+/// as the [uri] instead.
+///
+/// If the [uri] has a scheme or authority part which differs from
+/// the [baseUri], or if there is no overlap in the paths of the
+/// two URIs at all, the [uri] is returned as-is.
+///
+/// Otherwise the result is a path-only URI which satsifies
+/// `baseUri.resolveUri(result) == uri`,
+///
+/// The `baseUri` must be absolute.
+Uri relativizeUri(Uri uri, Uri baseUri) {
+  assert(baseUri.isAbsolute);
+  if (uri.hasQuery || uri.hasFragment) {
+    uri = Uri(
+        scheme: uri.scheme,
+        userInfo: uri.hasAuthority ? uri.userInfo : null,
+        host: uri.hasAuthority ? uri.host : null,
+        port: uri.hasAuthority ? uri.port : null,
+        path: uri.path);
+  }
+
+  // Already relative. We assume the caller knows what they are doing.
+  if (!uri.isAbsolute) return uri;
+
+  if (baseUri.scheme != uri.scheme) {
+    return uri;
+  }
+
+  // If authority differs, we could remove the scheme, but it's not worth it.
+  if (uri.hasAuthority != baseUri.hasAuthority) return uri;
+  if (uri.hasAuthority) {
+    if (uri.userInfo != baseUri.userInfo ||
+        uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
+        uri.port != baseUri.port) {
+      return uri;
+    }
+  }
+
+  baseUri = baseUri.normalizePath();
+  List<String> base = [...baseUri.pathSegments];
+  if (base.isNotEmpty) base.removeLast();
+  uri = uri.normalizePath();
+  List<String> target = [...uri.pathSegments];
+  if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
+  int index = 0;
+  while (index < base.length && index < target.length) {
+    if (base[index] != target[index]) {
+      break;
+    }
+    index++;
+  }
+  if (index == base.length) {
+    if (index == target.length) {
+      return Uri(path: "./");
+    }
+    return Uri(path: target.skip(index).join('/'));
+  } else if (index > 0) {
+    var buffer = StringBuffer();
+    for (int n = base.length - index; n > 0; --n) {
+      buffer.write("../");
+    }
+    buffer.writeAll(target.skip(index), "/");
+    return Uri(path: buffer.toString());
+  } else {
+    return uri;
+  }
+}
+
+Future<Uint8List> defaultLoader(Uri uri) async {
+  if (uri.isScheme("file")) {
+    var file = File.fromUri(uri);
+    try {
+      return file.readAsBytes();
+    } catch (_) {
+      return null;
+    }
+  }
+  if (uri.isScheme("http") || uri.isScheme("https")) {
+    return _httpGet(uri);
+  }
+  throw UnsupportedError("Default URI unsupported scheme: $uri");
+}
+
+Future<Uint8List /*?*/ > _httpGet(Uri uri) async {
+  assert(uri.isScheme("http") || uri.isScheme("https"));
+  HttpClient client = new HttpClient();
+  HttpClientRequest request = await client.getUrl(uri);
+  HttpClientResponse response = await request.close();
+  if (response.statusCode != HttpStatus.ok) {
+    return null;
+  }
+  List<List<int>> splitContent = await response.toList();
+  int totalLength = 0;
+  if (splitContent.length == 1) {
+    var part = splitContent[0];
+    if (part is Uint8List) {
+      return part;
+    }
+  }
+  for (var list in splitContent) {
+    totalLength += list.length;
+  }
+  Uint8List result = new Uint8List(totalLength);
+  int offset = 0;
+  for (Uint8List contentPart in splitContent) {
+    result.setRange(offset, offset + contentPart.length, contentPart);
+    offset += contentPart.length;
+  }
+  return result;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 72d299b..0cd7ddc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,11 +1,11 @@
-name: package_config
-version: 1.2.0
-description: Support for working with Package Resolution config files.
+name: package_config_2
+version: 2.0.0
+description: Support for working with Package Configuration files.
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/package_config
 
 environment:
-  sdk: '>=2.0.0-dev <3.0.0'
+  sdk: '>=2.5.0-dev <3.0.0'
 
 dependencies:
   charcode: ^1.1.0
diff --git a/test/all.dart b/test/all.dart
deleted file mode 100644
index 78e6cff..0000000
--- a/test/all.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2015, 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 "discovery_analysis_test.dart" as discovery_analysis;
-import "discovery_test.dart" as discovery;
-import "parse_test.dart" as parse;
-import "parse_write_test.dart" as parse_write;
-
-main() {
-  group("parse:", parse.main);
-  group("discovery:", discovery.main);
-  group("discovery-analysis:", discovery_analysis.main);
-  group("parse/write:", parse_write.main);
-}
diff --git a/test/discovery_analysis_test.dart b/test/discovery_analysis_test.dart
deleted file mode 100644
index e432454..0000000
--- a/test/discovery_analysis_test.dart
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library package_config.discovery_analysis_test;
-
-import "dart:async";
-import "dart:io";
-
-import "package:package_config/discovery_analysis.dart";
-import "package:package_config/packages.dart";
-import "package:path/path.dart" as path;
-import "package:test/test.dart";
-
-main() {
-  fileTest("basic", {
-    ".packages": packagesFile,
-    "foo": {".packages": packagesFile},
-    "bar": {
-      "packages": {"foo": {}, "bar": {}, "baz": {}}
-    },
-    "baz": {}
-  }, (Directory directory) {
-    var dirUri = new Uri.directory(directory.path);
-    PackageContext ctx = PackageContext.findAll(directory);
-    PackageContext root = ctx[directory];
-    expect(root, same(ctx));
-    validatePackagesFile(root.packages, dirUri);
-    var fooDir = sub(directory, "foo");
-    PackageContext foo = ctx[fooDir];
-    expect(identical(root, foo), isFalse);
-    validatePackagesFile(foo.packages, dirUri.resolve("foo/"));
-    var barDir = sub(directory, "bar");
-    PackageContext bar = ctx[sub(directory, "bar")];
-    validatePackagesDir(bar.packages, dirUri.resolve("bar/"));
-    PackageContext barbar = ctx[sub(barDir, "bar")];
-    expect(barbar, same(bar)); // inherited.
-    PackageContext baz = ctx[sub(directory, "baz")];
-    expect(baz, same(root)); // inherited.
-
-    var map = ctx.asMap();
-    expect(map.keys.map((dir) => dir.path),
-        unorderedEquals([directory.path, fooDir.path, barDir.path]));
-    return null;
-  });
-}
-
-Directory sub(Directory parent, String dirName) {
-  return new Directory(path.join(parent.path, dirName));
-}
-
-const packagesFile = """
-# A comment
-foo:file:///dart/packages/foo/
-bar:http://example.com/dart/packages/bar/
-baz:packages/baz/
-""";
-
-void validatePackagesFile(Packages resolver, Uri location) {
-  expect(resolver, isNotNull);
-  expect(resolver.resolve(pkg("foo", "bar/baz")),
-      equals(Uri.parse("file:///dart/packages/foo/bar/baz")));
-  expect(resolver.resolve(pkg("bar", "baz/qux")),
-      equals(Uri.parse("http://example.com/dart/packages/bar/baz/qux")));
-  expect(resolver.resolve(pkg("baz", "qux/foo")),
-      equals(location.resolve("packages/baz/qux/foo")));
-  expect(resolver.packages, unorderedEquals(["foo", "bar", "baz"]));
-}
-
-void validatePackagesDir(Packages resolver, Uri location) {
-  // Expect three packages: foo, bar and baz
-  expect(resolver, isNotNull);
-  expect(resolver.resolve(pkg("foo", "bar/baz")),
-      equals(location.resolve("packages/foo/bar/baz")));
-  expect(resolver.resolve(pkg("bar", "baz/qux")),
-      equals(location.resolve("packages/bar/baz/qux")));
-  expect(resolver.resolve(pkg("baz", "qux/foo")),
-      equals(location.resolve("packages/baz/qux/foo")));
-  if (location.scheme == "file") {
-    expect(resolver.packages, unorderedEquals(["foo", "bar", "baz"]));
-  } else {
-    expect(() => resolver.packages, throwsUnsupportedError);
-  }
-}
-
-Uri pkg(String packageName, String packagePath) {
-  var path;
-  if (packagePath.startsWith('/')) {
-    path = "$packageName$packagePath";
-  } else {
-    path = "$packageName/$packagePath";
-  }
-  return new Uri(scheme: "package", path: path);
-}
-
-/// Create a directory structure from [description] and run [fileTest].
-///
-/// Description is a map, each key is a file entry. If the value is a map,
-/// it's a sub-dir, otherwise it's a file and the value is the content
-/// as a string.
-void fileTest(
-    String name, Map description, Future fileTest(Directory directory)) {
-  group("file-test", () {
-    Directory tempDir = Directory.systemTemp.createTempSync("file-test");
-    setUp(() {
-      _createFiles(tempDir, description);
-    });
-    tearDown(() {
-      tempDir.deleteSync(recursive: true);
-    });
-    test(name, () => fileTest(tempDir));
-  });
-}
-
-void _createFiles(Directory target, Map description) {
-  description.forEach((name, content) {
-    if (content is Map) {
-      Directory subDir = new Directory(path.join(target.path, name));
-      subDir.createSync();
-      _createFiles(subDir, content);
-    } else {
-      File file = new File(path.join(target.path, name));
-      file.writeAsStringSync(content, flush: true);
-    }
-  });
-}
diff --git a/test/discovery_test.dart b/test/discovery_test.dart
index 2824272..5db24a1 100644
--- a/test/discovery_test.dart
+++ b/test/discovery_test.dart
@@ -1,327 +1,250 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
 library package_config.discovery_test;
 
-import "dart:async";
 import "dart:io";
 import "package:test/test.dart";
-import "package:package_config/packages.dart";
-import "package:package_config/discovery.dart";
-import "package:path/path.dart" as path;
+import "package:package_config_2/package_config.dart";
+
+import "src/util.dart";
 
 const packagesFile = """
 # A comment
 foo:file:///dart/packages/foo/
-bar:http://example.com/dart/packages/bar/
+bar:/dart/packages/bar/
 baz:packages/baz/
 """;
 
-void validatePackagesFile(Packages resolver, Uri location) {
+const packageConfigFile = """
+{
+  "configVersion": 2,
+  "packages": [
+    {
+      "name": "foo",
+      "rootUri": "file:///dart/packages/foo/"
+    },
+    {
+      "name": "bar",
+      "rootUri": "/dart/packages/bar/"
+    },
+    {
+      "name": "baz",
+      "rootUri": "../packages/baz/"
+    }
+  ],
+  "extra": [42]
+}
+""";
+
+void validatePackagesFile(PackageConfig resolver, Directory directory) {
   expect(resolver, isNotNull);
   expect(resolver.resolve(pkg("foo", "bar/baz")),
       equals(Uri.parse("file:///dart/packages/foo/bar/baz")));
   expect(resolver.resolve(pkg("bar", "baz/qux")),
-      equals(Uri.parse("http://example.com/dart/packages/bar/baz/qux")));
+      equals(Uri.parse("file:///dart/packages/bar/baz/qux")));
   expect(resolver.resolve(pkg("baz", "qux/foo")),
-      equals(location.resolve("packages/baz/qux/foo")));
-  expect(resolver.packages, unorderedEquals(["foo", "bar", "baz"]));
-}
-
-void validatePackagesDir(Packages resolver, Uri location) {
-  // Expect three packages: foo, bar and baz
-  expect(resolver, isNotNull);
-  expect(resolver.resolve(pkg("foo", "bar/baz")),
-      equals(location.resolve("packages/foo/bar/baz")));
-  expect(resolver.resolve(pkg("bar", "baz/qux")),
-      equals(location.resolve("packages/bar/baz/qux")));
-  expect(resolver.resolve(pkg("baz", "qux/foo")),
-      equals(location.resolve("packages/baz/qux/foo")));
-  if (location.scheme == "file") {
-    expect(resolver.packages, unorderedEquals(["foo", "bar", "baz"]));
-  } else {
-    expect(() => resolver.packages, throwsUnsupportedError);
-  }
-}
-
-Uri pkg(String packageName, String packagePath) {
-  var path;
-  if (packagePath.startsWith('/')) {
-    path = "$packageName$packagePath";
-  } else {
-    path = "$packageName/$packagePath";
-  }
-  return new Uri(scheme: "package", path: path);
+      equals(Uri.directory(directory.path).resolve("packages/baz/qux/foo")));
+  expect([for (var p in resolver.packages) p.name],
+      unorderedEquals(["foo", "bar", "baz"]));
 }
 
 main() {
-  generalTest(".packages", {
-    ".packages": packagesFile,
-    "script.dart": "main(){}",
-    "packages": {"shouldNotBeFound": {}}
-  }, (Uri location) async {
-    Packages resolver;
-    resolver = await findPackages(location);
-    validatePackagesFile(resolver, location);
-    resolver = await findPackages(location.resolve("script.dart"));
-    validatePackagesFile(resolver, location);
-    var specificDiscovery = (location.scheme == "file")
-        ? findPackagesFromFile
-        : findPackagesFromNonFile;
-    resolver = await specificDiscovery(location);
-    validatePackagesFile(resolver, location);
-    resolver = await specificDiscovery(location.resolve("script.dart"));
-    validatePackagesFile(resolver, location);
-  });
-
-  generalTest("packages/", {
-    "packages": {"foo": {}, "bar": {}, "baz": {}},
-    "script.dart": "main(){}"
-  }, (Uri location) async {
-    Packages resolver;
-    bool isFile = (location.scheme == "file");
-    resolver = await findPackages(location);
-    validatePackagesDir(resolver, location);
-    resolver = await findPackages(location.resolve("script.dart"));
-    validatePackagesDir(resolver, location);
-    var specificDiscovery =
-        isFile ? findPackagesFromFile : findPackagesFromNonFile;
-    resolver = await specificDiscovery(location);
-    validatePackagesDir(resolver, location);
-    resolver = await specificDiscovery(location.resolve("script.dart"));
-    validatePackagesDir(resolver, location);
-  });
-
-  generalTest("underscore packages", {
-    "packages": {"_foo": {}}
-  }, (Uri location) async {
-    Packages resolver = await findPackages(location);
-    expect(resolver.resolve(pkg("_foo", "foo.dart")),
-        equals(location.resolve("packages/_foo/foo.dart")));
-  });
-
-  fileTest(".packages recursive", {
-    ".packages": packagesFile,
-    "subdir": {"script.dart": "main(){}"}
-  }, (Uri location) async {
-    Packages resolver;
-    resolver = await findPackages(location.resolve("subdir/"));
-    validatePackagesFile(resolver, location);
-    resolver = await findPackages(location.resolve("subdir/script.dart"));
-    validatePackagesFile(resolver, location);
-    resolver = await findPackagesFromFile(location.resolve("subdir/"));
-    validatePackagesFile(resolver, location);
-    resolver =
-        await findPackagesFromFile(location.resolve("subdir/script.dart"));
-    validatePackagesFile(resolver, location);
-  });
-
-  httpTest(".packages not recursive", {
-    ".packages": packagesFile,
-    "subdir": {"script.dart": "main(){}"}
-  }, (Uri location) async {
-    Packages resolver;
-    var subdir = location.resolve("subdir/");
-    resolver = await findPackages(subdir);
-    validatePackagesDir(resolver, subdir);
-    resolver = await findPackages(subdir.resolve("script.dart"));
-    validatePackagesDir(resolver, subdir);
-    resolver = await findPackagesFromNonFile(subdir);
-    validatePackagesDir(resolver, subdir);
-    resolver = await findPackagesFromNonFile(subdir.resolve("script.dart"));
-    validatePackagesDir(resolver, subdir);
-  });
-
-  fileTest("no packages", {"script.dart": "main(){}"}, (Uri location) async {
-    // A file: location with no .packages or packages returns
-    // Packages.noPackages.
-    Packages resolver;
-    resolver = await findPackages(location);
-    expect(resolver, same(Packages.noPackages));
-    resolver = await findPackages(location.resolve("script.dart"));
-    expect(resolver, same(Packages.noPackages));
-    resolver = findPackagesFromFile(location);
-    expect(resolver, same(Packages.noPackages));
-    resolver = findPackagesFromFile(location.resolve("script.dart"));
-    expect(resolver, same(Packages.noPackages));
-  });
-
-  httpTest("no packages", {"script.dart": "main(){}"}, (Uri location) async {
-    // A non-file: location with no .packages or packages/:
-    // Assumes a packages dir exists, and resolves relative to that.
-    Packages resolver;
-    resolver = await findPackages(location);
-    validatePackagesDir(resolver, location);
-    resolver = await findPackages(location.resolve("script.dart"));
-    validatePackagesDir(resolver, location);
-    resolver = await findPackagesFromNonFile(location);
-    validatePackagesDir(resolver, location);
-    resolver = await findPackagesFromNonFile(location.resolve("script.dart"));
-    validatePackagesDir(resolver, location);
-  });
-
-  test(".packages w/ loader", () async {
-    Uri location = Uri.parse("krutch://example.com/path/");
-    Future<List<int>> loader(Uri file) async {
-      if (file.path.endsWith(".packages")) {
-        return packagesFile.codeUnits;
+  group("findPackages", () {
+    // Finds package_config.json if there.
+    fileTest("package_config.json", {
+      ".packages": "invalid .packages file",
+      "script.dart": "main(){}",
+      "packages": {"shouldNotBeFound": {}},
+      ".dart_tool": {
+        "package_config.json": packageConfigFile,
       }
-      throw "not found";
-    }
-
-    // A non-file: location with no .packages or packages/:
-    // Assumes a packages dir exists, and resolves relative to that.
-    Packages resolver;
-    resolver = await findPackages(location, loader: loader);
-    validatePackagesFile(resolver, location);
-    resolver =
-        await findPackages(location.resolve("script.dart"), loader: loader);
-    validatePackagesFile(resolver, location);
-    resolver = await findPackagesFromNonFile(location, loader: loader);
-    validatePackagesFile(resolver, location);
-    resolver = await findPackagesFromNonFile(location.resolve("script.dart"),
-        loader: loader);
-    validatePackagesFile(resolver, location);
-  });
-
-  test("no packages w/ loader", () async {
-    Uri location = Uri.parse("krutch://example.com/path/");
-    Future<List<int>> loader(Uri file) async {
-      throw "not found";
-    }
-
-    // A non-file: location with no .packages or packages/:
-    // Assumes a packages dir exists, and resolves relative to that.
-    Packages resolver;
-    resolver = await findPackages(location, loader: loader);
-    validatePackagesDir(resolver, location);
-    resolver =
-        await findPackages(location.resolve("script.dart"), loader: loader);
-    validatePackagesDir(resolver, location);
-    resolver = await findPackagesFromNonFile(location, loader: loader);
-    validatePackagesDir(resolver, location);
-    resolver = await findPackagesFromNonFile(location.resolve("script.dart"),
-        loader: loader);
-    validatePackagesDir(resolver, location);
-  });
-
-  generalTest("loadPackagesFile", {".packages": packagesFile},
-      (Uri directory) async {
-    Uri file = directory.resolve(".packages");
-    Packages resolver = await loadPackagesFile(file);
-    validatePackagesFile(resolver, file);
-  });
-
-  generalTest(
-      "loadPackagesFile non-default name", {"pheldagriff": packagesFile},
-      (Uri directory) async {
-    Uri file = directory.resolve("pheldagriff");
-    Packages resolver = await loadPackagesFile(file);
-    validatePackagesFile(resolver, file);
-  });
-
-  test("loadPackagesFile w/ loader", () async {
-    Future<List<int>> loader(Uri uri) async => packagesFile.codeUnits;
-    Uri file = Uri.parse("krutz://example.com/.packages");
-    Packages resolver = await loadPackagesFile(file, loader: loader);
-    validatePackagesFile(resolver, file);
-  });
-
-  generalTest("loadPackagesFile not found", {}, (Uri directory) async {
-    Uri file = directory.resolve(".packages");
-    expect(
-        loadPackagesFile(file),
-        throwsA(anyOf(new TypeMatcher<FileSystemException>(),
-            new TypeMatcher<HttpException>())));
-  });
-
-  generalTest("loadPackagesFile syntax error", {".packages": "syntax error"},
-      (Uri directory) async {
-    Uri file = directory.resolve(".packages");
-    expect(loadPackagesFile(file), throwsFormatException);
-  });
-
-  generalTest("getPackagesDir", {
-    "packages": {"foo": {}, "bar": {}, "baz": {}}
-  }, (Uri directory) async {
-    Uri packages = directory.resolve("packages/");
-    Packages resolver = getPackagesDirectory(packages);
-    Uri resolved = resolver.resolve(pkg("foo", "flip/flop"));
-    expect(resolved, packages.resolve("foo/flip/flop"));
-  });
-}
-
-/// Create a directory structure from [description] and run [fileTest].
-///
-/// Description is a map, each key is a file entry. If the value is a map,
-/// it's a sub-dir, otherwise it's a file and the value is the content
-/// as a string.
-void fileTest(String name, Map description, Future fileTest(Uri directory)) {
-  group("file-test", () {
-    Directory tempDir = Directory.systemTemp.createTempSync("file-test");
-    setUp(() {
-      _createFiles(tempDir, description);
+    }, (Directory directory) async {
+      PackageConfig config = await findPackageConfig(directory);
+      expect(config.version, 2); // Found package_config.json file.
+      validatePackagesFile(config, directory);
     });
-    tearDown(() {
-      tempDir.deleteSync(recursive: true);
-    });
-    test(name, () => fileTest(new Uri.file(path.join(tempDir.path, "."))));
-  });
-}
 
-/// HTTP-server the directory structure from [description] and run [htpTest].
-///
-/// Description is a map, each key is a file entry. If the value is a map,
-/// it's a sub-dir, otherwise it's a file and the value is the content
-/// as a string.
-void httpTest(String name, Map description, Future httpTest(Uri directory)) {
-  group("http-test", () {
-    var serverSub;
-    var uri;
-    setUp(() {
-      return HttpServer.bind(InternetAddress.loopbackIPv4, 0).then((server) {
-        uri = new Uri(
-            scheme: "http", host: "127.0.0.1", port: server.port, path: "/");
-        serverSub = server.listen((HttpRequest request) {
-          // No error handling.
-          var path = request.uri.path;
-          if (path.startsWith('/')) path = path.substring(1);
-          if (path.endsWith('/')) path = path.substring(0, path.length - 1);
-          var parts = path.split('/');
-          dynamic fileOrDir = description;
-          for (int i = 0; i < parts.length; i++) {
-            fileOrDir = fileOrDir[parts[i]];
-            if (fileOrDir == null) {
-              request.response.statusCode = 404;
-              request.response.close();
-              return;
-            }
-          }
-          request.response.write(fileOrDir);
-          request.response.close();
-        });
+    // Finds .packages if no package_config.json.
+    fileTest(".packages", {
+      ".packages": packagesFile,
+      "script.dart": "main(){}",
+      "packages": {"shouldNotBeFound": {}}
+    }, (Directory directory) async {
+      PackageConfig config = await findPackageConfig(directory);
+      expect(config.version, 1); // Found .packages file.
+      validatePackagesFile(config, directory);
+    });
+
+    // Finds package_config.json in super-directory.
+    fileTest("package_config.json recursive", {
+      ".packages": packagesFile,
+      ".dart_tool": {
+        "package_config.json": packageConfigFile,
+      },
+      "subdir": {
+        "script.dart": "main(){}",
+      }
+    }, (Directory directory) async {
+      PackageConfig config =
+          await findPackageConfig(subdir(directory, "subdir/"));
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    // Finds .packages in super-directory.
+    fileTest(".packages recursive", {
+      ".packages": packagesFile,
+      "subdir": {"script.dart": "main(){}"}
+    }, (Directory directory) async {
+      PackageConfig config;
+      config = await findPackageConfig(subdir(directory, "subdir/"));
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    // Does not find a packages/ directory, and returns null if nothing found.
+    fileTest("package directory packages not supported", {
+      "packages": {
+        "foo": {},
+      }
+    }, (Directory directory) async {
+      PackageConfig config = await findPackageConfig(directory);
+      expect(config, null);
+    });
+
+    fileTest("invalid .packages", {
+      ".packages": "not a .packages file",
+    }, (Directory directory) {
+      expect(() => findPackageConfig(directory),
+          throwsA(TypeMatcher<FormatException>()));
+    });
+
+    fileTest("invalid .packages as JSON", {
+      ".packages": packageConfigFile,
+    }, (Directory directory) {
+      expect(() => findPackageConfig(directory),
+          throwsA(TypeMatcher<FormatException>()));
+    });
+
+    fileTest("invalid .packages", {
+      ".dart_tool": {
+        "package_config.json": "not a JSON file",
+      }
+    }, (Directory directory) {
+      expect(() => findPackageConfig(directory),
+          throwsA(TypeMatcher<FormatException>()));
+    });
+
+    fileTest("invalid .packages as INI", {
+      ".dart_tool": {
+        "package_config.json": packagesFile,
+      }
+    }, (Directory directory) {
+      expect(() => findPackageConfig(directory),
+          throwsA(TypeMatcher<FormatException>()));
+    });
+  });
+
+  group("loadPackageConfig", () {
+    // Load a specific files
+    group("package_config.json", () {
+      var files = {
+        ".packages": packagesFile,
+        ".dart_tool": {
+          "package_config.json": packageConfigFile,
+        },
+      };
+      fileTest("directly", files, (Directory directory) async {
+        File file =
+            dirFile(subdir(directory, ".dart_tool"), "package_config.json");
+        PackageConfig config = await loadPackageConfig(file);
+        expect(config.version, 2);
+        validatePackagesFile(config, directory);
+      });
+      fileTest("indirectly through .packages", files,
+          (Directory directory) async {
+        File file = dirFile(directory, ".packages");
+        PackageConfig config = await loadPackageConfig(file);
+        expect(config.version, 2);
+        validatePackagesFile(config, directory);
       });
     });
-    tearDown(() => serverSub.cancel());
-    test(name, () => httpTest(uri));
-  });
-}
 
-void generalTest(String name, Map description, Future action(Uri location)) {
-  fileTest(name, description, action);
-  httpTest(name, description, action);
-}
+    fileTest("package_config.json non-default name", {
+      ".packages": packagesFile,
+      "subdir": {
+        "pheldagriff": packageConfigFile,
+      },
+    }, (Directory directory) async {
+      File file = dirFile(directory, "subdir/pheldagriff");
+      PackageConfig config = await loadPackageConfig(file);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
 
-void _createFiles(Directory target, Map description) {
-  description.forEach((name, content) {
-    if (content is Map) {
-      Directory subDir = new Directory(path.join(target.path, name));
-      subDir.createSync();
-      _createFiles(subDir, content);
-    } else {
-      File file = new File(path.join(target.path, name));
-      file.writeAsStringSync(content, flush: true);
-    }
+    fileTest("package_config.json named .packages", {
+      "subdir": {
+        ".packages": packageConfigFile,
+      },
+    }, (Directory directory) async {
+      File file = dirFile(directory, "subdir/.packages");
+      PackageConfig config = await loadPackageConfig(file);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    fileTest(".packages", {
+      ".packages": packagesFile,
+    }, (Directory directory) async {
+      File file = dirFile(directory, ".packages");
+      PackageConfig config = await loadPackageConfig(file);
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    fileTest(".packages non-default name", {
+      "pheldagriff": packagesFile,
+    }, (Directory directory) async {
+      File file = dirFile(directory, "pheldagriff");
+      PackageConfig config = await loadPackageConfig(file);
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    fileTest("no config found", {}, (Directory directory) {
+      File file = dirFile(directory, "anyname");
+      expect(() => loadPackageConfig(file),
+          throwsA(TypeMatcher<FileSystemException>()));
+    });
+
+    fileTest("specified file syntax error", {
+      "anyname": "syntax error",
+    }, (Directory directory) {
+      File file = dirFile(directory, "anyname");
+      expect(() => loadPackageConfig(file), throwsFormatException);
+    });
+
+    // Find package_config.json in subdir even if initial file syntax error.
+    fileTest("specified file syntax error", {
+      "anyname": "syntax error",
+      ".dart_tool": {
+        "package_config.json": packageConfigFile,
+      },
+    }, (Directory directory) async {
+      File file = dirFile(directory, "anyname");
+      PackageConfig config = await loadPackageConfig(file);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    // A file starting with `{` is a package_config.json file.
+    fileTest("file syntax error with {", {
+      ".packages": "{syntax error",
+    }, (Directory directory) {
+      File file = dirFile(directory, ".packages");
+      expect(() => loadPackageConfig(file), throwsFormatException);
+    });
   });
 }
diff --git a/test/discovery_uri_test.dart b/test/discovery_uri_test.dart
new file mode 100644
index 0000000..414a43a
--- /dev/null
+++ b/test/discovery_uri_test.dart
@@ -0,0 +1,256 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library package_config.discovery_test;
+
+import "dart:io";
+import "package:test/test.dart";
+import "package:package_config_2/package_config.dart";
+
+import "src/util.dart";
+
+const packagesFile = """
+# A comment
+foo:file:///dart/packages/foo/
+bar:/dart/packages/bar/
+baz:packages/baz/
+""";
+
+const packageConfigFile = """
+{
+  "configVersion": 2,
+  "packages": [
+    {
+      "name": "foo",
+      "rootUri": "file:///dart/packages/foo/"
+    },
+    {
+      "name": "bar",
+      "rootUri": "/dart/packages/bar/"
+    },
+    {
+      "name": "baz",
+      "rootUri": "../packages/baz/"
+    }
+  ],
+  "extra": [42]
+}
+""";
+
+void validatePackagesFile(PackageConfig resolver, Uri directory) {
+  expect(resolver, isNotNull);
+  expect(resolver.resolve(pkg("foo", "bar/baz")),
+      equals(Uri.parse("file:///dart/packages/foo/bar/baz")));
+  expect(resolver.resolve(pkg("bar", "baz/qux")),
+      equals(directory.resolve("/dart/packages/bar/baz/qux")));
+  expect(resolver.resolve(pkg("baz", "qux/foo")),
+      equals(directory.resolve("packages/baz/qux/foo")));
+  expect([for (var p in resolver.packages) p.name],
+      unorderedEquals(["foo", "bar", "baz"]));
+}
+
+main() {
+  group("findPackages", () {
+    // Finds package_config.json if there.
+    loaderTest("package_config.json", {
+      ".packages": "invalid .packages file",
+      "script.dart": "main(){}",
+      "packages": {"shouldNotBeFound": {}},
+      ".dart_tool": {
+        "package_config.json": packageConfigFile,
+      }
+    }, (Uri directory, loader) async {
+      PackageConfig config =
+          await findPackageConfigUri(directory, loader: loader);
+      expect(config.version, 2); // Found package_config.json file.
+      validatePackagesFile(config, directory);
+    });
+
+    // Finds .packages if no package_config.json.
+    loaderTest(".packages", {
+      ".packages": packagesFile,
+      "script.dart": "main(){}",
+      "packages": {"shouldNotBeFound": {}}
+    }, (Uri directory, loader) async {
+      PackageConfig config =
+          await findPackageConfigUri(directory, loader: loader);
+      expect(config.version, 1); // Found .packages file.
+      validatePackagesFile(config, directory);
+    });
+
+    // Finds package_config.json in super-directory.
+    loaderTest("package_config.json recursive", {
+      ".packages": packagesFile,
+      ".dart_tool": {
+        "package_config.json": packageConfigFile,
+      },
+      "subdir": {
+        "script.dart": "main(){}",
+      }
+    }, (Uri directory, loader) async {
+      PackageConfig config = await findPackageConfigUri(
+          directory.resolve("subdir/"),
+          loader: loader);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    // Finds .packages in super-directory.
+    loaderTest(".packages recursive", {
+      ".packages": packagesFile,
+      "subdir": {"script.dart": "main(){}"}
+    }, (Uri directory, loader) async {
+      PackageConfig config;
+      config = await findPackageConfigUri(directory.resolve("subdir/"),
+          loader: loader);
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    // Does not find a packages/ directory, and returns null if nothing found.
+    loaderTest("package directory packages not supported", {
+      "packages": {
+        "foo": {},
+      }
+    }, (Uri directory, loader) async {
+      PackageConfig config =
+          await findPackageConfigUri(directory, loader: loader);
+      expect(config, null);
+    });
+
+    loaderTest("invalid .packages", {
+      ".packages": "not a .packages file",
+    }, (Uri directory, loader) {
+      expect(() => findPackageConfigUri(directory, loader: loader),
+          throwsA(TypeMatcher<FormatException>()));
+    });
+
+    loaderTest("invalid .packages as JSON", {
+      ".packages": packageConfigFile,
+    }, (Uri directory, loader) {
+      expect(() => findPackageConfigUri(directory, loader: loader),
+          throwsA(TypeMatcher<FormatException>()));
+    });
+
+    loaderTest("invalid .packages", {
+      ".dart_tool": {
+        "package_config.json": "not a JSON file",
+      }
+    }, (Uri directory, loader) {
+      expect(() => findPackageConfigUri(directory, loader: loader),
+          throwsA(TypeMatcher<FormatException>()));
+    });
+
+    loaderTest("invalid .packages as INI", {
+      ".dart_tool": {
+        "package_config.json": packagesFile,
+      }
+    }, (Uri directory, loader) {
+      expect(() => findPackageConfigUri(directory, loader: loader),
+          throwsA(TypeMatcher<FormatException>()));
+    });
+  });
+
+  group("loadPackageConfig", () {
+    // Load a specific files
+    group("package_config.json", () {
+      var files = {
+        ".packages": packagesFile,
+        ".dart_tool": {
+          "package_config.json": packageConfigFile,
+        },
+      };
+      loaderTest("directly", files, (Uri directory, loader) async {
+        Uri file = directory.resolve(".dart_tool/package_config.json");
+        PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+        expect(config.version, 2);
+        validatePackagesFile(config, directory);
+      });
+      loaderTest("indirectly through .packages", files,
+          (Uri directory, loader) async {
+        Uri file = directory.resolve(".packages");
+        PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+        expect(config.version, 2);
+        validatePackagesFile(config, directory);
+      });
+    });
+
+    loaderTest("package_config.json non-default name", {
+      ".packages": packagesFile,
+      "subdir": {
+        "pheldagriff": packageConfigFile,
+      },
+    }, (Uri directory, loader) async {
+      Uri file = directory.resolve("subdir/pheldagriff");
+      PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    loaderTest("package_config.json named .packages", {
+      "subdir": {
+        ".packages": packageConfigFile,
+      },
+    }, (Uri directory, loader) async {
+      Uri file = directory.resolve("subdir/.packages");
+      PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    loaderTest(".packages", {
+      ".packages": packagesFile,
+    }, (Uri directory, loader) async {
+      Uri file = directory.resolve(".packages");
+      PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    loaderTest(".packages non-default name", {
+      "pheldagriff": packagesFile,
+    }, (Uri directory, loader) async {
+      Uri file = directory.resolve("pheldagriff");
+      PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    loaderTest("no config found", {}, (Uri directory, loader) {
+      Uri file = directory.resolve("anyname");
+      expect(() => loadPackageConfigUri(file, loader: loader),
+          throwsArgumentError);
+    });
+
+    loaderTest("specified file syntax error", {
+      "anyname": "syntax error",
+    }, (Uri directory, loader) {
+      Uri file = directory.resolve("anyname");
+      expect(() => loadPackageConfigUri(file, loader: loader),
+          throwsFormatException);
+    });
+
+    // Find package_config.json in subdir even if initial file syntax error.
+    loaderTest("specified file syntax error", {
+      "anyname": "syntax error",
+      ".dart_tool": {
+        "package_config.json": packageConfigFile,
+      },
+    }, (Uri directory, loader) async {
+      Uri file = directory.resolve("anyname");
+      PackageConfig config = await loadPackageConfigUri(file, loader: loader);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    // A file starting with `{` is a package_config.json file.
+    loaderTest("file syntax error with {", {
+      ".packages": "{syntax error",
+    }, (Uri directory, loader) {
+      Uri file = directory.resolve(".packages");
+      expect(() => loadPackageConfigUri(file, loader: loader),
+          throwsFormatException);
+    });
+  });
+}
diff --git a/test/parse_test.dart b/test/parse_test.dart
index b9b1bb5..235f493 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -1,245 +1,312 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-library package_config.parse_test;
+import "dart:io";
+import "dart:convert";
 
-import "package:package_config/packages.dart";
-import "package:package_config/packages_file.dart" show parse;
-import "package:package_config/src/packages_impl.dart";
 import "package:test/test.dart";
 
-main() {
-  var base = Uri.parse("file:///one/two/three/packages.map");
-  test("empty", () {
-    var packages = doParse(emptySample, base);
-    expect(packages.asMap(), isEmpty);
-  });
-  test("comment only", () {
-    var packages = doParse(commentOnlySample, base);
-    expect(packages.asMap(), isEmpty);
-  });
-  test("empty lines only", () {
-    var packages = doParse(emptyLinesSample, base);
-    expect(packages.asMap(), isEmpty);
-  });
+import "package:package_config_2/src/packages_file.dart" as packages;
+import "package:package_config_2/src/package_config_json.dart";
+import "src/util.dart";
 
-  test("empty lines only", () {
-    var packages = doParse(emptyLinesSample, base);
-    expect(packages.asMap(), isEmpty);
-  });
+void main() {
+  group(".packages", () {
+    test("valid", () {
+      var packagesFile = "# Generated by pub yadda yadda\n"
+          "foo:file:///foo/lib/\n"
+          "bar:/bar/lib/\n"
+          "baz:lib/\n";
+      var result = packages.parse(
+          utf8.encode(packagesFile), Uri.parse("file:///tmp/file.dart"));
+      expect(result.version, 1);
+      expect({for (var p in result.packages) p.name}, {"foo", "bar", "baz"});
+      expect(result.resolve(pkg("foo", "foo.dart")),
+          Uri.parse("file:///foo/lib/foo.dart"));
+      expect(result.resolve(pkg("bar", "bar.dart")),
+          Uri.parse("file:///bar/lib/bar.dart"));
+      expect(result.resolve(pkg("baz", "baz.dart")),
+          Uri.parse("file:///tmp/lib/baz.dart"));
 
-  test("single", () {
-    var packages = doParse(singleRelativeSample, base);
-    expect(packages.packages.toList(), equals(["foo"]));
-    expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
-        equals(base.resolve("../test/").resolve("bar/baz.dart")));
-  });
-
-  test("single no slash", () {
-    var packages = doParse(singleRelativeSampleNoSlash, base);
-    expect(packages.packages.toList(), equals(["foo"]));
-    expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
-        equals(base.resolve("../test/").resolve("bar/baz.dart")));
-  });
-
-  test("single no newline", () {
-    var packages = doParse(singleRelativeSampleNoNewline, base);
-    expect(packages.packages.toList(), equals(["foo"]));
-    expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
-        equals(base.resolve("../test/").resolve("bar/baz.dart")));
-  });
-
-  test("single absolute authority", () {
-    var packages = doParse(singleAbsoluteSample, base);
-    expect(packages.packages.toList(), equals(["foo"]));
-    expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
-        equals(Uri.parse("http://example.com/some/where/bar/baz.dart")));
-  });
-
-  test("single empty path", () {
-    var packages = doParse(singleEmptyPathSample, base);
-    expect(packages.packages.toList(), equals(["foo"]));
-    expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
-        equals(base.replace(path: "${base.path}/bar/baz.dart")));
-  });
-
-  test("single absolute path", () {
-    var packages = doParse(singleAbsolutePathSample, base);
-    expect(packages.packages.toList(), equals(["foo"]));
-    expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
-        equals(base.replace(path: "/test/bar/baz.dart")));
-  });
-
-  test("multiple", () {
-    var packages = doParse(multiRelativeSample, base);
-    expect(packages.packages.toList()..sort(), equals(["bar", "foo"]));
-    expect(packages.resolve(Uri.parse("package:foo/bar/baz.dart")),
-        equals(base.resolve("../test/").resolve("bar/baz.dart")));
-    expect(packages.resolve(Uri.parse("package:bar/foo/baz.dart")),
-        equals(base.resolve("../test2/").resolve("foo/baz.dart")));
-  });
-
-  test("dot-dot 1", () {
-    var packages = doParse(singleRelativeSample, base);
-    expect(packages.packages.toList(), equals(["foo"]));
-    expect(packages.resolve(Uri.parse("package:foo/qux/../bar/baz.dart")),
-        equals(base.resolve("../test/").resolve("bar/baz.dart")));
-  });
-
-  test("all valid chars can be used in URI segment", () {
-    var packages = doParse(allValidCharsSample, base);
-    expect(packages.packages.toList(), equals([allValidChars]));
-    expect(packages.resolve(Uri.parse("package:$allValidChars/bar/baz.dart")),
-        equals(base.resolve("../test/").resolve("bar/baz.dart")));
-  });
-
-  test("no invalid chars accepted", () {
-    var map = {};
-    for (int i = 0; i < allValidChars.length; i++) {
-      map[allValidChars.codeUnitAt(i)] = true;
-    }
-    for (int i = 0; i <= 255; i++) {
-      if (map[i] == true) continue;
-      var char = new String.fromCharCode(i);
-      expect(() => doParse("x${char}x:x", null),
-          anyOf(throwsNoSuchMethodError, throwsFormatException));
-    }
-  });
-
-  test("no escapes", () {
-    expect(() => doParse("x%41x:x", base), throwsFormatException);
-  });
-
-  test("same name twice", () {
-    expect(
-        () => doParse(singleRelativeSample * 2, base), throwsFormatException);
-  });
-
-  test("disallow default package", () {
-    expect(() => doParse(":foo", base, allowDefaultPackage: false),
-        throwsFormatException);
-  });
-
-  test("allow default package", () {
-    var packages = doParse(":foo", base, allowDefaultPackage: true);
-    expect(packages.defaultPackageName, "foo");
-  });
-
-  test("allow default package name with dot", () {
-    var packages = doParse(":foo.bar", base, allowDefaultPackage: true);
-    expect(packages.defaultPackageName, "foo.bar");
-  });
-
-  test("not two default packages", () {
-    expect(() => doParse(":foo\n:bar", base, allowDefaultPackage: true),
-        throwsFormatException);
-  });
-
-  test("default package invalid package name", () {
-    // Not a valid *package name*.
-    expect(() => doParse(":foo/bar", base, allowDefaultPackage: true),
-        throwsFormatException);
-  });
-
-  group("metadata", () {
-    var packages = doParse(
-        ":foo\n"
-        "foo:foo#metafoo=1\n"
-        "bar:bar#metabar=2\n"
-        "baz:baz\n"
-        "qux:qux#metaqux1=3&metaqux2=4\n",
-        base,
-        allowDefaultPackage: true);
-    test("non-existing", () {
-      // non-package name.
-      expect(packages.packageMetadata("///", "f"), null);
-      expect(packages.packageMetadata("", "f"), null);
-      // unconfigured package name.
-      expect(packages.packageMetadata("absent", "f"), null);
-      // package name without that metadata
-      expect(packages.packageMetadata("foo", "notfoo"), null);
+      var foo = result["foo"];
+      expect(foo, isNotNull);
+      expect(foo.root, Uri.parse("file:///foo/lib/"));
+      expect(foo.packageUriRoot, Uri.parse("file:///foo/lib/"));
+      expect(foo.languageVersion, null);
     });
-    test("lookup", () {
-      expect(packages.packageMetadata("foo", "metafoo"), "1");
-      expect(packages.packageMetadata("bar", "metabar"), "2");
-      expect(packages.packageMetadata("qux", "metaqux1"), "3");
-      expect(packages.packageMetadata("qux", "metaqux2"), "4");
-    });
-    test("by library URI", () {
-      expect(
-          packages.libraryMetadata(
-              Uri.parse("package:foo/index.dart"), "metafoo"),
-          "1");
-      expect(
-          packages.libraryMetadata(
-              Uri.parse("package:bar/index.dart"), "metabar"),
-          "2");
-      expect(
-          packages.libraryMetadata(
-              Uri.parse("package:qux/index.dart"), "metaqux1"),
-          "3");
-      expect(
-          packages.libraryMetadata(
-              Uri.parse("package:qux/index.dart"), "metaqux2"),
-          "4");
-    });
-    test("by default package", () {
-      expect(
-          packages.libraryMetadata(
-              Uri.parse("file:///whatever.dart"), "metafoo"),
-          "1");
-    });
-  });
 
-  for (String invalidSample in invalid) {
-    test("invalid '$invalidSample'", () {
-      var result;
-      try {
-        result = doParse(invalidSample, base);
-      } on FormatException {
-        // expected
-        return;
+    test("valid empty", () {
+      var packagesFile = "# Generated by pub yadda yadda\n";
+      var result =
+          packages.parse(utf8.encode(packagesFile), Uri.file("/tmp/file.dart"));
+      expect(result.version, 1);
+      expect({for (var p in result.packages) p.name}, <String>{});
+    });
+
+    group("invalid", () {
+      var baseFile = Uri.file("/tmp/file.dart");
+      testThrows(String name, String content) {
+        test(name, () {
+          expect(() => packages.parse(utf8.encode(content), baseFile),
+              throwsA(TypeMatcher<FormatException>()));
+        });
       }
-      fail("Resolved to $result");
+
+      testThrows("repeated package name", "foo:lib/\nfoo:lib\n");
+      testThrows("no colon", "foo\n");
+      testThrows("empty package name", ":lib/\n");
+      testThrows("dot only package name", ".:lib/\n");
+      testThrows("dot only package name", "..:lib/\n");
+      testThrows("invalid package name character", "f\\o:lib/\n");
+      testThrows("package URI", "foo:package:bar/lib/");
+      testThrows("location with query", "f\\o:lib/?\n");
+      testThrows("location with fragment", "f\\o:lib/#\n");
     });
-  }
+  });
+
+  group("package_config.json", () {
+    test("valid", () {
+      var packageConfigFile = """
+        {
+          "configVersion": 2,
+          "packages": [
+            {
+              "name": "foo",
+              "rootUri": "file:///foo/",
+              "packageUri": "lib/",
+              "languageVersion": "2.5",
+              "nonstandard": true
+            },
+            {
+              "name": "bar",
+              "rootUri": "/bar/",
+              "packageUri": "lib/",
+              "languageVersion": "100.100"
+            },
+            {
+              "name": "baz",
+              "rootUri": "../",
+              "packageUri": "lib/"
+            }
+          ],
+          "generator": "pub",
+          "other": [42]
+        }
+        """;
+      var config = parsePackageConfigBytes(utf8.encode(packageConfigFile),
+          Uri.parse("file:///tmp/.dart_tool/file.dart"));
+      expect(config.version, 2);
+      expect({for (var p in config.packages) p.name}, {"foo", "bar", "baz"});
+
+      expect(config.resolve(pkg("foo", "foo.dart")),
+          Uri.parse("file:///foo/lib/foo.dart"));
+      expect(config.resolve(pkg("bar", "bar.dart")),
+          Uri.parse("file:///bar/lib/bar.dart"));
+      expect(config.resolve(pkg("baz", "baz.dart")),
+          Uri.parse("file:///tmp/lib/baz.dart"));
+
+      var foo = config["foo"];
+      expect(foo, isNotNull);
+      expect(foo.root, Uri.parse("file:///foo/"));
+      expect(foo.packageUriRoot, Uri.parse("file:///foo/lib/"));
+      expect(foo.languageVersion, "2.5");
+      expect(foo.extraData, {"nonstandard": true});
+
+      var bar = config["bar"];
+      expect(bar, isNotNull);
+      expect(bar.root, Uri.parse("file:///bar/"));
+      expect(bar.packageUriRoot, Uri.parse("file:///bar/lib/"));
+      expect(bar.languageVersion, "100.100");
+      expect(bar.extraData, null);
+
+      var baz = config["baz"];
+      expect(baz, isNotNull);
+      expect(baz.root, Uri.parse("file:///tmp/"));
+      expect(baz.packageUriRoot, Uri.parse("file:///tmp/lib/"));
+      expect(baz.languageVersion, null);
+
+      expect(config.extraData, {
+        "generator": "pub",
+        "other": [42]
+      });
+    });
+
+    test("valid other order", () {
+      // The ordering in the file is not important.
+      var packageConfigFile = """
+        {
+          "generator": "pub",
+          "other": [42],
+          "packages": [
+            {
+              "languageVersion": "2.5",
+              "packageUri": "lib/",
+              "rootUri": "file:///foo/",
+              "name": "foo"
+            },
+            {
+              "packageUri": "lib/",
+              "languageVersion": "100.100",
+              "rootUri": "/bar/",
+              "name": "bar"
+            },
+            {
+              "packageUri": "lib/",
+              "name": "baz",
+              "rootUri": "../"
+            }
+          ],
+          "configVersion": 2
+        }
+        """;
+      var config = parsePackageConfigBytes(utf8.encode(packageConfigFile),
+          Uri.parse("file:///tmp/.dart_tool/file.dart"));
+      expect(config.version, 2);
+      expect({for (var p in config.packages) p.name}, {"foo", "bar", "baz"});
+
+      expect(config.resolve(pkg("foo", "foo.dart")),
+          Uri.parse("file:///foo/lib/foo.dart"));
+      expect(config.resolve(pkg("bar", "bar.dart")),
+          Uri.parse("file:///bar/lib/bar.dart"));
+      expect(config.resolve(pkg("baz", "baz.dart")),
+          Uri.parse("file:///tmp/lib/baz.dart"));
+      expect(config.extraData, {
+        "generator": "pub",
+        "other": [42]
+      });
+    });
+
+    // Check that a few minimal configurations are valid.
+    // These form the basis of invalid tests below.
+    var cfg = '"configVersion":2';
+    var pkgs = '"packages":[]';
+    var name = '"name":"foo"';
+    var root = '"rootUri":"/foo/"';
+    test("minimal", () {
+      var config = parsePackageConfigBytes(utf8.encode("{$cfg,$pkgs}"),
+          Uri.parse("file:///tmp/.dart_tool/file.dart"));
+      expect(config.version, 2);
+      expect(config.packages, isEmpty);
+    });
+    test("minimal package", () {
+      // A package must have a name and a rootUri, the remaining properties
+      // are optional.
+      var config = parsePackageConfigBytes(
+          utf8.encode('{$cfg,"packages":[{$name,$root}]}'),
+          Uri.parse("file:///tmp/.dart_tool/file.dart"));
+      expect(config.version, 2);
+      expect(config.packages.first.name, "foo");
+    });
+
+    group("invalid", () {
+      testThrows(String name, String source) {
+        test(name, () {
+          expect(
+              () => parsePackageConfigBytes(utf8.encode(source),
+                  Uri.parse("file:///tmp/.dart_tool/file.dart")),
+              throwsA(TypeMatcher<FormatException>()));
+        });
+      }
+
+      testThrows("comment", '# comment\n {$cfg,$pkgs}');
+      testThrows(".packages file", 'foo:/foo\n');
+      testThrows("no configVersion", '{$pkgs}');
+      testThrows("no packages", '{$cfg}');
+      group("config version:", () {
+        testThrows("null", '{"configVersion":null,$pkgs}');
+        testThrows("string", '{"configVersion":"2",$pkgs}');
+        testThrows("array", '{"configVersion":[2],$pkgs}');
+      });
+      group("packages:", () {
+        testThrows("null", '{$cfg,"packages":null}');
+        testThrows("string", '{$cfg,"packages":"foo"}');
+        testThrows("object", '{$cfg,"packages":{}}');
+      });
+      group("packages entry:", () {
+        testThrows("null", '{$cfg,"packages":[null]}');
+        testThrows("string", '{$cfg,"packages":["foo"]}');
+        testThrows("array", '{$cfg,"packages":[[]]}');
+      });
+      group("package", () {
+        testThrows("no name", '{$cfg,"packages":[{$root}]}');
+        group("name:", () {
+          testThrows("null", '{$cfg,"packages":[{"name":null,$root}]}');
+          testThrows("num", '{$cfg,"packages":[{"name":1,$root}]}');
+          testThrows("object", '{$cfg,"packages":[{"name":{},$root}]}');
+          testThrows("empty", '{$cfg,"packages":[{"name":"",$root}]}');
+          testThrows("one-dot", '{$cfg,"packages":[{"name":".",$root}]}');
+          testThrows("two-dot", '{$cfg,"packages":[{"name":"..",$root}]}');
+          testThrows(
+              "invalid char '\\'", '{$cfg,"packages":[{"name":"\\",$root}]}');
+          testThrows(
+              "invalid char ':'", '{$cfg,"packages":[{"name":":",$root}]}');
+          testThrows(
+              "invalid char ' '", '{$cfg,"packages":[{"name":" ",$root}]}');
+        });
+
+        testThrows("no root", '{$cfg,"packages":[{$name}]}');
+        group("root:", () {
+          testThrows("null", '{$cfg,"packages":[{$name,"rootUri":null}]}');
+          testThrows("num", '{$cfg,"packages":[{$name,"rootUri":1}]}');
+          testThrows("object", '{$cfg,"packages":[{$name,"rootUri":{}}]}');
+          testThrows("fragment", '{$cfg,"packages":[{$name,"rootUri":"x/#"}]}');
+          testThrows("query", '{$cfg,"packages":[{$name,"rootUri":"x/?"}]}');
+          testThrows("package-URI",
+              '{$cfg,"packages":[{$name,"rootUri":"package:x/x/"}]}');
+        });
+        group("package-URI root:", () {
+          testThrows(
+              "null", '{$cfg,"packages":[{$name,$root,"packageUri":null}]}');
+          testThrows("num", '{$cfg,"packages":[{$name,$root,"packageUri":1}]}');
+          testThrows(
+              "object", '{$cfg,"packages":[{$name,$root,"packageUri":{}}]}');
+          testThrows("fragment",
+              '{$cfg,"packages":[{$name,$root,"packageUri":"x/#"}]}');
+          testThrows(
+              "query", '{$cfg,"packages":[{$name,$root,"packageUri":"x/?"}]}');
+          testThrows("package: URI",
+              '{$cfg,"packages":[{$name,$root,"packageUri":"package:x/x/"}]}');
+          testThrows("not inside root",
+              '{$cfg,"packages":[{$name,$root,"packageUri":"../other/"}]}');
+        });
+        group("language version", () {
+          testThrows("null",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":null}]}');
+          testThrows(
+              "num", '{$cfg,"packages":[{$name,$root,"languageVersion":1}]}');
+          testThrows("object",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":{}}]}');
+          testThrows("empty",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":""}]}');
+          testThrows("non number.number",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":"x.1"}]}');
+          testThrows("number.non number",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.x"}]}');
+          testThrows("non number",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":"x"}]}');
+          testThrows("one number",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":"1"}]}');
+          testThrows("three numbers",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.2.3"}]}');
+          testThrows("leading zero first",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":"01.1"}]}');
+          testThrows("leading zero second",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.01"}]}');
+          testThrows("trailing-",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1-1"}]}');
+          testThrows("trailing+",
+              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1+1"}]}');
+        });
+      });
+      testThrows("duplicate package name",
+          '{$cfg,"packages":[{$name,$root},{$name,"rootUri":"/other/"}]}');
+      testThrows("same roots",
+          '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}');
+      testThrows(
+          "overlapping roots",
+          '{$cfg,"packages":[{$name,$root},'
+              '{"name":"bar","rootUri":"/foo/sub/"}]}');
+    });
+  });
 }
-
-Packages doParse(String sample, Uri baseUri,
-    {bool allowDefaultPackage = false}) {
-  Map<String, Uri> map = parse(sample.codeUnits, baseUri,
-      allowDefaultPackage: allowDefaultPackage);
-  return new MapPackages(map);
-}
-
-// Valid samples.
-var emptySample = "";
-var commentOnlySample = "# comment only\n";
-var emptyLinesSample = "\n\n\r\n";
-var singleRelativeSample = "foo:../test/\n";
-var singleRelativeSampleNoSlash = "foo:../test\n";
-var singleRelativeSampleNoNewline = "foo:../test/";
-var singleAbsoluteSample = "foo:http://example.com/some/where/\n";
-var singleEmptyPathSample = "foo:\n";
-var singleAbsolutePathSample = "foo:/test/\n";
-var multiRelativeSample = "foo:../test/\nbar:../test2/\n";
-// All valid path segment characters in an URI.
-var allValidChars = r"!$&'()*+,-.0123456789;="
-    r"@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~";
-
-var allValidCharsSample = "${allValidChars}:../test/\n";
-
-// Invalid samples.
-var invalid = [
-  ":baz.dart", // empty.
-  "foobar=baz.dart", // no colon (but an equals, which is not the same)
-  ".:../test/", // dot segment
-  "..:../test/", // dot-dot segment
-  "...:../test/", // dot-dot-dot segment
-  "foo/bar:../test/", // slash in name
-  "/foo:../test/", // slash at start of name
-  "?:../test/", // invalid characters.
-  "[:../test/", // invalid characters.
-  "x#:../test/", // invalid characters.
-];
diff --git a/test/parse_write_test.dart b/test/parse_write_test.dart
deleted file mode 100644
index 415b479..0000000
--- a/test/parse_write_test.dart
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library package_config.parse_write_test;
-
-import "dart:convert" show utf8;
-import "package:package_config/packages_file.dart";
-import "package:test/test.dart";
-
-main() {
-  testBase(baseDirString) {
-    var baseDir = Uri.parse(baseDirString);
-    group("${baseDir.scheme} base", () {
-      Uri packagesFile = baseDir.resolve(".packages");
-
-      roundTripTest(String name, Map<String, Uri> map) {
-        group(name, () {
-          test("write with no baseUri", () {
-            var content = writeToString(map).codeUnits;
-            var resultMap = parse(content, packagesFile);
-            expect(resultMap, map);
-          });
-
-          test("write with base directory", () {
-            var content = writeToString(map, baseUri: baseDir).codeUnits;
-            var resultMap = parse(content, packagesFile);
-            expect(resultMap, map);
-          });
-
-          test("write with base .packages file", () {
-            var content = writeToString(map, baseUri: packagesFile).codeUnits;
-            var resultMap = parse(content, packagesFile);
-            expect(resultMap, map);
-          });
-
-          test("write with defaultPackageName", () {
-            var content = writeToString(
-              {'': Uri.parse('my_pkg')}..addAll(map),
-              allowDefaultPackage: true,
-            ).codeUnits;
-            var resultMap = parse(
-              content,
-              packagesFile,
-              allowDefaultPackage: true,
-            );
-            expect(resultMap[''].toString(), 'my_pkg');
-            expect(
-              resultMap,
-              {'': Uri.parse('my_pkg')}..addAll(map),
-            );
-          });
-
-          test("write with defaultPackageName (utf8)", () {
-            var content = utf8.encode(writeToString(
-              {'': Uri.parse('my_pkg')}..addAll(map),
-              allowDefaultPackage: true,
-            ));
-            var resultMap = parse(
-              content,
-              packagesFile,
-              allowDefaultPackage: true,
-            );
-            expect(resultMap[''].toString(), 'my_pkg');
-            expect(
-              resultMap,
-              {'': Uri.parse('my_pkg')}..addAll(map),
-            );
-          });
-        });
-      }
-
-      var lowerDir = baseDir.resolve("path3/path4/");
-      var higherDir = baseDir.resolve("../");
-      var parallelDir = baseDir.resolve("../path3/");
-      var rootDir = baseDir.resolve("/");
-      var fileDir = Uri.parse("file:///path1/part2/");
-      var httpDir = Uri.parse("http://example.com/path1/path2/");
-      var otherDir = Uri.parse("other:/path1/path2/");
-
-      roundTripTest("empty", {});
-      roundTripTest("lower directory", {"foo": lowerDir});
-      roundTripTest("higher directory", {"foo": higherDir});
-      roundTripTest("parallel directory", {"foo": parallelDir});
-      roundTripTest("same directory", {"foo": baseDir});
-      roundTripTest("root directory", {"foo": rootDir});
-      roundTripTest("file directory", {"foo": fileDir});
-      roundTripTest("http directory", {"foo": httpDir});
-      roundTripTest("other scheme directory", {"foo": otherDir});
-      roundTripTest("multiple same-type directories",
-          {"foo": lowerDir, "bar": higherDir, "baz": parallelDir});
-      roundTripTest("multiple scheme directories",
-          {"foo": fileDir, "bar": httpDir, "baz": otherDir});
-      roundTripTest("multiple scheme directories and mutliple same type", {
-        "foo": fileDir,
-        "bar": httpDir,
-        "baz": otherDir,
-        "qux": lowerDir,
-        "hip": higherDir,
-        "dep": parallelDir
-      });
-    });
-  }
-
-  testBase("file:///base1/base2/");
-  testBase("http://example.com/base1/base2/");
-  testBase("other:/base1/base2/");
-
-  // Check that writing adds the comment.
-  test("write preserves comment", () {
-    var comment = "comment line 1\ncomment line 2\ncomment line 3";
-    var result = writeToString({}, comment: comment);
-    // Comment with "# " before each line and "\n" after last.
-    var expectedComment =
-        "# comment line 1\n# comment line 2\n# comment line 3\n";
-    expect(result, startsWith(expectedComment));
-  });
-}
-
-String writeToString(
-  Map<String, Uri> map, {
-  Uri baseUri,
-  String comment,
-  bool allowDefaultPackage = false,
-}) {
-  var buffer = new StringBuffer();
-  write(buffer, map,
-      baseUri: baseUri,
-      comment: comment,
-      allowDefaultPackage: allowDefaultPackage);
-  return buffer.toString();
-}
diff --git a/test/src/util.dart b/test/src/util.dart
new file mode 100644
index 0000000..ec4e5e8
--- /dev/null
+++ b/test/src/util.dart
@@ -0,0 +1,109 @@
+// Copyright (c) 2019, 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:convert';
+import "dart:io";
+import 'dart:typed_data';
+
+import "package:path/path.dart" as path;
+import "package:test/test.dart";
+
+/// Creates a directory structure from [description] and runs [fileTest].
+///
+/// Description is a map, each key is a file entry. If the value is a map,
+/// it's a subdirectory, otherwise it's a file and the value is the content
+/// as a string.
+/// Introduces a group to hold the [setUp]/[tearDown] logic.
+void fileTest(String name, Map<String, Object> description,
+    void fileTest(Directory directory)) {
+  group("file-test", () {
+    Directory tempDir = Directory.systemTemp.createTempSync("pkgcfgtest");
+    setUp(() {
+      _createFiles(tempDir, description);
+    });
+    tearDown(() {
+      tempDir.deleteSync(recursive: true);
+    });
+    test(name, () => fileTest(tempDir));
+  });
+}
+
+/// Creates a set of files under a new temporary directory.
+/// Returns the temporary directory.
+///
+/// The [description] is a map from file names to content.
+/// If the content is again a map, it represents a subdirectory
+/// with the content as description.
+/// Otherwise the content should be a string,
+/// which is written to the file as UTF-8.
+Directory createTestFiles(Map<String, Object> description) {
+  var target = Directory.systemTemp.createTempSync("pkgcfgtest");
+  _createFiles(target, description);
+  return target;
+}
+
+// Creates temporary files in the target directory.
+void _createFiles(Directory target, Map<Object, Object> description) {
+  description.forEach((name, content) {
+    var entryName = path.join(target.path, "$name");
+    if (content is Map<Object, Object>) {
+      _createFiles(Directory(entryName)..createSync(), content);
+    } else {
+      File(entryName).writeAsStringSync(content, flush: true);
+    }
+  });
+}
+
+/// Creates a [Directory] for a subdirectory of [parent].
+Directory subdir(Directory parent, String dirName) =>
+    Directory(path.joinAll([parent.path, ...dirName.split("/")]));
+
+/// Creates a [File] for an entry in the [directory] directory.
+File dirFile(Directory directory, String fileName) =>
+    File(path.join(directory.path, fileName));
+
+/// Creates a package: URI.
+Uri pkg(String packageName, String packagePath) {
+  var path =
+      "$packageName${packagePath.startsWith('/') ? "" : "/"}$packagePath";
+  return new Uri(scheme: "package", path: path);
+}
+
+// Remove if not used.
+String configFromPackages(List<List<String>> packages) => """
+{
+  "configVersion": 2,
+  "packages": [
+${packages.map((nu) => """
+    {
+      "name": "${nu[0]}",
+      "rootUri": "${nu[1]}"
+    }""").join(",\n")}
+  ]
+}
+""";
+
+/// Mimics a directory structure of [description] and runs [fileTest].
+///
+/// Description is a map, each key is a file entry. If the value is a map,
+/// it's a subdirectory, otherwise it's a file and the value is the content
+/// as a string.
+void loaderTest(String name, Map<String, Object> description,
+    void loaderTest(Uri root, Future<Uint8List> loader(Uri uri))) {
+  Uri root = Uri(scheme: "test", path: "/");
+  Future<Uint8List> loader(Uri uri) async {
+    var path = uri.path;
+    if (!uri.isScheme("test") || !path.startsWith("/")) return null;
+    var parts = path.split("/");
+    dynamic value = description;
+    for (int i = 1; i < parts.length; i++) {
+      if (value is! Map<String, dynamic>) return null;
+      value = value[parts[i]];
+    }
+    if (value is String) return utf8.encode(value);
+    return null;
+  }
+
+  test(name, () => loaderTest(root, loader));
+}