Merge pull request #1262 from dart-lang/merge-source_maps-package

Merge `package:source_maps`
diff --git a/.github/ISSUE_TEMPLATE/package_config.md b/.github/ISSUE_TEMPLATE/package_config.md
new file mode 100644
index 0000000..f6322d0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/package_config.md
@@ -0,0 +1,5 @@
+---
+name: "package:package_config"
+about: "Create a bug or file a feature request against package:package_config."
+labels: "package:package_config"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/pool.md b/.github/ISSUE_TEMPLATE/pool.md
new file mode 100644
index 0000000..7af32c4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/pool.md
@@ -0,0 +1,5 @@
+---
+name: "package:pool"
+about: "Create a bug or file a feature request against package:pool."
+labels: "package:pool"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/pub_semver.md b/.github/ISSUE_TEMPLATE/pub_semver.md
new file mode 100644
index 0000000..c7db9b5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/pub_semver.md
@@ -0,0 +1,5 @@
+---
+name: "package:pub_semver"
+about: "Create a bug or file a feature request against package:pub_semver."
+labels: "package:pub_semver"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 4ef8355..85a16b4 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -80,6 +80,18 @@
   - changed-files:
     - any-glob-to-any-file: 'pkgs/oauth2/**'
 
+'package:package_config':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/package_config/**'
+
+'package:pool':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/pool/**'
+
+'package:pub_semver':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/pub_semver/**'
+
 'package:source_map_stack_trace':
   - changed-files:
     - any-glob-to-any-file: 'pkgs/source_map_stack_trace/**'
diff --git a/.github/workflows/package_config.yaml b/.github/workflows/package_config.yaml
new file mode 100644
index 0000000..416ea1a
--- /dev/null
+++ b/.github/workflows/package_config.yaml
@@ -0,0 +1,71 @@
+name: package:package_config
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/package_config.yml'
+      - 'pkgs/package_config/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/package_config.yml'
+      - 'pkgs/package_config/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/package_config/
+
+jobs:
+  # Check code formatting and static analysis on a single OS (linux)
+  # against Dart dev.
+  analyze:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Check formatting
+        run: dart format --output=none --set-exit-if-changed .
+        if: always() && steps.install.outcome == 'success'
+      - name: Analyze code
+        run: dart analyze --fatal-infos
+        if: always() && steps.install.outcome == 'success'
+
+  # Run tests on a matrix consisting of two dimensions:
+  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+  # 2. release channel: dev
+  test:
+    needs: analyze
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, windows-latest]
+        sdk: [3.4, dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Run tests
+        run: dart test -p chrome,vm
+        if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/pool.yaml b/.github/workflows/pool.yaml
new file mode 100644
index 0000000..6d64062
--- /dev/null
+++ b/.github/workflows/pool.yaml
@@ -0,0 +1,78 @@
+name: package:pool
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/pool.yaml'
+      - 'pkgs/pool/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/pool.yaml'
+      - 'pkgs/pool/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/pool/
+
+jobs:
+  # Check code formatting and static analysis on a single OS (linux)
+  # against Dart dev.
+  analyze:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Check formatting
+        run: dart format --output=none --set-exit-if-changed .
+        if: always() && steps.install.outcome == 'success'
+      - name: Analyze code
+        run: dart analyze --fatal-infos
+        if: always() && steps.install.outcome == 'success'
+
+  # Run tests on a matrix consisting of two dimensions:
+  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+  # 2. release channel: dev
+  test:
+    needs: analyze
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        # Add macos-latest and/or windows-latest if relevant for this package.
+        os: [ubuntu-latest]
+        sdk: [3.4, dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Run VM tests
+        run: dart test --platform vm
+        if: always() && steps.install.outcome == 'success'
+      - name: Run Chrome tests
+        run: dart test --platform chrome
+        if: always() && steps.install.outcome == 'success'
+      - name: Run Chrome tests - wasm
+        run: dart test --platform chrome -c dart2wasm
+        if: always() && steps.install.outcome == 'success' && matrix.sdk == 'dev'
diff --git a/.github/workflows/pub_semver.yaml b/.github/workflows/pub_semver.yaml
new file mode 100644
index 0000000..ba0db18
--- /dev/null
+++ b/.github/workflows/pub_semver.yaml
@@ -0,0 +1,75 @@
+name: package:pub_semver
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/pub_semver.yaml'
+      - 'pkgs/pub_semver/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/pub_semver.yaml'
+      - 'pkgs/pub_semver/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/pub_semver/
+
+jobs:
+  # Check code formatting and static analysis on a single OS (linux)
+  # against Dart dev.
+  analyze:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Check formatting
+        run: dart format --output=none --set-exit-if-changed .
+        if: always() && steps.install.outcome == 'success'
+      - name: Analyze code
+        run: dart analyze --fatal-infos
+        if: always() && steps.install.outcome == 'success'
+
+  # Run tests on a matrix consisting of two dimensions:
+  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+  # 2. release channel: dev
+  test:
+    needs: analyze
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        # Add macos-latest and/or windows-latest if relevant for this package.
+        os: [ubuntu-latest]
+        sdk: [3.4, dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Run VM tests
+        run: dart test --platform vm
+        if: always() && steps.install.outcome == 'success'
+      - name: Run Chrome tests
+        run: dart test --platform chrome --compiler dart2js,dart2wasm
+        if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index 84b3571..41e78cf 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,9 @@
 | [json_rpc_2](pkgs/json_rpc_2/) | Utilities to write a client or server using the JSON-RPC 2.0 spec. | [![package issues](https://img.shields.io/badge/package:json_rpc_2-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ajson_rpc_2) | [![pub package](https://img.shields.io/pub/v/json_rpc_2.svg)](https://pub.dev/packages/json_rpc_2) |
 | [mime](pkgs/mime/) | Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. | [![package issues](https://img.shields.io/badge/package:mime-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Amime) | [![pub package](https://img.shields.io/pub/v/mime.svg)](https://pub.dev/packages/mime) |
 | [oauth2](pkgs/oauth2/) | A client library for authenticating with a remote service via OAuth2 on behalf of a user, and making authorized HTTP requests with the user's OAuth2 credentials. | [![package issues](https://img.shields.io/badge/package:oauth2-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aoauth2) | [![pub package](https://img.shields.io/pub/v/oauth2.svg)](https://pub.dev/packages/oauth2) |
+| [package_config](pkgs/package_config/) | Support for reading and writing Dart Package Configuration files. | [![package issues](https://img.shields.io/badge/package:package_config-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apackage_config) | [![pub package](https://img.shields.io/pub/v/package_config.svg)](https://pub.dev/packages/package_config) |
+| [pool](pkgs/pool/) | Manage a finite pool of resources. Useful for controlling concurrent file system or network requests. | [![package issues](https://img.shields.io/badge/package:pool-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apool) | [![pub package](https://img.shields.io/pub/v/pool.svg)](https://pub.dev/packages/pool) |
+| [pub_semver](pkgs/pub_semver/) | Versions and version constraints implementing pub's versioning policy. This is very similar to vanilla semver, with a few corner cases. | [![package issues](https://img.shields.io/badge/package:pub_semver-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apub_semver) | [![pub package](https://img.shields.io/pub/v/pub_semver.svg)](https://pub.dev/packages/pub_semver) |
 | [source_map_stack_trace](pkgs/source_map_stack_trace/) | A package for applying source maps to stack traces. | [![package issues](https://img.shields.io/badge/package:source_map_stack_trace-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_map_stack_trace) | [![pub package](https://img.shields.io/pub/v/source_map_stack_trace.svg)](https://pub.dev/packages/source_map_stack_trace) |
 | [source_maps](pkgs/source_maps/) | A library to programmatically manipulate source map files. | [![package issues](https://img.shields.io/badge/package:source_maps-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_maps) | [![pub package](https://img.shields.io/pub/v/source_maps.svg)](https://pub.dev/packages/source_maps) |
 | [unified_analytics](pkgs/unified_analytics/) | A package for logging analytics for all Dart and Flutter related tooling to Google Analytics. | [![package issues](https://img.shields.io/badge/package:unified_analytics-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aunified_analytics) | [![pub package](https://img.shields.io/pub/v/unified_analytics.svg)](https://pub.dev/packages/unified_analytics) |
diff --git a/pkgs/package_config/.gitignore b/pkgs/package_config/.gitignore
new file mode 100644
index 0000000..7b888b8
--- /dev/null
+++ b/pkgs/package_config/.gitignore
@@ -0,0 +1,7 @@
+.packages
+.pub
+.dart_tool/
+.vscode/
+packages
+pubspec.lock
+doc/api/
diff --git a/pkgs/package_config/AUTHORS b/pkgs/package_config/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/package_config/AUTHORS
@@ -0,0 +1,6 @@
+# Below is a list of people and organizations that have contributed
+# to the project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
diff --git a/pkgs/package_config/CHANGELOG.md b/pkgs/package_config/CHANGELOG.md
new file mode 100644
index 0000000..101a0fe
--- /dev/null
+++ b/pkgs/package_config/CHANGELOG.md
@@ -0,0 +1,108 @@
+## 2.1.1
+
+- Require Dart 3.4
+- Move to `dart-lang/tools` monorepo.
+
+## 2.1.0
+
+- Adds `minVersion` to `findPackageConfig` and `findPackageConfigVersion`
+  which allows ignoring earlier versions (which currently only means
+  ignoring version 1, aka. `.packages` files.)
+
+- Changes the version number of `SimplePackageConfig.empty` to the
+  current maximum version.
+
+- Improve file read performance; improve lookup performance.
+- Emit an error when a package is inside the package root of another package.
+- Fix a link in the readme.
+
+## 2.0.2
+
+- Update package description and README.
+- Change to package:lints for style checking.
+- Add an example.
+
+## 2.0.1
+
+- Use unique library names to correct docs issue.
+
+## 2.0.0
+
+- Migrate to null safety.
+- Remove legacy APIs.
+- Adds `relativeRoot` property to `Package` which controls whether to
+  make the root URI relative when writing a configuration file.
+
+## 1.9.3
+
+- Fix `Package` constructor not accepting relative `packageUriRoot`.
+
+## 1.9.2
+
+- Updated to support new rules for picking `package_config.json` over
+  a specified `.packages`.
+- Deduce package root from `.packages` derived package configuration,
+  and default all such packages to language version 2.7.
+
+## 1.9.1
+
+- Remove accidental transitive import of `dart:io` from entrypoints that are
+  supposed to be cross-platform compatible.
+
+## 1.9.0
+
+- Based on new JSON file format with more content.
+- This version includes all the new functionality intended for a 2.0.0
+  version, as well as the, now deprecated, version 1 functionality.
+  When we release 2.0.0, the deprecated functionality will be removed.
+
+## 1.1.0
+
+- Allow parsing files with default-package entries and metadata.
+  A default-package entry has an empty key and a valid package name
+  as value.
+  Metadata is attached as fragments to base URIs.
+
+## 1.0.5
+
+- Fix usage of SDK constants.
+
+## 1.0.4
+
+- Set max SDK version to <3.0.0.
+
+## 1.0.3
+
+- Removed unneeded dependency constraint on SDK.
+
+## 1.0.2
+
+- Update SDK constraint to be 2.0.0 dev friendly.
+
+## 1.0.1
+
+- Fix test to not write to sink after it's closed.
+
+## 1.0.0
+
+- Public API marked stable.
+
+## 0.1.5
+
+- `FilePackagesDirectoryPackages.getBase(..)` performance improvements.
+
+## 0.1.4
+
+- Strong mode fixes.
+
+## 0.1.3
+
+- Invalid test cleanup (to keep up with changes in `Uri`).
+
+## 0.1.1
+
+- Syntax updates.
+
+## 0.1.0
+
+- Initial implementation.
diff --git a/pkgs/package_config/LICENSE b/pkgs/package_config/LICENSE
new file mode 100644
index 0000000..7670007
--- /dev/null
+++ b/pkgs/package_config/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/package_config/README.md b/pkgs/package_config/README.md
new file mode 100644
index 0000000..76fd3cb
--- /dev/null
+++ b/pkgs/package_config/README.md
@@ -0,0 +1,26 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/package_config.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/package_config.yaml)
+[![pub package](https://img.shields.io/pub/v/package_config.svg)](https://pub.dev/packages/package_config)
+[![package publisher](https://img.shields.io/pub/publisher/package_config.svg)](https://pub.dev/packages/package_config/publisher)
+
+Support for working with **Package Configuration** files as described
+in the Package Configuration v2 [design document](https://github.com/dart-lang/language/blob/master/accepted/2.8/language-versioning/package-config-file-v2.md).
+
+A Dart package configuration file is used to resolve Dart package names (e.g.
+`foobar`) to Dart files containing the source code for that package (e.g.
+`file:///Users/myuser/.pub-cache/hosted/pub.dartlang.org/foobar-1.1.0`). The
+standard package configuration file is `.dart_tool/package_config.json`, and is
+written by the Dart tool when the command `dart pub get` is run.
+
+The primary libraries of this package are
+* `package_config.dart`:
+    Defines the `PackageConfig` class and other types needed to use
+    package configurations, and provides functions to find, read and
+    write package configuration files.
+
+* `package_config_types.dart`:
+    Just the `PackageConfig` class and other types needed to use
+    package configurations. This library does not depend on `dart:io`.
+
+The package includes deprecated backwards compatible functionality to
+work with the `.packages` file. This functionality will not be maintained,
+and will be removed in a future version of this package.
diff --git a/pkgs/package_config/analysis_options.yaml b/pkgs/package_config/analysis_options.yaml
new file mode 100644
index 0000000..c0249e5
--- /dev/null
+++ b/pkgs/package_config/analysis_options.yaml
@@ -0,0 +1,5 @@
+# Copyright (c) 2020, 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.
+
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/package_config/example/main.dart b/pkgs/package_config/example/main.dart
new file mode 100644
index 0000000..db137ca
--- /dev/null
+++ b/pkgs/package_config/example/main.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2020, 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' show Directory;
+
+import 'package:package_config/package_config.dart';
+
+void main() async {
+  var packageConfig = await findPackageConfig(Directory.current);
+  if (packageConfig == null) {
+    print('Failed to locate or read package config.');
+  } else {
+    print('This package depends on ${packageConfig.packages.length} packages:');
+    for (var package in packageConfig.packages) {
+      print('- ${package.name}');
+    }
+  }
+}
diff --git a/pkgs/package_config/lib/package_config.dart b/pkgs/package_config/lib/package_config.dart
new file mode 100644
index 0000000..074c977
--- /dev/null
+++ b/pkgs/package_config/lib/package_config.dart
@@ -0,0 +1,199 @@
+// 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.
+///
+/// This package provides functionality to find, read and write package
+/// configurations in the [specified format](https://github.com/dart-lang/language/blob/master/accepted/future-releases/language-versioning/package-config-file-v2.md).
+library;
+
+import 'dart:io' show Directory, File;
+import 'dart:typed_data' show Uint8List;
+
+import 'src/discovery.dart' as discover;
+import 'src/errors.dart' show throwError;
+import 'src/package_config.dart';
+import 'src/package_config_io.dart';
+
+export 'package_config_types.dart';
+
+/// 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 (the file name is `.packages`)
+/// and [preferNewest] is true, the default, also checks if there is
+/// a `.dart_tool/package_config.json` file next
+/// to the original file, and if so, loads that instead.
+/// If [preferNewest] is set to false, a directly specified `.packages` file
+/// is loaded even if there is an available `package_config.json` file.
+/// The caller can determine this from the [PackageConfig.version]
+/// being 1 and look for a `package_config.json` file themselves.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+Future<PackageConfig> loadPackageConfig(File file,
+        {bool preferNewest = true, void Function(Object error)? onError}) =>
+    readAnyConfigFile(file, preferNewest, onError ?? throwError);
+
+/// 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 [preferNewest] is true, the default, and the file is a `.packages` file,
+/// as determined by its file name being `.packages`,
+/// 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 [preferNewest] is set to false, a directly specified `.packages` file
+/// is loaded even if there is an available `package_config.json` file.
+/// The caller can determine this from the [PackageConfig.version]
+/// being 1 and look for a `package_config.json` file themselves.
+///
+/// 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 encoded as UTF-8,
+/// 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.
+/// As such, it may throw any error that [loader] throws.
+///
+/// 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.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+Future<PackageConfig> loadPackageConfigUri(Uri file,
+        {Future<Uint8List?> Function(Uri uri)? loader,
+        bool preferNewest = true,
+        void Function(Object error)? onError}) =>
+    readAnyConfigFileUri(file, loader, onError ?? throwError, preferNewest);
+
+/// 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.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+///
+/// If [minVersion] is set to something greater than its default,
+/// any lower-version configuration files are ignored in the search.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig?> findPackageConfig(Directory directory,
+    {bool recurse = true,
+    void Function(Object error)? onError,
+    int minVersion = 1}) {
+  if (minVersion > PackageConfig.maxVersion) {
+    throw ArgumentError.value(minVersion, 'minVersion',
+        'Maximum known version is ${PackageConfig.maxVersion}');
+  }
+  return discover.findPackageConfig(
+      directory, minVersion, recurse, onError ?? throwError);
+}
+
+/// 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.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+///
+/// If [minVersion] is set to something greater than its default,
+/// any lower-version configuration files are ignored in the search.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig?> findPackageConfigUri(Uri location,
+    {bool recurse = true,
+    int minVersion = 1,
+    Future<Uint8List?> Function(Uri uri)? loader,
+    void Function(Object error)? onError}) {
+  if (minVersion > PackageConfig.maxVersion) {
+    throw ArgumentError.value(minVersion, 'minVersion',
+        'Maximum known version is ${PackageConfig.maxVersion}');
+  }
+  return discover.findPackageConfigUri(
+      location, minVersion, loader, onError ?? throwError, 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.
+///
+/// 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) =>
+    writePackageConfigJsonFile(configuration, directory);
diff --git a/pkgs/package_config/lib/package_config_types.dart b/pkgs/package_config/lib/package_config_types.dart
new file mode 100644
index 0000000..825f7ac
--- /dev/null
+++ b/pkgs/package_config/lib/package_config_types.dart
@@ -0,0 +1,17 @@
+// 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.
+///
+/// {@canonicalFor package_config.InvalidLanguageVersion}
+/// {@canonicalFor package_config.LanguageVersion}
+/// {@canonicalFor package_config.Package}
+/// {@canonicalFor package_config.PackageConfig}
+/// {@canonicalFor errors.PackageConfigError}
+library;
+
+export 'src/errors.dart' show PackageConfigError;
+export 'src/package_config.dart'
+    show InvalidLanguageVersion, LanguageVersion, Package, PackageConfig;
diff --git a/pkgs/package_config/lib/src/discovery.dart b/pkgs/package_config/lib/src/discovery.dart
new file mode 100644
index 0000000..b678410
--- /dev/null
+++ b/pkgs/package_config/lib/src/discovery.dart
@@ -0,0 +1,148 @@
+// 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 'errors.dart';
+import 'package_config_impl.dart';
+import 'package_config_io.dart';
+import 'package_config_json.dart';
+import 'packages_file.dart' as packages_file;
+import 'util_io.dart' show defaultLoader, pathJoin;
+
+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
+///   (if `minVersion <= 1`).
+/// * 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].
+///
+/// If [minVersion] is greater than 1, `.packages` files are ignored.
+/// If [minVersion] is greater than the version read from the
+/// `package_config.json` file, it too is ignored.
+Future<PackageConfig?> findPackageConfig(Directory baseDirectory,
+    int minVersion, bool recursive, void Function(Object error) onError) async {
+  var directory = baseDirectory;
+  if (!directory.isAbsolute) directory = directory.absolute;
+  if (!await directory.exists()) {
+    return null;
+  }
+  do {
+    // Check for $cwd/.packages
+    var packageConfig =
+        await findPackageConfigInDirectory(directory, minVersion, onError);
+    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,
+    int minVersion,
+    Future<Uint8List?> Function(Uri uri)? loader,
+    void Function(Object error) onError,
+    bool recursive) async {
+  if (location.isScheme('package')) {
+    onError(PackageConfigArgumentError(
+        location, 'location', 'Must not be a package: URI'));
+    return null;
+  }
+  if (loader == null) {
+    if (location.isScheme('file')) {
+      return findPackageConfig(
+          Directory.fromUri(location.resolveUri(currentPath)),
+          minVersion,
+          recursive,
+          onError);
+    }
+    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) {
+      var config = parsePackageConfigBytes(bytes, file, onError);
+      if (config.version >= minVersion) return config;
+    }
+    if (minVersion <= 1) {
+      file = location.resolveUri(dotPackagesPath);
+      bytes = await loader(file);
+      if (bytes != null) {
+        return packages_file.parse(bytes, file, onError);
+      }
+    }
+    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.
+/// Reports a [FormatException] if a file is there but the content is not valid.
+/// If the file exists, but fails to be read, the file system error is reported.
+///
+/// If [onError] is supplied, parsing errors are reported using that, and
+/// a best-effort attempt is made to return a package configuration.
+/// This may be the empty package configuration.
+///
+/// If [minVersion] is greater than 1, `.packages` files are ignored.
+/// If [minVersion] is greater than the version read from the
+/// `package_config.json` file, it too is ignored.
+Future<PackageConfig?> findPackageConfigInDirectory(Directory directory,
+    int minVersion, void Function(Object error) onError) async {
+  var packageConfigFile = await checkForPackageConfigJsonFile(directory);
+  if (packageConfigFile != null) {
+    var config = await readPackageConfigJsonFile(packageConfigFile, onError);
+    if (config.version < minVersion) return null;
+    return config;
+  }
+  if (minVersion <= 1) {
+    packageConfigFile = await checkForDotPackagesFile(directory);
+    if (packageConfigFile != null) {
+      return await readDotPackagesFile(packageConfigFile, onError);
+    }
+  }
+  return null;
+}
+
+Future<File?> checkForPackageConfigJsonFile(Directory directory) async {
+  assert(directory.isAbsolute);
+  var file =
+      File(pathJoin(directory.path, '.dart_tool', 'package_config.json'));
+  if (await file.exists()) return file;
+  return null;
+}
+
+Future<File?> checkForDotPackagesFile(Directory directory) async {
+  var file = File(pathJoin(directory.path, '.packages'));
+  if (await file.exists()) return file;
+  return null;
+}
diff --git a/pkgs/package_config/lib/src/errors.dart b/pkgs/package_config/lib/src/errors.dart
new file mode 100644
index 0000000..a66fef7
--- /dev/null
+++ b/pkgs/package_config/lib/src/errors.dart
@@ -0,0 +1,34 @@
+// 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? super.value, String super.name, String super.message)
+      : super.value();
+
+  PackageConfigArgumentError.from(ArgumentError error)
+      : super.value(error.invalidValue, error.name, error.message);
+}
+
+class PackageConfigFormatException extends FormatException
+    implements PackageConfigError {
+  PackageConfigFormatException(super.message, Object? super.source,
+      [super.offset]);
+
+  PackageConfigFormatException.from(FormatException exception)
+      : super(exception.message, exception.source, exception.offset);
+}
+
+/// The default `onError` handler.
+// ignore: only_throw_errors
+Never throwError(Object error) => throw error;
diff --git a/pkgs/package_config/lib/src/package_config.dart b/pkgs/package_config/lib/src/package_config.dart
new file mode 100644
index 0000000..155dfc5
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config.dart
@@ -0,0 +1,402 @@
+// 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:typed_data';
+
+import 'errors.dart';
+import 'package_config_impl.dart';
+import 'package_config_json.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 = SimplePackageConfig.empty();
+
+  /// Creates 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.
+  ///
+  /// The package's root ([Package.root]) and package-root
+  /// ([Package.packageUriRoot]) paths must satisfy a number of constraints
+  /// We say that one path (which we know ends with a `/` character)
+  /// is inside another path, if the latter path is a prefix of the former path,
+  /// including the two paths being the same.
+  ///
+  /// * No package's root must be the same as another package's root.
+  /// * The package-root of a package must be inside the package's root.
+  /// * If one package's package-root is inside another package's root,
+  ///   then the latter package's package root must not be inside the former
+  ///   package's root. (No getting between a package and its package root!)
+  ///   This also disallows a package's root being the same as another
+  ///   package's package root.
+  ///
+  /// 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, {Object? extraData}) =>
+      SimplePackageConfig(maxVersion, packages, extraData);
+
+  /// Parses a package configuration file.
+  ///
+  /// The [bytes] must be an UTF-8 encoded JSON object
+  /// containing a valid package configuration.
+  ///
+  /// The [baseUri] is used as the base for resolving relative
+  /// URI references in the configuration file. If the configuration
+  /// has been read from a file, the [baseUri] can be the URI of that
+  /// file, or of the directory it occurs in.
+  ///
+  /// If [onError] is provided, errors found during parsing or building
+  /// the configuration are reported by calling [onError] instead of
+  /// throwing, and parser makes a *best effort* attempt to continue
+  /// despite the error. The input must still be valid JSON.
+  /// The result may be [PackageConfig.empty] if there is no way to
+  /// extract useful information from the bytes.
+  static PackageConfig parseBytes(Uint8List bytes, Uri baseUri,
+          {void Function(Object error)? onError}) =>
+      parsePackageConfigBytes(bytes, baseUri, onError ?? throwError);
+
+  /// Parses a package configuration file.
+  ///
+  /// The [configuration] must be a JSON object
+  /// containing a valid package configuration.
+  ///
+  /// The [baseUri] is used as the base for resolving relative
+  /// URI references in the configuration file. If the configuration
+  /// has been read from a file, the [baseUri] can be the URI of that
+  /// file, or of the directory it occurs in.
+  ///
+  /// If [onError] is provided, errors found during parsing or building
+  /// the configuration are reported by calling [onError] instead of
+  /// throwing, and parser makes a *best effort* attempt to continue
+  /// despite the error. The input must still be valid JSON.
+  /// The result may be [PackageConfig.empty] if there is no way to
+  /// extract useful information from the bytes.
+  static PackageConfig parseString(String configuration, Uri baseUri,
+          {void Function(Object error)? onError}) =>
+      parsePackageConfigString(configuration, baseUri, onError ?? throwError);
+
+  /// Parses the JSON data of a package configuration file.
+  ///
+  /// The [jsonData] must be a JSON-like Dart data structure,
+  /// like the one provided by parsing JSON text using `dart:convert`,
+  /// containing a valid package configuration.
+  ///
+  /// The [baseUri] is used as the base for resolving relative
+  /// URI references in the configuration file. If the configuration
+  /// has been read from a file, the [baseUri] can be the URI of that
+  /// file, or of the directory it occurs in.
+  ///
+  /// If [onError] is provided, errors found during parsing or building
+  /// the configuration are reported by calling [onError] instead of
+  /// throwing, and parser makes a *best effort* attempt to continue
+  /// despite the error. The input must still be valid JSON.
+  /// The result may be [PackageConfig.empty] if there is no way to
+  /// extract useful information from the bytes.
+  static PackageConfig parseJson(Object? jsonData, Uri baseUri,
+          {void Function(Object error)? onError}) =>
+      parsePackageConfigJson(jsonData, baseUri, onError ?? throwError);
+
+  /// Writes a configuration file for this configuration on [output].
+  ///
+  /// If [baseUri] is provided, URI references in the generated file
+  /// will be made relative to [baseUri] where possible.
+  static void writeBytes(PackageConfig configuration, Sink<Uint8List> output,
+      [Uri? baseUri]) {
+    writePackageConfigJsonUtf8(configuration, baseUri, output);
+  }
+
+  /// Writes a configuration JSON text for this configuration on [output].
+  ///
+  /// If [baseUri] is provided, URI references in the generated file
+  /// will be made relative to [baseUri] where possible.
+  static void writeString(PackageConfig configuration, StringSink output,
+      [Uri? baseUri]) {
+    writePackageConfigJsonString(configuration, baseUri, output);
+  }
+
+  /// Converts a configuration to a JSON-like data structure.
+  ///
+  /// If [baseUri] is provided, URI references in the generated data
+  /// will be made relative to [baseUri] where possible.
+  static Map<String, Object?> toJson(PackageConfig configuration,
+          [Uri? baseUri]) =>
+      packageConfigToJson(configuration, baseUri);
+
+  /// 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] from [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.root] 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 `package_config.json` file storage will only store
+  /// JSON-like list/map data structures.
+  Object? 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, and must end with `/`.
+  /// 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.
+  ///
+  /// The [relativeRoot] controls whether the [root] is written as
+  /// relative to the `package_config.json` file when the package
+  /// configuration is written to a file. It defaults to being relative.
+  ///
+  /// If [extraData] is supplied, it will be available as the
+  /// [Package.extraData] of the created package.
+  factory Package(String name, Uri root,
+          {Uri? packageUriRoot,
+          LanguageVersion? languageVersion,
+          Object? extraData,
+          bool relativeRoot = true}) =>
+      SimplePackage.validate(name, root, packageUriRoot, languageVersion,
+          extraData, relativeRoot, throwError)!;
+
+  /// 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 [root] 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 defined by two non-negative numbers,
+  /// the *major* and *minor* version numbers.
+  ///
+  /// A package may have no language version associated with it
+  /// in the package configuration, in which case tools should
+  /// use a default behavior for the package.
+  LanguageVersion? get languageVersion;
+
+  /// Extra data associated with the specific package.
+  ///
+  /// The data may be in any format, depending on who introduced it.
+  /// The standard `package_config.json` file storage will only store
+  /// JSON-like list/map data structures.
+  Object? get extraData;
+
+  /// Whether the [root] URI should be written as relative.
+  ///
+  /// When the configuration is written to a `package_config.json`
+  /// file, the [root] URI can be either relative to the file
+  /// location or absolute, controller by this value.
+  bool get relativeRoot;
+}
+
+/// A language version.
+///
+/// A language version is represented by two non-negative integers,
+/// the [major] and [minor] version numbers.
+///
+/// If errors during parsing are handled using an `onError` handler,
+/// then an *invalid* language version may be represented by an
+/// [InvalidLanguageVersion] object.
+abstract class LanguageVersion implements Comparable<LanguageVersion> {
+  /// The maximal value allowed by [major] and [minor] values;
+  static const int maxValue = 0x7FFFFFFF;
+  factory LanguageVersion(int major, int minor) {
+    RangeError.checkValueInInterval(major, 0, maxValue, 'major');
+    RangeError.checkValueInInterval(minor, 0, maxValue, 'major');
+    return SimpleLanguageVersion(major, minor, null);
+  }
+
+  /// Parses a language version string.
+  ///
+  /// A valid language version string has the form
+  ///
+  /// > *decimalNumber* `.` *decimalNumber*
+  ///
+  /// where a *decimalNumber* is a non-empty sequence of decimal digits
+  /// with no unnecessary leading zeros (the decimal number only starts
+  /// with a zero digit if that digit is the entire number).
+  /// No spaces are allowed in the string.
+  ///
+  /// If the [source] is valid then it is parsed into a valid
+  /// [LanguageVersion] object.
+  /// If not, then the [onError] is called with a [FormatException].
+  /// If [onError] is not supplied, it defaults to throwing the exception.
+  /// If the call does not throw, then an [InvalidLanguageVersion] is returned
+  /// containing the original [source].
+  static LanguageVersion parse(String source,
+          {void Function(Object error)? onError}) =>
+      parseLanguageVersion(source, onError ?? throwError);
+
+  /// The major language version.
+  ///
+  /// A non-negative integer less than 2<sup>31</sup>.
+  ///
+  /// The value is negative for objects representing *invalid* language
+  /// versions ([InvalidLanguageVersion]).
+  int get major;
+
+  /// The minor language version.
+  ///
+  /// A non-negative integer less than 2<sup>31</sup>.
+  ///
+  /// The value is negative for objects representing *invalid* language
+  /// versions ([InvalidLanguageVersion]).
+  int get minor;
+
+  /// Compares language versions.
+  ///
+  /// Two language versions are considered equal if they have the
+  /// same major and minor version numbers.
+  ///
+  /// A language version is greater then another if the former's major version
+  /// is greater than the latter's major version, or if they have
+  /// the same major version and the former's minor version is greater than
+  /// the latter's.
+  @override
+  int compareTo(LanguageVersion other);
+
+  /// Valid language versions with the same [major] and [minor] values are
+  /// equal.
+  ///
+  /// Invalid language versions ([InvalidLanguageVersion]) are not equal to
+  /// any other object.
+  @override
+  bool operator ==(Object other);
+
+  @override
+  int get hashCode;
+
+  /// A string representation of the language version.
+  ///
+  /// A valid language version is represented as
+  /// `"${version.major}.${version.minor}"`.
+  @override
+  String toString();
+}
+
+/// An *invalid* language version.
+///
+/// Stored in a [Package] when the original language version string
+/// was invalid and a `onError` handler was passed to the parser
+/// which did not throw on an error.
+abstract class InvalidLanguageVersion implements LanguageVersion {
+  /// The value -1 for an invalid language version.
+  @override
+  int get major;
+
+  /// The value -1 for an invalid language version.
+  @override
+  int get minor;
+
+  /// An invalid language version is only equal to itself.
+  @override
+  bool operator ==(Object other);
+
+  @override
+  int get hashCode;
+
+  /// The original invalid version string.
+  @override
+  String toString();
+}
diff --git a/pkgs/package_config/lib/src/package_config_impl.dart b/pkgs/package_config/lib/src/package_config_impl.dart
new file mode 100644
index 0000000..865e99a
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config_impl.dart
@@ -0,0 +1,568 @@
+// 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';
+import 'util.dart';
+
+export 'package_config.dart';
+
+const bool _disallowPackagesInsidePackageUriRoot = false;
+
+// Implementations of the main data types exposed by the API of this package.
+
+class SimplePackageConfig implements PackageConfig {
+  @override
+  final int version;
+  final Map<String, Package> _packages;
+  final PackageTree _packageTree;
+  @override
+  final Object? extraData;
+
+  factory SimplePackageConfig(int version, Iterable<Package> packages,
+      [Object? extraData, void Function(Object error)? onError]) {
+    onError ??= throwError;
+    var validVersion = _validateVersion(version, onError);
+    var sortedPackages = [...packages]..sort(_compareRoot);
+    var packageTree = _validatePackages(packages, sortedPackages, onError);
+    return SimplePackageConfig._(validVersion, packageTree,
+        {for (var p in packageTree.allPackages) p.name: p}, extraData);
+  }
+
+  SimplePackageConfig._(
+      this.version, this._packageTree, this._packages, this.extraData);
+
+  /// Creates empty configuration.
+  ///
+  /// The empty configuration can be used in cases where no configuration is
+  /// found, but code expects a non-null configuration.
+  ///
+  /// The version number is [PackageConfig.maxVersion] to avoid
+  /// minimum-version filters discarding the configuration.
+  const SimplePackageConfig.empty()
+      : version = PackageConfig.maxVersion,
+        _packageTree = const EmptyPackageTree(),
+        _packages = const <String, Package>{},
+        extraData = null;
+
+  static int _validateVersion(
+      int version, void Function(Object error) onError) {
+    if (version < 0 || version > PackageConfig.maxVersion) {
+      onError(PackageConfigArgumentError(version, 'version',
+          'Must be in the range 1 to ${PackageConfig.maxVersion}'));
+      return 2; // The minimal version supporting a SimplePackageConfig.
+    }
+    return version;
+  }
+
+  static PackageTree _validatePackages(Iterable<Package> originalPackages,
+      List<Package> packages, void Function(Object error) onError) {
+    var packageNames = <String>{};
+    var tree = TriePackageTree();
+    for (var originalPackage in packages) {
+      SimplePackage? newPackage;
+      if (originalPackage is! SimplePackage) {
+        // SimplePackage validates these properties.
+        newPackage = SimplePackage.validate(
+            originalPackage.name,
+            originalPackage.root,
+            originalPackage.packageUriRoot,
+            originalPackage.languageVersion,
+            originalPackage.extraData,
+            originalPackage.relativeRoot, (error) {
+          if (error is PackageConfigArgumentError) {
+            onError(PackageConfigArgumentError(packages, 'packages',
+                'Package ${newPackage!.name}: ${error.message}'));
+          } else {
+            onError(error);
+          }
+        });
+        if (newPackage == null) continue;
+      } else {
+        newPackage = originalPackage;
+      }
+      var name = newPackage.name;
+      if (packageNames.contains(name)) {
+        onError(PackageConfigArgumentError(
+            name, 'packages', "Duplicate package name '$name'"));
+        continue;
+      }
+      packageNames.add(name);
+      tree.add(newPackage, (error) {
+        if (error is ConflictException) {
+          // There is a conflict with an existing package.
+          var existingPackage = error.existingPackage;
+          switch (error.conflictType) {
+            case ConflictType.sameRoots:
+              onError(PackageConfigArgumentError(
+                  originalPackages,
+                  'packages',
+                  'Packages ${newPackage!.name} and ${existingPackage.name} '
+                      'have the same root directory: ${newPackage.root}.\n'));
+              break;
+            case ConflictType.interleaving:
+              // The new package is inside the package URI root of the existing
+              // package.
+              onError(PackageConfigArgumentError(
+                  originalPackages,
+                  'packages',
+                  'Package ${newPackage!.name} is inside the root of '
+                      'package ${existingPackage.name}, and the package root '
+                      'of ${existingPackage.name} is inside the root of '
+                      '${newPackage.name}.\n'
+                      '${existingPackage.name} package root: '
+                      '${existingPackage.packageUriRoot}\n'
+                      '${newPackage.name} root: ${newPackage.root}\n'));
+              break;
+            case ConflictType.insidePackageRoot:
+              onError(PackageConfigArgumentError(
+                  originalPackages,
+                  'packages',
+                  'Package ${newPackage!.name} is inside the package root of '
+                      'package ${existingPackage.name}.\n'
+                      '${existingPackage.name} package root: '
+                      '${existingPackage.packageUriRoot}\n'
+                      '${newPackage.name} root: ${newPackage.root}\n'));
+              break;
+          }
+        } else {
+          // Any other error.
+          onError(error);
+        }
+      });
+    }
+    return tree;
+  }
+
+  @override
+  Iterable<Package> get packages => _packages.values;
+
+  @override
+  Package? operator [](String packageName) => _packages[packageName];
+
+  @override
+  Package? packageOf(Uri file) => _packageTree.packageOf(file);
+
+  @override
+  Uri? resolve(Uri packageUri) {
+    var packageName = checkValidPackageUri(packageUri, 'packageUri');
+    return _packages[packageName]?.packageUriRoot.resolveUri(
+        Uri(path: packageUri.path.substring(packageName.length + 1)));
+  }
+
+  @override
+  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');
+    }
+    // Find package that file belongs to.
+    var package = _packageTree.packageOf(nonPackageUri);
+    if (package == null) return null;
+    // Check if it is inside the package URI root.
+    var path = nonPackageUri.toString();
+    var root = package.packageUriRoot.toString();
+    if (_beginsWith(package.root.toString().length, root, path)) {
+      var rest = path.substring(root.length);
+      return Uri(scheme: 'package', path: '${package.name}/$rest');
+    }
+    return null;
+  }
+}
+
+/// Configuration data for a single package.
+class SimplePackage implements Package {
+  @override
+  final String name;
+  @override
+  final Uri root;
+  @override
+  final Uri packageUriRoot;
+  @override
+  final LanguageVersion? languageVersion;
+  @override
+  final Object? extraData;
+  @override
+  final bool relativeRoot;
+
+  SimplePackage._(this.name, this.root, this.packageUriRoot,
+      this.languageVersion, this.extraData, this.relativeRoot);
+
+  /// Creates a [SimplePackage] with the provided content.
+  ///
+  /// The provided arguments must be valid.
+  ///
+  /// If the arguments are invalid then the error is reported by
+  /// calling [onError], then the erroneous entry is ignored.
+  ///
+  /// If [onError] is provided, the user is expected to be able to handle
+  /// errors themselves. An invalid [languageVersion] string
+  /// will be replaced with the string `"invalid"`. This allows
+  /// users to detect the difference between an absent version and
+  /// an invalid one.
+  ///
+  /// Returns `null` if the input is invalid and an approximately valid package
+  /// cannot be salvaged from the input.
+  static SimplePackage? validate(
+      String name,
+      Uri root,
+      Uri? packageUriRoot,
+      LanguageVersion? languageVersion,
+      Object? extraData,
+      bool relativeRoot,
+      void Function(Object error) onError) {
+    var fatalError = false;
+    var invalidIndex = checkPackageName(name);
+    if (invalidIndex >= 0) {
+      onError(PackageConfigFormatException(
+          'Not a valid package name', name, invalidIndex));
+      fatalError = true;
+    }
+    if (root.isScheme('package')) {
+      onError(PackageConfigArgumentError(
+          '$root', 'root', 'Must not be a package URI'));
+      fatalError = true;
+    } else if (!isAbsoluteDirectoryUri(root)) {
+      onError(PackageConfigArgumentError(
+          '$root',
+          'root',
+          'In package $name: Not an absolute URI with no query or fragment '
+              'with a path ending in /'));
+      // Try to recover. If the URI has a scheme,
+      // then ensure that the path ends with `/`.
+      if (!root.hasScheme) {
+        fatalError = true;
+      } else if (!root.path.endsWith('/')) {
+        root = root.replace(path: '${root.path}/');
+      }
+    }
+    if (packageUriRoot == null) {
+      packageUriRoot = root;
+    } else if (!fatalError) {
+      packageUriRoot = root.resolveUri(packageUriRoot);
+      if (!isAbsoluteDirectoryUri(packageUriRoot)) {
+        onError(PackageConfigArgumentError(
+            packageUriRoot,
+            'packageUriRoot',
+            'In package $name: Not an absolute URI with no query or fragment '
+                'with a path ending in /'));
+        packageUriRoot = root;
+      } else if (!isUriPrefix(root, packageUriRoot)) {
+        onError(PackageConfigArgumentError(packageUriRoot, 'packageUriRoot',
+            'The package URI root is not below the package root'));
+        packageUriRoot = root;
+      }
+    }
+    if (fatalError) return null;
+    return SimplePackage._(
+        name, root, packageUriRoot, languageVersion, extraData, relativeRoot);
+  }
+}
+
+/// Checks whether [source] is a valid Dart language version string.
+///
+/// The format is (as RegExp) `^(0|[1-9]\d+)\.(0|[1-9]\d+)$`.
+///
+/// Reports a format exception on [onError] if not, or if the numbers
+/// are too large (at most 32-bit signed integers).
+LanguageVersion parseLanguageVersion(
+    String? source, void Function(Object error) onError) {
+  var index = 0;
+  // Reads a positive decimal numeral. Returns the value of the numeral,
+  // or a negative number in case of an error.
+  // Starts at [index] and increments the index to the position after
+  // the numeral.
+  // It is an error if the numeral value is greater than 0x7FFFFFFFF.
+  // It is a recoverable error if the numeral starts with leading zeros.
+  int readNumeral() {
+    const maxValue = 0x7FFFFFFF;
+    if (index == source!.length) {
+      onError(PackageConfigFormatException('Missing number', source, index));
+      return -1;
+    }
+    var start = index;
+
+    var char = source.codeUnitAt(index);
+    var digit = char ^ 0x30;
+    if (digit > 9) {
+      onError(PackageConfigFormatException('Missing number', source, index));
+      return -1;
+    }
+    var firstDigit = digit;
+    var value = 0;
+    do {
+      value = value * 10 + digit;
+      if (value > maxValue) {
+        onError(
+            PackageConfigFormatException('Number too large', source, start));
+        return -1;
+      }
+      index++;
+      if (index == source.length) break;
+      char = source.codeUnitAt(index);
+      digit = char ^ 0x30;
+    } while (digit <= 9);
+    if (firstDigit == 0 && index > start + 1) {
+      onError(PackageConfigFormatException(
+          'Leading zero not allowed', source, start));
+    }
+    return value;
+  }
+
+  var major = readNumeral();
+  if (major < 0) {
+    return SimpleInvalidLanguageVersion(source);
+  }
+  if (index == source!.length || source.codeUnitAt(index) != $dot) {
+    onError(PackageConfigFormatException("Missing '.'", source, index));
+    return SimpleInvalidLanguageVersion(source);
+  }
+  index++;
+  var minor = readNumeral();
+  if (minor < 0) {
+    return SimpleInvalidLanguageVersion(source);
+  }
+  if (index != source.length) {
+    onError(PackageConfigFormatException(
+        'Unexpected trailing character', source, index));
+    return SimpleInvalidLanguageVersion(source);
+  }
+  return SimpleLanguageVersion(major, minor, source);
+}
+
+abstract class _SimpleLanguageVersionBase implements LanguageVersion {
+  @override
+  int compareTo(LanguageVersion other) {
+    var result = major.compareTo(other.major);
+    if (result != 0) return result;
+    return minor.compareTo(other.minor);
+  }
+}
+
+class SimpleLanguageVersion extends _SimpleLanguageVersionBase {
+  @override
+  final int major;
+  @override
+  final int minor;
+  String? _source;
+  SimpleLanguageVersion(this.major, this.minor, this._source);
+
+  @override
+  bool operator ==(Object other) =>
+      other is LanguageVersion && major == other.major && minor == other.minor;
+
+  @override
+  int get hashCode => (major * 17 ^ minor * 37) & 0x3FFFFFFF;
+
+  @override
+  String toString() => _source ??= '$major.$minor';
+}
+
+class SimpleInvalidLanguageVersion extends _SimpleLanguageVersionBase
+    implements InvalidLanguageVersion {
+  final String? _source;
+  SimpleInvalidLanguageVersion(this._source);
+  @override
+  int get major => -1;
+  @override
+  int get minor => -1;
+
+  @override
+  String toString() => _source!;
+}
+
+abstract class PackageTree {
+  Iterable<Package> get allPackages;
+  SimplePackage? packageOf(Uri file);
+}
+
+class _PackageTrieNode {
+  SimplePackage? package;
+
+  /// Indexed by path segment.
+  Map<String, _PackageTrieNode> map = {};
+}
+
+/// Packages of a package configuration ordered by root path.
+///
+/// A package has a root path and a package root path, where the latter
+/// contains the files exposed by `package:` URIs.
+///
+/// A package is said to be inside another package if the root path URI of
+/// the latter is a prefix of the root path URI of the former.
+///
+/// No two packages of a package may have the same root path.
+/// The package root path of a package must not be inside another package's
+/// root path.
+/// Entire other packages are allowed inside a package's root.
+class TriePackageTree implements PackageTree {
+  /// Indexed by URI scheme.
+  final Map<String, _PackageTrieNode> _map = {};
+
+  /// A list of all packages.
+  final List<SimplePackage> _packages = [];
+
+  @override
+  Iterable<Package> get allPackages sync* {
+    for (var package in _packages) {
+      yield package;
+    }
+  }
+
+  bool _checkConflict(_PackageTrieNode node, SimplePackage newPackage,
+      void Function(Object error) onError) {
+    var existingPackage = node.package;
+    if (existingPackage != null) {
+      // Trying to add package that is inside the existing package.
+      // 1) If it's an exact match it's not allowed (i.e. the roots can't be
+      //    the same).
+      if (newPackage.root.path.length == existingPackage.root.path.length) {
+        onError(ConflictException(
+            newPackage, existingPackage, ConflictType.sameRoots));
+        return true;
+      }
+      // 2) The existing package has a packageUriRoot thats inside the
+      //    root of the new package.
+      if (_beginsWith(0, newPackage.root.toString(),
+          existingPackage.packageUriRoot.toString())) {
+        onError(ConflictException(
+            newPackage, existingPackage, ConflictType.interleaving));
+        return true;
+      }
+
+      // For internal reasons we allow this (for now). One should still never do
+      // it though.
+      // 3) The new package is inside the packageUriRoot of existing package.
+      if (_disallowPackagesInsidePackageUriRoot) {
+        if (_beginsWith(0, existingPackage.packageUriRoot.toString(),
+            newPackage.root.toString())) {
+          onError(ConflictException(
+              newPackage, existingPackage, ConflictType.insidePackageRoot));
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  /// Tries to add `newPackage` to the tree.
+  ///
+  /// Reports a [ConflictException] if the added package conflicts with an
+  /// existing package.
+  /// It conflicts if its root or package root is the same as an existing
+  /// package's root or package root, is between the two, or if it's inside the
+  /// package root of an existing package.
+  ///
+  /// If a conflict is detected between [newPackage] and a previous package,
+  /// then [onError] is called with a [ConflictException] object
+  /// and the [newPackage] is not added to the tree.
+  ///
+  /// The packages are added in order of their root path.
+  void add(SimplePackage newPackage, void Function(Object error) onError) {
+    var root = newPackage.root;
+    var node = _map[root.scheme] ??= _PackageTrieNode();
+    if (_checkConflict(node, newPackage, onError)) return;
+    var segments = root.pathSegments;
+    // Notice that we're skipping the last segment as it's always the empty
+    // string because roots are directories.
+    for (var i = 0; i < segments.length - 1; i++) {
+      var path = segments[i];
+      node = node.map[path] ??= _PackageTrieNode();
+      if (_checkConflict(node, newPackage, onError)) return;
+    }
+    node.package = newPackage;
+    _packages.add(newPackage);
+  }
+
+  bool _isMatch(
+      String path, _PackageTrieNode node, List<SimplePackage> potential) {
+    var currentPackage = node.package;
+    if (currentPackage != null) {
+      var currentPackageRootLength = currentPackage.root.toString().length;
+      if (path.length == currentPackageRootLength) return true;
+      var currentPackageUriRoot = currentPackage.packageUriRoot.toString();
+      // Is [file] inside the package root of [currentPackage]?
+      if (currentPackageUriRoot.length == currentPackageRootLength ||
+          _beginsWith(currentPackageRootLength, currentPackageUriRoot, path)) {
+        return true;
+      }
+      potential.add(currentPackage);
+    }
+    return false;
+  }
+
+  @override
+  SimplePackage? packageOf(Uri file) {
+    var currentTrieNode = _map[file.scheme];
+    if (currentTrieNode == null) return null;
+    var path = file.toString();
+    var potential = <SimplePackage>[];
+    if (_isMatch(path, currentTrieNode, potential)) {
+      return currentTrieNode.package;
+    }
+    var segments = file.pathSegments;
+
+    for (var i = 0; i < segments.length - 1; i++) {
+      var segment = segments[i];
+      currentTrieNode = currentTrieNode!.map[segment];
+      if (currentTrieNode == null) break;
+      if (_isMatch(path, currentTrieNode, potential)) {
+        return currentTrieNode.package;
+      }
+    }
+    if (potential.isEmpty) return null;
+    return potential.last;
+  }
+}
+
+class EmptyPackageTree implements PackageTree {
+  const EmptyPackageTree();
+
+  @override
+  Iterable<Package> get allPackages => const Iterable<Package>.empty();
+
+  @override
+  SimplePackage? packageOf(Uri file) => null;
+}
+
+/// Checks whether [longerPath] begins with [parentPath].
+///
+/// Skips checking the [start] first characters which are assumed to
+/// already have been matched.
+bool _beginsWith(int start, String parentPath, String longerPath) {
+  if (longerPath.length < parentPath.length) return false;
+  for (var i = start; i < parentPath.length; i++) {
+    if (longerPath.codeUnitAt(i) != parentPath.codeUnitAt(i)) return false;
+  }
+  return true;
+}
+
+enum ConflictType { sameRoots, interleaving, insidePackageRoot }
+
+/// Conflict between packages added to the same configuration.
+///
+/// The [package] conflicts with [existingPackage] if it has
+/// the same root path or the package URI root path
+/// of [existingPackage] is inside the root path of [package].
+class ConflictException {
+  /// The existing package that [package] conflicts with.
+  final SimplePackage existingPackage;
+
+  /// The package that could not be added without a conflict.
+  final SimplePackage package;
+
+  /// Whether the conflict is with the package URI root of [existingPackage].
+  final ConflictType conflictType;
+
+  /// Creates a root conflict between [package] and [existingPackage].
+  ConflictException(this.package, this.existingPackage, this.conflictType);
+}
+
+/// Used for sorting packages by root path.
+int _compareRoot(Package p1, Package p2) =>
+    p1.root.toString().compareTo(p2.root.toString());
diff --git a/pkgs/package_config/lib/src/package_config_io.dart b/pkgs/package_config/lib/src/package_config_io.dart
new file mode 100644
index 0000000..8c5773b
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config_io.dart
@@ -0,0 +1,166 @@
+// 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.
+
+// dart:io dependent functionality for reading and writing configuration files.
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'errors.dart';
+import 'package_config_impl.dart';
+import 'package_config_json.dart';
+import 'packages_file.dart' as packages_file;
+import 'util.dart';
+import 'util_io.dart';
+
+/// Name of directory where Dart tools store their configuration.
+///
+/// Directory is created in the package root directory.
+const dartToolDirName = '.dart_tool';
+
+/// Name of file containing new package configuration data.
+///
+/// File is stored in the dart tool directory.
+const packageConfigFileName = 'package_config.json';
+
+/// Name of file containing legacy package configuration data.
+///
+/// File is stored in the package root directory.
+const packagesFileName = '.packages';
+
+/// 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 and [preferNewest] is true,
+/// first checks whether there is an adjacent `.dart_tool/package_config.json`
+/// file, and if so, reads that instead.
+/// If [preferNewest] is false, the specified file is loaded even if it is
+/// a `.packages` file and there is an available `package_config.json` file.
+///
+/// The file must exist and be a normal file.
+Future<PackageConfig> readAnyConfigFile(
+    File file, bool preferNewest, void Function(Object error) onError) async {
+  if (preferNewest && fileName(file.path) == packagesFileName) {
+    var alternateFile = File(
+        pathJoin(dirName(file.path), dartToolDirName, packageConfigFileName));
+    if (alternateFile.existsSync()) {
+      return await readPackageConfigJsonFile(alternateFile, onError);
+    }
+  }
+  Uint8List bytes;
+  try {
+    bytes = await file.readAsBytes();
+  } catch (e) {
+    onError(e);
+    return const SimplePackageConfig.empty();
+  }
+  return parseAnyConfigFile(bytes, file.uri, onError);
+}
+
+/// Like [readAnyConfigFile] but uses a URI and an optional loader.
+Future<PackageConfig> readAnyConfigFileUri(
+    Uri file,
+    Future<Uint8List?> Function(Uri uri)? loader,
+    void Function(Object error) onError,
+    bool preferNewest) async {
+  if (file.isScheme('package')) {
+    throw PackageConfigArgumentError(
+        file, 'file', 'Must not be a package: URI');
+  }
+  if (loader == null) {
+    if (file.isScheme('file')) {
+      return await readAnyConfigFile(File.fromUri(file), preferNewest, onError);
+    }
+    loader = defaultLoader;
+  }
+  if (preferNewest && file.pathSegments.last == packagesFileName) {
+    var alternateFile = file.resolve('$dartToolDirName/$packageConfigFileName');
+    Uint8List? bytes;
+    try {
+      bytes = await loader(alternateFile);
+    } catch (e) {
+      onError(e);
+      return const SimplePackageConfig.empty();
+    }
+    if (bytes != null) {
+      return parsePackageConfigBytes(bytes, alternateFile, onError);
+    }
+  }
+  Uint8List? bytes;
+  try {
+    bytes = await loader(file);
+  } catch (e) {
+    onError(e);
+    return const SimplePackageConfig.empty();
+  }
+  if (bytes == null) {
+    onError(PackageConfigArgumentError(
+        file.toString(), 'file', 'File cannot be read'));
+    return const SimplePackageConfig.empty();
+  }
+  return parseAnyConfigFile(bytes, file, onError);
+}
+
+/// Parses a `.packages` or `package_config.json` file's contents.
+///
+/// Assumes it's a JSON file if the first non-whitespace character
+/// is `{`, otherwise assumes it's a `.packages` file.
+PackageConfig parseAnyConfigFile(
+    Uint8List bytes, Uri file, void Function(Object error) onError) {
+  var firstChar = firstNonWhitespaceChar(bytes);
+  if (firstChar != $lbrace) {
+    // Definitely not a JSON object, probably a .packages.
+    return packages_file.parse(bytes, file, onError);
+  }
+  return parsePackageConfigBytes(bytes, file, onError);
+}
+
+Future<PackageConfig> readPackageConfigJsonFile(
+    File file, void Function(Object error) onError) async {
+  Uint8List bytes;
+  try {
+    bytes = await file.readAsBytes();
+  } catch (error) {
+    onError(error);
+    return const SimplePackageConfig.empty();
+  }
+  return parsePackageConfigBytes(bytes, file.uri, onError);
+}
+
+Future<PackageConfig> readDotPackagesFile(
+    File file, void Function(Object error) onError) async {
+  Uint8List bytes;
+  try {
+    bytes = await file.readAsBytes();
+  } catch (error) {
+    onError(error);
+    return const SimplePackageConfig.empty();
+  }
+  return packages_file.parse(bytes, file.uri, onError);
+}
+
+Future<void> writePackageConfigJsonFile(
+    PackageConfig config, Directory targetDirectory) async {
+  // Write .dart_tool/package_config.json first.
+  var dartToolDir = Directory(pathJoin(targetDirectory.path, dartToolDirName));
+  await dartToolDir.create(recursive: true);
+  var file = File(pathJoin(dartToolDir.path, packageConfigFileName));
+  var baseUri = file.uri;
+
+  var sink = file.openWrite(encoding: utf8);
+  writePackageConfigJsonUtf8(config, baseUri, sink);
+  var doneJson = sink.close();
+
+  // Write .packages too.
+  file = File(pathJoin(targetDirectory.path, packagesFileName));
+  baseUri = file.uri;
+  sink = file.openWrite(encoding: utf8);
+  writeDotPackages(config, baseUri, sink);
+  var donePackages = sink.close();
+
+  await Future.wait([doneJson, donePackages]);
+}
diff --git a/pkgs/package_config/lib/src/package_config_json.dart b/pkgs/package_config/lib/src/package_config_json.dart
new file mode 100644
index 0000000..65560a0
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config_json.dart
@@ -0,0 +1,321 @@
+// 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.
+
+// Parsing and serialization of package configurations.
+
+import 'dart:convert';
+import 'dart:typed_data';
+
+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';
+
+final _jsonUtf8Decoder = json.fuse(utf8).decoder;
+
+PackageConfig parsePackageConfigBytes(
+    Uint8List bytes, Uri file, void Function(Object error) onError) {
+  // TODO(lrn): Make this simpler. Maybe parse directly from bytes.
+  Object? jsonObject;
+  try {
+    jsonObject = _jsonUtf8Decoder.convert(bytes);
+  } on FormatException catch (e) {
+    onError(PackageConfigFormatException.from(e));
+    return const SimplePackageConfig.empty();
+  }
+  return parsePackageConfigJson(jsonObject, file, onError);
+}
+
+PackageConfig parsePackageConfigString(
+    String source, Uri file, void Function(Object error) onError) {
+  Object? jsonObject;
+  try {
+    jsonObject = jsonDecode(source);
+  } on FormatException catch (e) {
+    onError(PackageConfigFormatException.from(e));
+    return const SimplePackageConfig.empty();
+  }
+  return parsePackageConfigJson(jsonObject, file, onError);
+}
+
+/// Creates a [PackageConfig] from a parsed JSON-like object structure.
+///
+/// The [json] argument must be a JSON object (`Map<String, Object?>`)
+/// 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<Object?>`)
+/// 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.
+///
+/// The [baseLocation] is used as base URI to resolve the "rootUri"
+/// URI reference string.
+PackageConfig parsePackageConfigJson(
+    Object? json, Uri baseLocation, void Function(Object error) onError) {
+  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 <Object?>[] is T) return 'array';
+    return 'object';
+  }
+
+  T? checkType<T>(Object? value, String name, [String? packageName]) {
+    if (value is T) return value;
+    // The only types we are called with are [int], [String], [List<Object?>]
+    // and Map<String, Object?>. Recognize which to give a better error message.
+    var message =
+        "$name${packageName != null ? " of package $packageName" : ""}"
+        ' is not a JSON ${typeName<T>()}';
+    onError(PackageConfigFormatException(message, value));
+    return null;
+  }
+
+  Package? parsePackage(Map<String, Object?> entry) {
+    String? name;
+    String? rootUri;
+    String? packageUri;
+    String? languageVersion;
+    Map<String, Object?>? extraData;
+    var hasName = false;
+    var hasRoot = false;
+    var hasVersion = false;
+    entry.forEach((key, value) {
+      switch (key) {
+        case _nameKey:
+          hasName = true;
+          name = checkType<String>(value, _nameKey);
+          break;
+        case _rootUriKey:
+          hasRoot = true;
+          rootUri = checkType<String>(value, _rootUriKey, name);
+          break;
+        case _packageUriKey:
+          packageUri = checkType<String>(value, _packageUriKey, name);
+          break;
+        case _languageVersionKey:
+          hasVersion = true;
+          languageVersion = checkType<String>(value, _languageVersionKey, name);
+          break;
+        default:
+          (extraData ??= {})[key] = value;
+          break;
+      }
+    });
+    if (!hasName) {
+      onError(PackageConfigFormatException('Missing name entry', entry));
+    }
+    if (!hasRoot) {
+      onError(PackageConfigFormatException('Missing rootUri entry', entry));
+    }
+    if (name == null || rootUri == null) return null;
+    var parsedRootUri = Uri.parse(rootUri!);
+    var relativeRoot = !hasAbsolutePath(parsedRootUri);
+    var root = baseLocation.resolveUri(parsedRootUri);
+    if (!root.path.endsWith('/')) root = root.replace(path: '${root.path}/');
+    var packageRoot = root;
+    if (packageUri != null) packageRoot = root.resolve(packageUri!);
+    if (!packageRoot.path.endsWith('/')) {
+      packageRoot = packageRoot.replace(path: '${packageRoot.path}/');
+    }
+
+    LanguageVersion? version;
+    if (languageVersion != null) {
+      version = parseLanguageVersion(languageVersion, onError);
+    } else if (hasVersion) {
+      version = SimpleInvalidLanguageVersion('invalid');
+    }
+
+    return SimplePackage.validate(
+        name!, root, packageRoot, version, extraData, relativeRoot, (error) {
+      if (error is ArgumentError) {
+        onError(
+          PackageConfigFormatException(
+              error.message.toString(), error.invalidValue),
+        );
+      } else {
+        onError(error);
+      }
+    });
+  }
+
+  var map = checkType<Map<String, Object?>>(json, 'value');
+  if (map == null) return const SimplePackageConfig.empty();
+  Map<String, Object?>? extraData;
+  List<Package>? packageList;
+  int? configVersion;
+  map.forEach((key, value) {
+    switch (key) {
+      case _configVersionKey:
+        configVersion = checkType<int>(value, _configVersionKey) ?? 2;
+        break;
+      case _packagesKey:
+        var packageArray = checkType<List<Object?>>(value, _packagesKey) ?? [];
+        var packages = <Package>[];
+        for (var package in packageArray) {
+          var packageMap =
+              checkType<Map<String, Object?>>(package, 'package entry');
+          if (packageMap != null) {
+            var entry = parsePackage(packageMap);
+            if (entry != null) {
+              packages.add(entry);
+            }
+          }
+        }
+        packageList = packages;
+        break;
+      default:
+        (extraData ??= {})[key] = value;
+        break;
+    }
+  });
+  if (configVersion == null) {
+    onError(PackageConfigFormatException('Missing configVersion entry', json));
+    configVersion = 2;
+  }
+  if (packageList == null) {
+    onError(PackageConfigFormatException('Missing packages list', json));
+    packageList = [];
+  }
+  return SimplePackageConfig(configVersion!, packageList!, extraData, (error) {
+    if (error is ArgumentError) {
+      onError(
+        PackageConfigFormatException(
+            error.message.toString(), error.invalidValue),
+      );
+    } else {
+      onError(error);
+    }
+  });
+}
+
+final _jsonUtf8Encoder = JsonUtf8Encoder('  ');
+
+void writePackageConfigJsonUtf8(
+    PackageConfig config, Uri? baseUri, Sink<List<int>> output) {
+  // Can be optimized.
+  var data = packageConfigToJson(config, baseUri);
+  output.add(_jsonUtf8Encoder.convert(data) as Uint8List);
+}
+
+void writePackageConfigJsonString(
+    PackageConfig config, Uri? baseUri, StringSink output) {
+  // Can be optimized.
+  var data = packageConfigToJson(config, baseUri);
+  output.write(const JsonEncoder.withIndent('  ').convert(data));
+}
+
+Map<String, Object?> packageConfigToJson(PackageConfig config, Uri? baseUri) =>
+    <String, Object?>{
+      ...?_extractExtraData(config.extraData, _topNames),
+      _configVersionKey: PackageConfig.maxVersion,
+      _packagesKey: [
+        for (var package in config.packages)
+          <String, Object?>{
+            _nameKey: package.name,
+            _rootUriKey: trailingSlash((package.relativeRoot
+                    ? relativizeUri(package.root, baseUri)
+                    : package.root)
+                .toString()),
+            if (package.root != package.packageUriRoot)
+              _packageUriKey: trailingSlash(
+                  relativizeUri(package.packageUriRoot, package.root)
+                      .toString()),
+            if (package.languageVersion != null &&
+                package.languageVersion is! InvalidLanguageVersion)
+              _languageVersionKey: package.languageVersion.toString(),
+            ...?_extractExtraData(package.extraData, _packageNames),
+          }
+      ],
+    };
+
+void writeDotPackages(PackageConfig config, Uri baseUri, StringSink output) {
+  var extraData = config.extraData;
+  // Write .packages too.
+  String? comment;
+  if (extraData is Map<String, Object?>) {
+    var generator = extraData[_generatorKey];
+    if (generator is String) {
+      var generated = extraData[_generatedKey];
+      var generatorVersion = extraData[_generatorVersionKey];
+      comment = 'Generated by $generator'
+          "${generatorVersion is String ? " $generatorVersion" : ""}"
+          "${generated is String ? " on $generated" : ""}.";
+    }
+  }
+  packages_file.write(output, config, baseUri: baseUri, comment: comment);
+}
+
+/// 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, Object?>? _extractExtraData(
+    Object? data, Iterable<String> reservedNames) {
+  if (data is Map<String, Object?>) {
+    if (data.isEmpty) return null;
+    for (var name in reservedNames) {
+      if (data.containsKey(name)) {
+        var filteredData = {
+          for (var key in data.keys)
+            if (!reservedNames.contains(key)) key: data[key]
+        };
+        if (filteredData.isEmpty) return null;
+        for (var value in filteredData.values) {
+          if (!_validateJson(value)) return null;
+        }
+        return filteredData;
+      }
+    }
+    return data;
+  }
+  return null;
+}
+
+/// Checks that the object is a valid JSON-like data structure.
+bool _validateJson(Object? object) {
+  if (object == null || true == object || false == object) return true;
+  if (object is num || object is String) return true;
+  if (object is List<Object?>) {
+    return object.every(_validateJson);
+  }
+  if (object is Map<String, Object?>) {
+    return object.values.every(_validateJson);
+  }
+  return false;
+}
diff --git a/pkgs/package_config/lib/src/packages_file.dart b/pkgs/package_config/lib/src/packages_file.dart
new file mode 100644
index 0000000..bf68f2c
--- /dev/null
+++ b/pkgs/package_config/lib/src/packages_file.dart
@@ -0,0 +1,193 @@
+// 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_impl.dart';
+import 'util.dart';
+
+/// The language version prior to the release of language versioning.
+///
+/// This is the default language version used by all packages from a
+/// `.packages` file.
+final LanguageVersion _languageVersion = LanguageVersion(2, 7);
+
+/// 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, void Function(Object error) onError) {
+  if (baseLocation.isScheme('package')) {
+    onError(PackageConfigArgumentError(
+        baseLocation, 'baseLocation', 'Must not be a package: URI'));
+    return PackageConfig.empty;
+  }
+  var index = 0;
+  var packages = <Package>[];
+  var packageNames = <String>{};
+  while (index < source.length) {
+    var ignoreLine = false;
+    var start = index;
+    var separatorIndex = -1;
+    var end = source.length;
+    var char = source[index++];
+    if (char == $cr || char == $lf) {
+      continue;
+    }
+    if (char == $colon) {
+      onError(PackageConfigFormatException(
+          'Missing package name', source, index - 1));
+      ignoreLine = true; // Ignore if package name is invalid.
+    } else {
+      ignoreLine = char == $hash; // Ignore if comment.
+    }
+    var queryStart = -1;
+    var fragmentStart = -1;
+    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;
+      } else if (char == $question && queryStart < 0 && fragmentStart < 0) {
+        queryStart = index - 1;
+      } else if (char == $hash && fragmentStart < 0) {
+        fragmentStart = index - 1;
+      }
+    }
+    if (ignoreLine) continue;
+    if (separatorIndex < 0) {
+      onError(
+          PackageConfigFormatException("No ':' on line", source, index - 1));
+      continue;
+    }
+    var packageName = String.fromCharCodes(source, start, separatorIndex);
+    var invalidIndex = checkPackageName(packageName);
+    if (invalidIndex >= 0) {
+      onError(PackageConfigFormatException(
+          'Not a valid package name', source, start + invalidIndex));
+      continue;
+    }
+    if (queryStart >= 0) {
+      onError(PackageConfigFormatException(
+          'Location URI must not have query', source, queryStart));
+      end = queryStart;
+    } else if (fragmentStart >= 0) {
+      onError(PackageConfigFormatException(
+          'Location URI must not have fragment', source, fragmentStart));
+      end = fragmentStart;
+    }
+    var packageValue = String.fromCharCodes(source, separatorIndex + 1, end);
+    Uri packageLocation;
+    try {
+      packageLocation = Uri.parse(packageValue);
+    } on FormatException catch (e) {
+      onError(PackageConfigFormatException.from(e));
+      continue;
+    }
+    var relativeRoot = !hasAbsolutePath(packageLocation);
+    packageLocation = baseLocation.resolveUri(packageLocation);
+    if (packageLocation.isScheme('package')) {
+      onError(PackageConfigFormatException(
+          'Package URI as location for package', source, separatorIndex + 1));
+      continue;
+    }
+    var path = packageLocation.path;
+    if (!path.endsWith('/')) {
+      path += '/';
+      packageLocation = packageLocation.replace(path: path);
+    }
+    if (packageNames.contains(packageName)) {
+      onError(PackageConfigFormatException(
+          'Same package name occurred more than once', source, start));
+      continue;
+    }
+    var rootUri = packageLocation;
+    if (path.endsWith('/lib/')) {
+      // Assume default Pub package layout. Include package itself in root.
+      rootUri =
+          packageLocation.replace(path: path.substring(0, path.length - 4));
+    }
+    var package = SimplePackage.validate(packageName, rootUri, packageLocation,
+        _languageVersion, null, relativeRoot, (error) {
+      if (error is ArgumentError) {
+        onError(PackageConfigFormatException(error.message.toString(), source));
+      } else {
+        onError(error);
+      }
+    });
+    if (package != null) {
+      packages.add(package);
+      packageNames.add(packageName);
+    }
+  }
+  return SimplePackageConfig(1, packages, null, onError);
+}
+
+/// 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.
+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 is provided, make the URI relative to baseUri.
+    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/pkgs/package_config/lib/src/util.dart b/pkgs/package_config/lib/src/util.dart
new file mode 100644
index 0000000..4f0210c
--- /dev/null
+++ b/pkgs/package_config/lib/src/util.dart
@@ -0,0 +1,253 @@
+// 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;
+
+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 =
+    r"                                 !  $ &'()*+,-. 0123456789 ; =  "
+    r'@ABCDEFGHIJKLMNOPQRSTUVWXYZ    _ abcdefghijklmnopqrstuvwxyz   ~ ';
+
+/// Tests whether something is a valid Dart package name.
+bool isValidPackageName(String string) {
+  return checkPackageName(string) < 0;
+}
+
+/// Check if a string is a valid package name.
+///
+/// Valid package names contain only characters in [_validPackageNameCharacters]
+/// and must contain at least one non-'.' character.
+///
+/// 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 checkPackageName(String string) {
+  // Becomes non-zero if any non-'.' character is encountered.
+  var nonDot = 0;
+  for (var i = 0; i < string.length; i++) {
+    var c = string.codeUnitAt(i);
+    if (c > 0x7f || _validPackageNameCharacters.codeUnitAt(c) <= $space) {
+      return i;
+    }
+    nonDot += c ^ $dot;
+  }
+  if (nonDot == 0) return string.length;
+  return -1;
+}
+
+/// Validate that a [Uri] is a valid `package:` URI.
+///
+/// Used to validate user input.
+///
+/// Returns the package name extracted from the package URI,
+/// which is the path segment between `package:` and the first `/`.
+String checkValidPackageUri(Uri packageUri, String name) {
+  if (packageUri.scheme != 'package') {
+    throw PackageConfigArgumentError(packageUri, name, 'Not a package: URI');
+  }
+  if (packageUri.hasAuthority) {
+    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 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 PackageConfigArgumentError(
+        packageUri, name, 'Package URIs must not have a fragment part');
+  }
+  if (packageUri.path.startsWith('/')) {
+    throw PackageConfigArgumentError(
+        packageUri, name, "Package URIs must not start with a '/'");
+  }
+  var firstSlash = packageUri.path.indexOf('/');
+  if (firstSlash == -1) {
+    throw PackageConfigArgumentError(packageUri, name,
+        "Package URIs must start with the package name followed by a '/'");
+  }
+  var packageName = packageUri.path.substring(0, firstSlash);
+  var badIndex = checkPackageName(packageName);
+  if (badIndex >= 0) {
+    if (packageName.isEmpty) {
+      throw PackageConfigArgumentError(
+          packageUri, name, 'Package names mus be non-empty');
+    }
+    if (badIndex == packageName.length) {
+      throw PackageConfigArgumentError(packageUri, name,
+          "Package names must contain at least one non-'.' character");
+    }
+    assert(badIndex < packageName.length);
+    var badCharCode = packageName.codeUnitAt(badIndex);
+    var badChar = 'U+${badCharCode.toRadixString(16).padLeft(4, '0')}';
+    if (badCharCode >= 0x20 && badCharCode <= 0x7e) {
+      // Printable character.
+      badChar = "'${packageName[badIndex]}' ($badChar)";
+    }
+    throw PackageConfigArgumentError(
+        packageUri, name, 'Package names must not contain $badChar');
+  }
+  return packageName;
+}
+
+/// Checks whether URI is just an absolute directory.
+///
+/// * It must have a scheme.
+/// * It must not have a query or fragment.
+/// * The path must 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.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 (var i = 0; i < bytes.length; i++) {
+    var char = bytes[i];
+    if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) {
+      return char;
+    }
+  }
+  return -1;
+}
+
+/// Appends a trailing `/` if the path doesn't end with one.
+String trailingSlash(String path) {
+  if (path.isEmpty || path.endsWith('/')) return path;
+  return '$path/';
+}
+
+/// Whether a URI should not be considered relative to the base URI.
+///
+/// Used to determine whether a parsed root URI is relative
+/// to the configuration file or not.
+/// If it is relative, then it's rewritten as relative when
+/// output again later. If not, it's output as absolute.
+bool hasAbsolutePath(Uri uri) =>
+    uri.hasScheme || uri.hasAuthority || uri.hasAbsolutePath;
+
+/// 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 satisfies
+/// `baseUri.resolveUri(result) == uri`,
+///
+/// The `baseUri` must be absolute.
+Uri? relativizeUri(Uri? uri, Uri? baseUri) {
+  if (baseUri == null) return uri;
+  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();
+  var base = [...baseUri.pathSegments];
+  if (base.isNotEmpty) base.removeLast();
+  uri = uri.normalizePath();
+  var target = [...uri.pathSegments];
+  if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
+  var 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 (var n = base.length - index; n > 0; --n) {
+      buffer.write('../');
+    }
+    buffer.writeAll(target.skip(index), '/');
+    return Uri(path: buffer.toString());
+  } else {
+    return uri;
+  }
+}
+
+// Character constants used by this package.
+/// "Line feed" control character.
+const int $lf = 0x0a;
+
+/// "Carriage return" control character.
+const int $cr = 0x0d;
+
+/// Space character.
+const int $space = 0x20;
+
+/// Character `#`.
+const int $hash = 0x23;
+
+/// Character `.`.
+const int $dot = 0x2e;
+
+/// Character `:`.
+const int $colon = 0x3a;
+
+/// Character `?`.
+const int $question = 0x3f;
+
+/// Character `{`.
+const int $lbrace = 0x7b;
diff --git a/pkgs/package_config/lib/src/util_io.dart b/pkgs/package_config/lib/src/util_io.dart
new file mode 100644
index 0000000..4680eef
--- /dev/null
+++ b/pkgs/package_config/lib/src/util_io.dart
@@ -0,0 +1,108 @@
+// Copyright (c) 2020, 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 requiring dart:io and used by more than one library in the
+/// package.
+library;
+
+import 'dart:io';
+import 'dart:typed_data';
+
+Future<Uint8List?> defaultLoader(Uri uri) async {
+  if (uri.isScheme('file')) {
+    var file = File.fromUri(uri);
+    try {
+      return await 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'));
+  var client = HttpClient();
+  var request = await client.getUrl(uri);
+  var response = await request.close();
+  if (response.statusCode != HttpStatus.ok) {
+    return null;
+  }
+  var splitContent = await response.toList();
+  var totalLength = 0;
+  if (splitContent.length == 1) {
+    var part = splitContent[0];
+    if (part is Uint8List) {
+      return part;
+    }
+  }
+  for (var list in splitContent) {
+    totalLength += list.length;
+  }
+  var result = Uint8List(totalLength);
+  var offset = 0;
+  for (var contentPart in splitContent as Iterable<Uint8List>) {
+    result.setRange(offset, offset + contentPart.length, contentPart);
+    offset += contentPart.length;
+  }
+  return result;
+}
+
+/// The file name of a path.
+///
+/// The file name is everything after the last occurrence of
+/// [Platform.pathSeparator], or the entire string if no
+/// path separator occurs in the string.
+String fileName(String path) {
+  var separator = Platform.pathSeparator;
+  var lastSeparator = path.lastIndexOf(separator);
+  if (lastSeparator < 0) return path;
+  return path.substring(lastSeparator + separator.length);
+}
+
+/// The directory name of a path.
+///
+/// The directory name is everything before the last occurrence of
+/// [Platform.pathSeparator], or the empty string if no
+/// path separator occurs in the string.
+String dirName(String path) {
+  var separator = Platform.pathSeparator;
+  var lastSeparator = path.lastIndexOf(separator);
+  if (lastSeparator < 0) return '';
+  return path.substring(0, lastSeparator);
+}
+
+/// Join path parts with the [Platform.pathSeparator].
+///
+/// If a part ends with a path separator, then no extra separator is
+/// inserted.
+String pathJoin(String part1, String part2, [String? part3]) {
+  var separator = Platform.pathSeparator;
+  var separator1 = part1.endsWith(separator) ? '' : separator;
+  if (part3 == null) {
+    return '$part1$separator1$part2';
+  }
+  var separator2 = part2.endsWith(separator) ? '' : separator;
+  return '$part1$separator1$part2$separator2$part3';
+}
+
+/// Join an unknown number of path parts with [Platform.pathSeparator].
+///
+/// If a part ends with a path separator, then no extra separator is
+/// inserted.
+String pathJoinAll(Iterable<String> parts) {
+  var buffer = StringBuffer();
+  var separator = '';
+  for (var part in parts) {
+    buffer
+      ..write(separator)
+      ..write(part);
+    separator =
+        part.endsWith(Platform.pathSeparator) ? '' : Platform.pathSeparator;
+  }
+  return buffer.toString();
+}
diff --git a/pkgs/package_config/pubspec.yaml b/pkgs/package_config/pubspec.yaml
new file mode 100644
index 0000000..28f3e13
--- /dev/null
+++ b/pkgs/package_config/pubspec.yaml
@@ -0,0 +1,14 @@
+name: package_config
+version: 2.1.1
+description: Support for reading and writing Dart Package Configuration files.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/package_config
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  path: ^1.8.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.0
diff --git a/pkgs/package_config/test/bench.dart b/pkgs/package_config/test/bench.dart
new file mode 100644
index 0000000..8428481
--- /dev/null
+++ b/pkgs/package_config/test/bench.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2021, 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:typed_data';
+
+import 'package:package_config/src/errors.dart';
+import 'package:package_config/src/package_config_json.dart';
+
+void bench(final int size, final bool doPrint) {
+  var sb = StringBuffer();
+  sb.writeln('{');
+  sb.writeln('"configVersion": 2,');
+  sb.writeln('"packages": [');
+  for (var i = 0; i < size; i++) {
+    if (i != 0) {
+      sb.writeln(',');
+    }
+    sb.writeln('{');
+    sb.writeln('  "name": "p_$i",');
+    sb.writeln('  "rootUri": "file:///p_$i/",');
+    sb.writeln('  "packageUri": "lib/",');
+    sb.writeln('  "languageVersion": "2.5",');
+    sb.writeln('  "nonstandard": true');
+    sb.writeln('}');
+  }
+  sb.writeln('],');
+  sb.writeln('"generator": "pub",');
+  sb.writeln('"other": [42]');
+  sb.writeln('}');
+  var stopwatch = Stopwatch()..start();
+  var config = parsePackageConfigBytes(
+    // ignore: unnecessary_cast
+    utf8.encode(sb.toString()) as Uint8List,
+    Uri.parse('file:///tmp/.dart_tool/file.dart'),
+    throwError,
+  );
+  final read = stopwatch.elapsedMilliseconds;
+
+  stopwatch.reset();
+  for (var i = 0; i < size; i++) {
+    if (config.packageOf(Uri.parse('file:///p_$i/lib/src/foo.dart'))!.name !=
+        'p_$i') {
+      throw StateError('Unexpected result!');
+    }
+  }
+  final lookup = stopwatch.elapsedMilliseconds;
+
+  if (doPrint) {
+    print('Read file with $size packages in $read ms, '
+        'looked up all packages in $lookup ms');
+  }
+}
+
+void main(List<String> args) {
+  if (args.length != 1 && args.length != 2) {
+    throw ArgumentError('Expects arguments: <size> <warmup iterations>?');
+  }
+  final size = int.parse(args[0]);
+  if (args.length > 1) {
+    final warmups = int.parse(args[1]);
+    print('Performing $warmups warmup iterations.');
+    for (var i = 0; i < warmups; i++) {
+      bench(10, false);
+    }
+  }
+
+  // Benchmark.
+  bench(size, true);
+}
diff --git a/pkgs/package_config/test/discovery_test.dart b/pkgs/package_config/test/discovery_test.dart
new file mode 100644
index 0000000..6d1b655
--- /dev/null
+++ b/pkgs/package_config/test/discovery_test.dart
@@ -0,0 +1,346 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+
+import 'package:package_config/package_config.dart';
+import 'package:test/test.dart';
+
+import 'src/util.dart';
+import 'src/util_io.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, 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('file:///dart/packages/bar/baz/qux')));
+  expect(resolver.resolve(pkg('baz', 'qux/foo')),
+      equals(Uri.directory(directory.path).resolve('packages/baz/qux/foo')));
+  expect([for (var p in resolver.packages) p.name],
+      unorderedEquals(['foo', 'bar', 'baz']));
+}
+
+void main() {
+  group('findPackages', () {
+    // Finds package_config.json if there.
+    fileTest('package_config.json', {
+      '.packages': 'invalid .packages file',
+      'script.dart': 'main(){}',
+      'packages': {'shouldNotBeFound': <Never, Never>{}},
+      '.dart_tool': {
+        'package_config.json': packageConfigFile,
+      }
+    }, (Directory directory) async {
+      var config = (await findPackageConfig(directory))!;
+      expect(config.version, 2); // Found package_config.json file.
+      validatePackagesFile(config, directory);
+    });
+
+    // Finds .packages if no package_config.json.
+    fileTest('.packages', {
+      '.packages': packagesFile,
+      'script.dart': 'main(){}',
+      'packages': {'shouldNotBeFound': <Object, Object>{}}
+    }, (Directory directory) async {
+      var 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 {
+      var 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 {
+      var 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': <String, dynamic>{},
+      }
+    }, (Directory directory) async {
+      var config = await findPackageConfig(directory);
+      expect(config, null);
+    });
+
+    group('throws', () {
+      fileTest('invalid .packages', {
+        '.packages': 'not a .packages file',
+      }, (Directory directory) {
+        expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+      });
+
+      fileTest('invalid .packages as JSON', {
+        '.packages': packageConfigFile,
+      }, (Directory directory) {
+        expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+      });
+
+      fileTest('invalid .packages', {
+        '.dart_tool': {
+          'package_config.json': 'not a JSON file',
+        }
+      }, (Directory directory) {
+        expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+      });
+
+      fileTest('invalid .packages as INI', {
+        '.dart_tool': {
+          'package_config.json': packagesFile,
+        }
+      }, (Directory directory) {
+        expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+      });
+    });
+
+    group('handles error', () {
+      fileTest('invalid .packages', {
+        '.packages': 'not a .packages file',
+      }, (Directory directory) async {
+        var hadError = false;
+        await findPackageConfig(directory,
+            onError: expectAsync1((error) {
+              hadError = true;
+              expect(error, isA<FormatException>());
+            }, max: -1));
+        expect(hadError, true);
+      });
+
+      fileTest('invalid .packages as JSON', {
+        '.packages': packageConfigFile,
+      }, (Directory directory) async {
+        var hadError = false;
+        await findPackageConfig(directory,
+            onError: expectAsync1((error) {
+              hadError = true;
+              expect(error, isA<FormatException>());
+            }, max: -1));
+        expect(hadError, true);
+      });
+
+      fileTest('invalid package_config not JSON', {
+        '.dart_tool': {
+          'package_config.json': 'not a JSON file',
+        }
+      }, (Directory directory) async {
+        var hadError = false;
+        await findPackageConfig(directory,
+            onError: expectAsync1((error) {
+              hadError = true;
+              expect(error, isA<FormatException>());
+            }, max: -1));
+        expect(hadError, true);
+      });
+
+      fileTest('invalid package config as INI', {
+        '.dart_tool': {
+          'package_config.json': packagesFile,
+        }
+      }, (Directory directory) async {
+        var hadError = false;
+        await findPackageConfig(directory,
+            onError: expectAsync1((error) {
+              hadError = true;
+              expect(error, isA<FormatException>());
+            }, max: -1));
+        expect(hadError, true);
+      });
+    });
+
+    // Does not find .packages if no package_config.json and minVersion > 1.
+    fileTest('.packages ignored', {
+      '.packages': packagesFile,
+      'script.dart': 'main(){}'
+    }, (Directory directory) async {
+      var config = await findPackageConfig(directory, minVersion: 2);
+      expect(config, null);
+    });
+
+    // Finds package_config.json in super-directory, with .packages in
+    // subdir and minVersion > 1.
+    fileTest('package_config.json recursive .packages ignored', {
+      '.dart_tool': {
+        'package_config.json': packageConfigFile,
+      },
+      'subdir': {
+        '.packages': packagesFile,
+        'script.dart': 'main(){}',
+      }
+    }, (Directory directory) async {
+      var config = (await findPackageConfig(subdir(directory, 'subdir/'),
+          minVersion: 2))!;
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+  });
+
+  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 {
+        var file =
+            dirFile(subdir(directory, '.dart_tool'), 'package_config.json');
+        var config = await loadPackageConfig(file);
+        expect(config.version, 2);
+        validatePackagesFile(config, directory);
+      });
+      fileTest('indirectly through .packages', files,
+          (Directory directory) async {
+        var file = dirFile(directory, '.packages');
+        var config = await loadPackageConfig(file);
+        expect(config.version, 2);
+        validatePackagesFile(config, directory);
+      });
+      fileTest('prefer .packages', files, (Directory directory) async {
+        var file = dirFile(directory, '.packages');
+        var config = await loadPackageConfig(file, preferNewest: false);
+        expect(config.version, 1);
+        validatePackagesFile(config, directory);
+      });
+    });
+
+    fileTest('package_config.json non-default name', {
+      '.packages': packagesFile,
+      'subdir': {
+        'pheldagriff': packageConfigFile,
+      },
+    }, (Directory directory) async {
+      var file = dirFile(directory, 'subdir/pheldagriff');
+      var config = await loadPackageConfig(file);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    fileTest('package_config.json named .packages', {
+      'subdir': {
+        '.packages': packageConfigFile,
+      },
+    }, (Directory directory) async {
+      var file = dirFile(directory, 'subdir/.packages');
+      var config = await loadPackageConfig(file);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    fileTest('.packages', {
+      '.packages': packagesFile,
+    }, (Directory directory) async {
+      var file = dirFile(directory, '.packages');
+      var config = await loadPackageConfig(file);
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    fileTest('.packages non-default name', {
+      'pheldagriff': packagesFile,
+    }, (Directory directory) async {
+      var file = dirFile(directory, 'pheldagriff');
+      var config = await loadPackageConfig(file);
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    fileTest('no config found', {}, (Directory directory) {
+      var file = dirFile(directory, 'anyname');
+      expect(
+          () => loadPackageConfig(file), throwsA(isA<FileSystemException>()));
+    });
+
+    fileTest('no config found, handled', {}, (Directory directory) async {
+      var file = dirFile(directory, 'anyname');
+      var hadError = false;
+      await loadPackageConfig(file,
+          onError: expectAsync1((error) {
+            hadError = true;
+            expect(error, isA<FileSystemException>());
+          }, max: -1));
+      expect(hadError, true);
+    });
+
+    fileTest('specified file syntax error', {
+      'anyname': 'syntax error',
+    }, (Directory directory) {
+      var file = dirFile(directory, 'anyname');
+      expect(() => loadPackageConfig(file), throwsFormatException);
+    });
+
+    // Find package_config.json in subdir even if initial file syntax error.
+    fileTest('specified file syntax onError', {
+      '.packages': 'syntax error',
+      '.dart_tool': {
+        'package_config.json': packageConfigFile,
+      },
+    }, (Directory directory) async {
+      var file = dirFile(directory, '.packages');
+      var 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) {
+      var file = dirFile(directory, '.packages');
+      expect(() => loadPackageConfig(file), throwsFormatException);
+    });
+  });
+}
diff --git a/pkgs/package_config/test/discovery_uri_test.dart b/pkgs/package_config/test/discovery_uri_test.dart
new file mode 100644
index 0000000..542bf0a
--- /dev/null
+++ b/pkgs/package_config/test/discovery_uri_test.dart
@@ -0,0 +1,310 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:package_config/package_config.dart';
+import 'package:test/test.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']));
+}
+
+void main() {
+  group('findPackages', () {
+    // Finds package_config.json if there.
+    loaderTest('package_config.json', {
+      '.packages': 'invalid .packages file',
+      'script.dart': 'main(){}',
+      'packages': {'shouldNotBeFound': <String, dynamic>{}},
+      '.dart_tool': {
+        'package_config.json': packageConfigFile,
+      }
+    }, (directory, loader) async {
+      var 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': <String, dynamic>{}}
+    }, (directory, loader) async {
+      var 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(){}',
+      }
+    }, (directory, loader) async {
+      var 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(){}'}
+    }, (directory, loader) async {
+      var 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': <String, dynamic>{},
+      }
+    }, (Uri directory, loader) async {
+      var 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(isA<FormatException>()));
+    });
+
+    loaderTest('invalid .packages as JSON', {
+      '.packages': packageConfigFile,
+    }, (Uri directory, loader) {
+      expect(() => findPackageConfigUri(directory, loader: loader),
+          throwsA(isA<FormatException>()));
+    });
+
+    loaderTest('invalid .packages', {
+      '.dart_tool': {
+        'package_config.json': 'not a JSON file',
+      }
+    }, (Uri directory, loader) {
+      expect(() => findPackageConfigUri(directory, loader: loader),
+          throwsA(isA<FormatException>()));
+    });
+
+    loaderTest('invalid .packages as INI', {
+      '.dart_tool': {
+        'package_config.json': packagesFile,
+      }
+    }, (Uri directory, loader) {
+      expect(() => findPackageConfigUri(directory, loader: loader),
+          throwsA(isA<FormatException>()));
+    });
+
+    // Does not find .packages if no package_config.json and minVersion > 1.
+    loaderTest('.packages ignored', {
+      '.packages': packagesFile,
+      'script.dart': 'main(){}'
+    }, (directory, loader) async {
+      var config =
+          await findPackageConfigUri(directory, minVersion: 2, loader: loader);
+      expect(config, null);
+    });
+
+    // Finds package_config.json in super-directory, with .packages in
+    // subdir and minVersion > 1.
+    loaderTest('package_config.json recursive ignores .packages', {
+      '.dart_tool': {
+        'package_config.json': packageConfigFile,
+      },
+      'subdir': {
+        '.packages': packagesFile,
+        'script.dart': 'main(){}',
+      }
+    }, (directory, loader) async {
+      var config = (await findPackageConfigUri(directory.resolve('subdir/'),
+          minVersion: 2, loader: loader))!;
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+  });
+
+  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 {
+        var file = directory.resolve('.dart_tool/package_config.json');
+        var config = await loadPackageConfigUri(file, loader: loader);
+        expect(config.version, 2);
+        validatePackagesFile(config, directory);
+      });
+      loaderTest('indirectly through .packages', files,
+          (Uri directory, loader) async {
+        var file = directory.resolve('.packages');
+        var 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 {
+      var file = directory.resolve('subdir/pheldagriff');
+      var 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 {
+      var file = directory.resolve('subdir/.packages');
+      var config = await loadPackageConfigUri(file, loader: loader);
+      expect(config.version, 2);
+      validatePackagesFile(config, directory);
+    });
+
+    loaderTest('.packages', {
+      '.packages': packagesFile,
+    }, (Uri directory, loader) async {
+      var file = directory.resolve('.packages');
+      var config = await loadPackageConfigUri(file, loader: loader);
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    loaderTest('.packages non-default name', {
+      'pheldagriff': packagesFile,
+    }, (Uri directory, loader) async {
+      var file = directory.resolve('pheldagriff');
+      var config = await loadPackageConfigUri(file, loader: loader);
+      expect(config.version, 1);
+      validatePackagesFile(config, directory);
+    });
+
+    loaderTest('no config found', {}, (Uri directory, loader) {
+      var file = directory.resolve('anyname');
+      expect(() => loadPackageConfigUri(file, loader: loader),
+          throwsA(isA<ArgumentError>()));
+    });
+
+    loaderTest('no config found, handle error', {},
+        (Uri directory, loader) async {
+      var file = directory.resolve('anyname');
+      var hadError = false;
+      await loadPackageConfigUri(file,
+          loader: loader,
+          onError: expectAsync1((error) {
+            hadError = true;
+            expect(error, isA<ArgumentError>());
+          }, max: -1));
+      expect(hadError, true);
+    });
+
+    loaderTest('specified file syntax error', {
+      'anyname': 'syntax error',
+    }, (Uri directory, loader) {
+      var file = directory.resolve('anyname');
+      expect(() => loadPackageConfigUri(file, loader: loader),
+          throwsFormatException);
+    });
+
+    loaderTest('specified file syntax onError', {
+      'anyname': 'syntax error',
+    }, (directory, loader) async {
+      var file = directory.resolve('anyname');
+      var hadError = false;
+      await loadPackageConfigUri(file,
+          loader: loader,
+          onError: expectAsync1((error) {
+            hadError = true;
+            expect(error, isA<FormatException>());
+          }, max: -1));
+      expect(hadError, true);
+    });
+
+    // Don't look for package_config.json if original file not named .packages.
+    loaderTest('specified file syntax error with alternative', {
+      'anyname': 'syntax error',
+      '.dart_tool': {
+        'package_config.json': packageConfigFile,
+      },
+    }, (directory, loader) async {
+      var file = directory.resolve('anyname');
+      expect(() => loadPackageConfigUri(file, loader: loader),
+          throwsFormatException);
+    });
+
+    // A file starting with `{` is a package_config.json file.
+    loaderTest('file syntax error with {', {
+      '.packages': '{syntax error',
+    }, (directory, loader) async {
+      var file = directory.resolve('.packages');
+      var hadError = false;
+      await loadPackageConfigUri(file,
+          loader: loader,
+          onError: expectAsync1((error) {
+            hadError = true;
+            expect(error, isA<FormatException>());
+          }, max: -1));
+      expect(hadError, true);
+    });
+  });
+}
diff --git a/pkgs/package_config/test/package_config_impl_test.dart b/pkgs/package_config/test/package_config_impl_test.dart
new file mode 100644
index 0000000..0f39963
--- /dev/null
+++ b/pkgs/package_config/test/package_config_impl_test.dart
@@ -0,0 +1,188 @@
+// Copyright (c) 2020, 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' show jsonDecode;
+
+import 'package:package_config/package_config_types.dart';
+import 'package:test/test.dart';
+import 'src/util.dart';
+
+void main() {
+  var unique = Object();
+  var root = Uri.file('/tmp/root/');
+
+  group('LanguageVersion', () {
+    test('minimal', () {
+      var version = LanguageVersion(3, 5);
+      expect(version.major, 3);
+      expect(version.minor, 5);
+    });
+
+    test('negative major', () {
+      expect(() => LanguageVersion(-1, 1), throwsArgumentError);
+    });
+
+    test('negative minor', () {
+      expect(() => LanguageVersion(1, -1), throwsArgumentError);
+    });
+
+    test('minimal parse', () {
+      var version = LanguageVersion.parse('3.5');
+      expect(version.major, 3);
+      expect(version.minor, 5);
+    });
+
+    void failParse(String name, String input) {
+      test('$name - error', () {
+        expect(() => LanguageVersion.parse(input),
+            throwsA(isA<PackageConfigError>()));
+        expect(() => LanguageVersion.parse(input), throwsFormatException);
+        var failed = false;
+        var actual = LanguageVersion.parse(input, onError: (_) {
+          failed = true;
+        });
+        expect(failed, true);
+        expect(actual, isA<LanguageVersion>());
+      });
+    }
+
+    failParse('Leading zero major', '01.1');
+    failParse('Leading zero minor', '1.01');
+    failParse('Sign+ major', '+1.1');
+    failParse('Sign- major', '-1.1');
+    failParse('Sign+ minor', '1.+1');
+    failParse('Sign- minor', '1.-1');
+    failParse('WhiteSpace 1', ' 1.1');
+    failParse('WhiteSpace 2', '1 .1');
+    failParse('WhiteSpace 3', '1. 1');
+    failParse('WhiteSpace 4', '1.1 ');
+  });
+
+  group('Package', () {
+    test('minimal', () {
+      var package = Package('name', root, extraData: unique);
+      expect(package.name, 'name');
+      expect(package.root, root);
+      expect(package.packageUriRoot, root);
+      expect(package.languageVersion, null);
+      expect(package.extraData, same(unique));
+    });
+
+    test('absolute package root', () {
+      var version = LanguageVersion(1, 1);
+      var absolute = root.resolve('foo/bar/');
+      var package = Package('name', root,
+          packageUriRoot: absolute,
+          relativeRoot: false,
+          languageVersion: version,
+          extraData: unique);
+      expect(package.name, 'name');
+      expect(package.root, root);
+      expect(package.packageUriRoot, absolute);
+      expect(package.languageVersion, version);
+      expect(package.extraData, same(unique));
+      expect(package.relativeRoot, false);
+    });
+
+    test('relative package root', () {
+      var relative = Uri.parse('foo/bar/');
+      var absolute = root.resolveUri(relative);
+      var package = Package('name', root,
+          packageUriRoot: relative, relativeRoot: true, extraData: unique);
+      expect(package.name, 'name');
+      expect(package.root, root);
+      expect(package.packageUriRoot, absolute);
+      expect(package.relativeRoot, true);
+      expect(package.languageVersion, null);
+      expect(package.extraData, same(unique));
+    });
+
+    for (var badName in ['a/z', 'a:z', '', '...']) {
+      test("Invalid name '$badName'", () {
+        expect(() => Package(badName, root), throwsPackageConfigError);
+      });
+    }
+
+    test('Invalid root, not absolute', () {
+      expect(
+          () => Package('name', Uri.parse('/foo/')), throwsPackageConfigError);
+    });
+
+    test('Invalid root, not ending in slash', () {
+      expect(() => Package('name', Uri.parse('file:///foo')),
+          throwsPackageConfigError);
+    });
+
+    test('invalid package root, not ending in slash', () {
+      expect(() => Package('name', root, packageUriRoot: Uri.parse('foo')),
+          throwsPackageConfigError);
+    });
+
+    test('invalid package root, not inside root', () {
+      expect(() => Package('name', root, packageUriRoot: Uri.parse('../baz/')),
+          throwsPackageConfigError);
+    });
+  });
+
+  group('package config', () {
+    test('empty', () {
+      var empty = PackageConfig([], extraData: unique);
+      expect(empty.version, 2);
+      expect(empty.packages, isEmpty);
+      expect(empty.extraData, same(unique));
+      expect(empty.resolve(pkg('a', 'b')), isNull);
+    });
+
+    test('single', () {
+      var package = Package('name', root);
+      var single = PackageConfig([package], extraData: unique);
+      expect(single.version, 2);
+      expect(single.packages, hasLength(1));
+      expect(single.extraData, same(unique));
+      expect(single.resolve(pkg('a', 'b')), isNull);
+      var resolved = single.resolve(pkg('name', 'a/b'));
+      expect(resolved, root.resolve('a/b'));
+    });
+  });
+  test('writeString', () {
+    var config = PackageConfig([
+      Package('foo', Uri.parse('file:///pkg/foo/'),
+          packageUriRoot: Uri.parse('file:///pkg/foo/lib/'),
+          relativeRoot: false,
+          languageVersion: LanguageVersion(2, 4),
+          extraData: {'foo': 'foo!'}),
+      Package('bar', Uri.parse('file:///pkg/bar/'),
+          packageUriRoot: Uri.parse('file:///pkg/bar/lib/'),
+          relativeRoot: true,
+          extraData: {'bar': 'bar!'}),
+    ], extraData: {
+      'extra': 'data'
+    });
+    var buffer = StringBuffer();
+    PackageConfig.writeString(config, buffer, Uri.parse('file:///pkg/'));
+    var text = buffer.toString();
+    var json = jsonDecode(text); // Is valid JSON.
+    expect(json, {
+      'configVersion': 2,
+      'packages': unorderedEquals([
+        {
+          'name': 'foo',
+          'rootUri': 'file:///pkg/foo/',
+          'packageUri': 'lib/',
+          'languageVersion': '2.4',
+          'foo': 'foo!',
+        },
+        {
+          'name': 'bar',
+          'rootUri': 'bar/',
+          'packageUri': 'lib/',
+          'bar': 'bar!',
+        },
+      ]),
+      'extra': 'data',
+    });
+  });
+}
+
+final Matcher throwsPackageConfigError = throwsA(isA<PackageConfigError>());
diff --git a/pkgs/package_config/test/parse_test.dart b/pkgs/package_config/test/parse_test.dart
new file mode 100644
index 0000000..a92b9bf
--- /dev/null
+++ b/pkgs/package_config/test/parse_test.dart
@@ -0,0 +1,552 @@
+// 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:typed_data';
+
+import 'package:package_config/package_config_types.dart';
+import 'package:package_config/src/errors.dart';
+import 'package:package_config/src/package_config_json.dart';
+import 'package:package_config/src/packages_file.dart' as packages;
+import 'package:test/test.dart';
+
+import 'src/util.dart';
+
+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'), throwError);
+      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'));
+
+      var foo = result['foo']!;
+      expect(foo, isNotNull);
+      expect(foo.root, Uri.parse('file:///foo/'));
+      expect(foo.packageUriRoot, Uri.parse('file:///foo/lib/'));
+      expect(foo.languageVersion, LanguageVersion(2, 7));
+      expect(foo.relativeRoot, false);
+    });
+
+    test('valid empty', () {
+      var packagesFile = '# Generated by pub yadda yadda\n';
+      var result = packages.parse(
+          utf8.encode(packagesFile), Uri.file('/tmp/file.dart'), throwError);
+      expect(result.version, 1);
+      expect({for (var p in result.packages) p.name}, <String>{});
+    });
+
+    group('invalid', () {
+      var baseFile = Uri.file('/tmp/file.dart');
+      void testThrows(String name, String content) {
+        test(name, () {
+          expect(
+              () => packages.parse(utf8.encode(content), baseFile, throwError),
+              throwsA(isA<FormatException>()));
+        });
+        test('$name, handle error', () {
+          var hadError = false;
+          packages.parse(utf8.encode(content), baseFile, (error) {
+            hadError = true;
+            expect(error, isA<FormatException>());
+          });
+          expect(hadError, true);
+        });
+      }
+
+      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": "9999.9999"
+            },
+            {
+              "name": "baz",
+              "rootUri": "../",
+              "packageUri": "lib/"
+            },
+            {
+              "name": "noslash",
+              "rootUri": "../noslash",
+              "packageUri": "lib"
+            }
+          ],
+          "generator": "pub",
+          "other": [42]
+        }
+        ''';
+      var config = parsePackageConfigBytes(
+          // ignore: unnecessary_cast
+          utf8.encode(packageConfigFile) as Uint8List,
+          Uri.parse('file:///tmp/.dart_tool/file.dart'),
+          throwError);
+      expect(config.version, 2);
+      expect({for (var p in config.packages) p.name},
+          {'foo', 'bar', 'baz', 'noslash'});
+
+      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, LanguageVersion(2, 5));
+      expect(foo.extraData, {'nonstandard': true});
+      expect(foo.relativeRoot, false);
+
+      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, LanguageVersion(9999, 9999));
+      expect(bar.extraData, null);
+      expect(bar.relativeRoot, false);
+
+      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(baz.relativeRoot, true);
+
+      // No slash after root or package root. One is inserted.
+      var noslash = config['noslash']!;
+      expect(noslash, isNotNull);
+      expect(noslash.root, Uri.parse('file:///tmp/noslash/'));
+      expect(noslash.packageUriRoot, Uri.parse('file:///tmp/noslash/lib/'));
+      expect(noslash.languageVersion, null);
+      expect(noslash.relativeRoot, true);
+
+      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": "9999.9999",
+              "rootUri": "/bar/",
+              "name": "bar"
+            },
+            {
+              "packageUri": "lib/",
+              "name": "baz",
+              "rootUri": "../"
+            }
+          ],
+          "configVersion": 2
+        }
+        ''';
+      var config = parsePackageConfigBytes(
+          // ignore: unnecessary_cast
+          utf8.encode(packageConfigFile) as Uint8List,
+          Uri.parse('file:///tmp/.dart_tool/file.dart'),
+          throwError);
+      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(
+          // ignore: unnecessary_cast
+          utf8.encode('{$cfg,$pkgs}') as Uint8List,
+          Uri.parse('file:///tmp/.dart_tool/file.dart'),
+          throwError);
+      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(
+          // ignore: unnecessary_cast
+          utf8.encode('{$cfg,"packages":[{$name,$root}]}') as Uint8List,
+          Uri.parse('file:///tmp/.dart_tool/file.dart'),
+          throwError);
+      expect(config.version, 2);
+      expect(config.packages.first.name, 'foo');
+    });
+
+    test('nested packages', () {
+      var configBytes = utf8.encode(json.encode({
+        'configVersion': 2,
+        'packages': [
+          {'name': 'foo', 'rootUri': '/foo/', 'packageUri': 'lib/'},
+          {'name': 'bar', 'rootUri': '/foo/bar/', 'packageUri': 'lib/'},
+          {'name': 'baz', 'rootUri': '/foo/bar/baz/', 'packageUri': 'lib/'},
+          {'name': 'qux', 'rootUri': '/foo/qux/', 'packageUri': 'lib/'},
+        ]
+      }));
+      // ignore: unnecessary_cast
+      var config = parsePackageConfigBytes(configBytes as Uint8List,
+          Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError);
+      expect(config.version, 2);
+      expect(config.packageOf(Uri.parse('file:///foo/lala/lala.dart'))!.name,
+          'foo');
+      expect(config.packageOf(Uri.parse('file:///foo/bar/lala.dart'))!.name,
+          'bar');
+      expect(config.packageOf(Uri.parse('file:///foo/bar/baz/lala.dart'))!.name,
+          'baz');
+      expect(config.packageOf(Uri.parse('file:///foo/qux/lala.dart'))!.name,
+          'qux');
+      expect(config.toPackageUri(Uri.parse('file:///foo/lib/diz')),
+          Uri.parse('package:foo/diz'));
+      expect(config.toPackageUri(Uri.parse('file:///foo/bar/lib/diz')),
+          Uri.parse('package:bar/diz'));
+      expect(config.toPackageUri(Uri.parse('file:///foo/bar/baz/lib/diz')),
+          Uri.parse('package:baz/diz'));
+      expect(config.toPackageUri(Uri.parse('file:///foo/qux/lib/diz')),
+          Uri.parse('package:qux/diz'));
+    });
+
+    test('nested packages 2', () {
+      var configBytes = utf8.encode(json.encode({
+        'configVersion': 2,
+        'packages': [
+          {'name': 'foo', 'rootUri': '/', 'packageUri': 'lib/'},
+          {'name': 'bar', 'rootUri': '/bar/', 'packageUri': 'lib/'},
+          {'name': 'baz', 'rootUri': '/bar/baz/', 'packageUri': 'lib/'},
+          {'name': 'qux', 'rootUri': '/qux/', 'packageUri': 'lib/'},
+        ]
+      }));
+      // ignore: unnecessary_cast
+      var config = parsePackageConfigBytes(configBytes as Uint8List,
+          Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError);
+      expect(config.version, 2);
+      expect(
+          config.packageOf(Uri.parse('file:///lala/lala.dart'))!.name, 'foo');
+      expect(config.packageOf(Uri.parse('file:///bar/lala.dart'))!.name, 'bar');
+      expect(config.packageOf(Uri.parse('file:///bar/baz/lala.dart'))!.name,
+          'baz');
+      expect(config.packageOf(Uri.parse('file:///qux/lala.dart'))!.name, 'qux');
+      expect(config.toPackageUri(Uri.parse('file:///lib/diz')),
+          Uri.parse('package:foo/diz'));
+      expect(config.toPackageUri(Uri.parse('file:///bar/lib/diz')),
+          Uri.parse('package:bar/diz'));
+      expect(config.toPackageUri(Uri.parse('file:///bar/baz/lib/diz')),
+          Uri.parse('package:baz/diz'));
+      expect(config.toPackageUri(Uri.parse('file:///qux/lib/diz')),
+          Uri.parse('package:qux/diz'));
+    });
+
+    test('packageOf is case sensitive on windows', () {
+      var configBytes = utf8.encode(json.encode({
+        'configVersion': 2,
+        'packages': [
+          {'name': 'foo', 'rootUri': 'file:///C:/Foo/', 'packageUri': 'lib/'},
+        ]
+      }));
+      var config = parsePackageConfigBytes(
+          // ignore: unnecessary_cast
+          configBytes as Uint8List,
+          Uri.parse('file:///C:/tmp/.dart_tool/file.dart'),
+          throwError);
+      expect(config.version, 2);
+      expect(
+          config.packageOf(Uri.parse('file:///C:/foo/lala/lala.dart')), null);
+      expect(config.packageOf(Uri.parse('file:///C:/Foo/lala/lala.dart'))!.name,
+          'foo');
+    });
+
+    group('invalid', () {
+      void testThrows(String name, String source) {
+        test(name, () {
+          expect(
+              // ignore: unnecessary_cast
+              () => parsePackageConfigBytes(utf8.encode(source) as Uint8List,
+                  Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError),
+              throwsA(isA<FormatException>()));
+        });
+      }
+
+      void testThrowsContains(
+          String name, String source, String containsString) {
+        test(name, () {
+          dynamic exception;
+          try {
+            parsePackageConfigBytes(
+              // ignore: unnecessary_cast
+              utf8.encode(source) as Uint8List,
+              Uri.parse('file:///tmp/.dart_tool/file.dart'),
+              throwError,
+            );
+          } catch (e) {
+            exception = e;
+          }
+          if (exception == null) fail("Didn't get exception");
+          expect('$exception', contains(containsString));
+        });
+      }
+
+      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/"}]}');
+      testThrowsContains(
+          // The roots of foo and bar are the same.
+          'same roots',
+          '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}',
+          'the same root directory');
+      testThrowsContains(
+          // The roots of foo and bar are the same.
+          'same roots 2',
+          '{$cfg,"packages":[{$name,"rootUri":"/"},{"name":"bar","rootUri":"/"}]}',
+          'the same root directory');
+      testThrowsContains(
+          // The root of bar is inside the root of foo,
+          // but the package root of foo is inside the root of bar.
+          'between root and lib',
+          '{$cfg,"packages":['
+              '{"name":"foo","rootUri":"/foo/","packageUri":"bar/lib/"},'
+              '{"name":"bar","rootUri":"/foo/bar/","packageUri":"baz/lib"}]}',
+          'package root of foo is inside the root of bar');
+
+      // This shouldn't be allowed, but for internal reasons it is.
+      test('package inside package root', () {
+        var config = parsePackageConfigBytes(
+            // ignore: unnecessary_cast
+            utf8.encode(
+              '{$cfg,"packages":['
+              '{"name":"foo","rootUri":"/foo/","packageUri":"lib/"},'
+              '{"name":"bar","rootUri":"/foo/lib/bar/","packageUri":"lib"}]}',
+            ) as Uint8List,
+            Uri.parse('file:///tmp/.dart_tool/file.dart'),
+            throwError);
+        expect(
+            config
+                .packageOf(Uri.parse('file:///foo/lib/bar/lib/lala.dart'))!
+                .name,
+            'foo'); // why not bar?
+        expect(config.toPackageUri(Uri.parse('file:///foo/lib/bar/lib/diz')),
+            Uri.parse('package:foo/bar/lib/diz')); // why not package:bar/diz?
+      });
+    });
+  });
+
+  group('factories', () {
+    void testConfig(String name, PackageConfig config, PackageConfig expected) {
+      group(name, () {
+        test('structure', () {
+          expect(config.version, expected.version);
+          var expectedPackages = {for (var p in expected.packages) p.name};
+          var actualPackages = {for (var p in config.packages) p.name};
+          expect(actualPackages, expectedPackages);
+        });
+        for (var package in config.packages) {
+          var name = package.name;
+          test('package $name', () {
+            var expectedPackage = expected[name]!;
+            expect(expectedPackage, isNotNull);
+            expect(package.root, expectedPackage.root, reason: 'root');
+            expect(package.packageUriRoot, expectedPackage.packageUriRoot,
+                reason: 'package root');
+            expect(package.languageVersion, expectedPackage.languageVersion,
+                reason: 'languageVersion');
+          });
+        }
+      });
+    }
+
+    var configText = '''
+     {"configVersion": 2, "packages": [
+       {
+         "name": "foo",
+         "rootUri": "foo/",
+         "packageUri": "bar/",
+         "languageVersion": "1.2"
+       }
+     ]}
+    ''';
+    var baseUri = Uri.parse('file:///start/');
+    var config = PackageConfig([
+      Package('foo', Uri.parse('file:///start/foo/'),
+          packageUriRoot: Uri.parse('file:///start/foo/bar/'),
+          languageVersion: LanguageVersion(1, 2))
+    ]);
+    testConfig(
+        'string', PackageConfig.parseString(configText, baseUri), config);
+    testConfig(
+        'bytes',
+        PackageConfig.parseBytes(
+            Uint8List.fromList(configText.codeUnits), baseUri),
+        config);
+    testConfig('json', PackageConfig.parseJson(jsonDecode(configText), baseUri),
+        config);
+
+    baseUri = Uri.parse('file:///start2/');
+    config = PackageConfig([
+      Package('foo', Uri.parse('file:///start2/foo/'),
+          packageUriRoot: Uri.parse('file:///start2/foo/bar/'),
+          languageVersion: LanguageVersion(1, 2))
+    ]);
+    testConfig(
+        'string2', PackageConfig.parseString(configText, baseUri), config);
+    testConfig(
+        'bytes2',
+        PackageConfig.parseBytes(
+            Uint8List.fromList(configText.codeUnits), baseUri),
+        config);
+    testConfig('json2',
+        PackageConfig.parseJson(jsonDecode(configText), baseUri), config);
+  });
+}
diff --git a/pkgs/package_config/test/src/util.dart b/pkgs/package_config/test/src/util.dart
new file mode 100644
index 0000000..780ee80
--- /dev/null
+++ b/pkgs/package_config/test/src/util.dart
@@ -0,0 +1,57 @@
+// 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:typed_data';
+
+import 'package:test/test.dart';
+
+/// Creates a package: URI.
+Uri pkg(String packageName, String packagePath) {
+  var path =
+      "$packageName${packagePath.startsWith('/') ? "" : "/"}$packagePath";
+  return 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 [loaderTest].
+///
+/// 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 Function(Uri root, Future<Uint8List?> Function(Uri) loader) loaderTest,
+) {
+  var 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('/');
+    Object? value = description;
+    for (var i = 1; i < parts.length; i++) {
+      if (value is! Map<String, Object?>) return null;
+      value = value[parts[i]];
+    }
+    // ignore: unnecessary_cast
+    if (value is String) return utf8.encode(value) as Uint8List;
+    return null;
+  }
+
+  test(name, () => loaderTest(root, loader));
+}
diff --git a/pkgs/package_config/test/src/util_io.dart b/pkgs/package_config/test/src/util_io.dart
new file mode 100644
index 0000000..e032556
--- /dev/null
+++ b/pkgs/package_config/test/src/util_io.dart
@@ -0,0 +1,62 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:package_config/src/util_io.dart';
+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 Function(Directory directory) fileTest) {
+  group('file-test', () {
+    var 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 = pathJoin(target.path, '$name');
+    if (content is Map<Object?, Object?>) {
+      _createFiles(Directory(entryName)..createSync(), content);
+    } else {
+      File(entryName).writeAsStringSync(content as String, flush: true);
+    }
+  });
+}
+
+/// Creates a [Directory] for a subdirectory of [parent].
+Directory subdir(Directory parent, String dirName) =>
+    Directory(pathJoinAll([parent.path, ...dirName.split('/')]));
+
+/// Creates a [File] for an entry in the [directory] directory.
+File dirFile(Directory directory, String fileName) =>
+    File(pathJoin(directory.path, fileName));
diff --git a/pkgs/pool/.gitignore b/pkgs/pool/.gitignore
new file mode 100644
index 0000000..e450c83
--- /dev/null
+++ b/pkgs/pool/.gitignore
@@ -0,0 +1,5 @@
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+.pub/
+pubspec.lock
diff --git a/pkgs/pool/CHANGELOG.md b/pkgs/pool/CHANGELOG.md
new file mode 100644
index 0000000..56424fc
--- /dev/null
+++ b/pkgs/pool/CHANGELOG.md
@@ -0,0 +1,105 @@
+## 1.5.2-wip
+
+* Require Dart 3.4.
+* Move to `dart-lang/tools` monorepo.
+
+## 1.5.1
+
+* Populate the pubspec `repository` field.
+
+## 1.5.0
+
+* Stable release for null safety.
+
+## 1.5.0-nullsafety.3
+
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+## 1.5.0-nullsafety.2
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.5.0-nullsafety.1
+
+* Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 1.5.0-nullsafety
+
+* Migrate to null safety.
+* `forEach`: Avoid `await null` if the `Stream` is not paused.
+  Improves trivial benchmark by 40%.
+
+## 1.4.0
+
+* Add `forEach` to `Pool` to support efficient async processing of an
+  `Iterable`.
+
+* Throw ArgumentError if poolSize <= 0
+
+## 1.3.6
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.3.5
+
+- Updated SDK version to 2.0.0-dev.17.0
+
+## 1.3.4
+
+* Modify code to eliminate Future flattening.
+
+## 1.3.3
+
+* Declare support for `async` 2.0.0.
+
+## 1.3.2
+
+* Update to make the code work with strong-mode clean Zone API.
+
+* Required minimum SDK of 1.23.0.
+
+## 1.3.1
+
+* Fix the type annotation of `Pool.withResource()` to indicate that it takes
+  `() -> FutureOr<T>`.
+
+## 1.3.0
+
+* Add a `Pool.done` getter that returns the same future returned by
+  `Pool.close()`.
+
+## 1.2.4
+
+* Fix a strong-mode error.
+
+## 1.2.3
+
+* Fix a bug in which `Pool.withResource()` could throw a `StateError` when
+  called immediately before closing the pool.
+
+## 1.2.2
+
+* Fix strong mode warnings and add generic method annotations.
+
+## 1.2.1
+
+* Internal changes only.
+
+## 1.2.0
+
+* Add `Pool.close()`, which forbids new resource requests and releases all
+  releasable resources.
+
+## 1.1.0
+
+* Add `PoolResource.allowRelease()`, which allows a resource to indicate that it
+  can be released without forcing it to deallocate immediately.
+
+## 1.0.2
+
+* Fixed the homepage.
+
+## 1.0.1
+
+* A `TimeoutException` is now correctly thrown if the pool detects a deadlock.
diff --git a/pkgs/pool/LICENSE b/pkgs/pool/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/pool/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/pool/README.md b/pkgs/pool/README.md
new file mode 100644
index 0000000..461e872
--- /dev/null
+++ b/pkgs/pool/README.md
@@ -0,0 +1,57 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/pool.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/pool.yaml)
+[![pub package](https://img.shields.io/pub/v/pool.svg)](https://pub.dev/packages/pool)
+[![package publisher](https://img.shields.io/pub/publisher/pool.svg)](https://pub.dev/packages/pool/publisher)
+
+The pool package exposes a `Pool` class which makes it easy to manage a limited
+pool of resources.
+
+The easiest way to use a pool is by calling `withResource`. This runs a callback
+and returns its result, but only once there aren't too many other callbacks
+currently running.
+
+```dart
+// Create a Pool that will only allocate 10 resources at once. After 30 seconds
+// of inactivity with all resources checked out, the pool will throw an error.
+final pool = new Pool(10, timeout: new Duration(seconds: 30));
+
+Future<String> readFile(String path) {
+  // Since the call to [File.readAsString] is within [withResource], no more
+  // than ten files will be open at once.
+  return pool.withResource(() => new File(path).readAsString());
+}
+```
+
+For more fine-grained control, the user can also explicitly request generic
+`PoolResource` objects that can later be released back into the pool. This is
+what `withResource` does under the covers: requests a resource, then releases it
+once the callback completes.
+
+`Pool` ensures that only a limited number of resources are allocated at once.
+It's the caller's responsibility to ensure that the corresponding physical
+resource is only consumed when a `PoolResource` is allocated.
+
+```dart
+class PooledFile implements RandomAccessFile {
+  final RandomAccessFile _file;
+  final PoolResource _resource;
+
+  static Future<PooledFile> open(String path) {
+    return pool.request().then((resource) {
+      return new File(path).open().then((file) {
+        return new PooledFile._(file, resource);
+      });
+    });
+  }
+
+  PooledFile(this._file, this._resource);
+
+  // ...
+
+  Future<RandomAccessFile> close() {
+    return _file.close.then((_) {
+      _resource.release();
+      return this;
+    });
+  }
+}
+```
diff --git a/pkgs/pool/analysis_options.yaml b/pkgs/pool/analysis_options.yaml
new file mode 100644
index 0000000..44cda4d
--- /dev/null
+++ b/pkgs/pool/analysis_options.yaml
@@ -0,0 +1,5 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
diff --git a/pkgs/pool/benchmark/for_each_benchmark.dart b/pkgs/pool/benchmark/for_each_benchmark.dart
new file mode 100644
index 0000000..0cd2543
--- /dev/null
+++ b/pkgs/pool/benchmark/for_each_benchmark.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2024, 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:pool/pool.dart';
+
+void main(List<String> args) async {
+  var poolSize = args.isEmpty ? 5 : int.parse(args.first);
+  print('Pool size: $poolSize');
+
+  final pool = Pool(poolSize);
+  final watch = Stopwatch()..start();
+  final start = DateTime.now();
+
+  DateTime? lastLog;
+  Duration? fastest;
+  late int fastestIteration;
+  var i = 1;
+
+  void log(bool force) {
+    var now = DateTime.now();
+    if (force ||
+        lastLog == null ||
+        now.difference(lastLog!) > const Duration(seconds: 1)) {
+      lastLog = now;
+      print([
+        now.difference(start),
+        i.toString().padLeft(10),
+        fastestIteration.toString().padLeft(7),
+        fastest!.inMicroseconds.toString().padLeft(9)
+      ].join('   '));
+    }
+  }
+
+  print(['Elapsed       ', 'Iterations', 'Fastest', 'Time (us)'].join('   '));
+
+  for (;; i++) {
+    watch.reset();
+
+    var sum = await pool
+        .forEach<int, int>(Iterable<int>.generate(100000), (i) => i)
+        .reduce((a, b) => a + b);
+
+    assert(sum == 4999950000, 'was $sum');
+
+    var elapsed = watch.elapsed;
+    if (fastest == null || fastest > elapsed) {
+      fastest = elapsed;
+      fastestIteration = i;
+      log(true);
+    } else {
+      log(false);
+    }
+  }
+}
diff --git a/pkgs/pool/lib/pool.dart b/pkgs/pool/lib/pool.dart
new file mode 100644
index 0000000..70e9df1
--- /dev/null
+++ b/pkgs/pool/lib/pool.dart
@@ -0,0 +1,380 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:async/async.dart';
+import 'package:stack_trace/stack_trace.dart';
+
+/// Manages an abstract pool of resources with a limit on how many may be in use
+/// at once.
+///
+/// When a resource is needed, the user should call [request]. When the returned
+/// future completes with a [PoolResource], the resource may be allocated. Once
+/// the resource has been released, the user should call [PoolResource.release].
+/// The pool will ensure that only a certain number of [PoolResource]s may be
+/// allocated at once.
+class Pool {
+  /// Completers for requests beyond the first [_maxAllocatedResources].
+  ///
+  /// When an item is released, the next element of [_requestedResources] will
+  /// be completed.
+  final _requestedResources = Queue<Completer<PoolResource>>();
+
+  /// Callbacks that must be called before additional resources can be
+  /// allocated.
+  ///
+  /// See [PoolResource.allowRelease].
+  final _onReleaseCallbacks = Queue<void Function()>();
+
+  /// Completers that will be completed once `onRelease` callbacks are done
+  /// running.
+  ///
+  /// These are kept in a queue to ensure that the earliest request completes
+  /// first regardless of what order the `onRelease` callbacks complete in.
+  final _onReleaseCompleters = Queue<Completer<PoolResource>>();
+
+  /// The maximum number of resources that may be allocated at once.
+  final int _maxAllocatedResources;
+
+  /// The number of resources that are currently allocated.
+  int _allocatedResources = 0;
+
+  /// The timeout timer.
+  ///
+  /// This timer is canceled as long as the pool is below the resource limit.
+  /// It's reset once the resource limit is reached and again every time an
+  /// resource is released or a new resource is requested. If it fires, that
+  /// indicates that the caller became deadlocked, likely due to files waiting
+  /// for additional files to be read before they could be closed.
+  ///
+  /// This is `null` if this pool shouldn't time out.
+  RestartableTimer? _timer;
+
+  /// The amount of time to wait before timing out the pending resources.
+  final Duration? _timeout;
+
+  /// A [FutureGroup] that tracks all the `onRelease` callbacks for resources
+  /// that have been marked releasable.
+  ///
+  /// This is `null` until [close] is called.
+  FutureGroup? _closeGroup;
+
+  /// Whether [close] has been called.
+  bool get isClosed => _closeMemo.hasRun;
+
+  /// A future that completes once the pool is closed and all its outstanding
+  /// resources have been released.
+  ///
+  /// If any [PoolResource.allowRelease] callback throws an exception after the
+  /// pool is closed, this completes with that exception.
+  Future get done => _closeMemo.future;
+
+  /// Creates a new pool with the given limit on how many resources may be
+  /// allocated at once.
+  ///
+  /// If [timeout] is passed, then if that much time passes without any activity
+  /// all pending [request] futures will throw a [TimeoutException]. This is
+  /// intended to avoid deadlocks.
+  Pool(this._maxAllocatedResources, {Duration? timeout}) : _timeout = timeout {
+    if (_maxAllocatedResources <= 0) {
+      throw ArgumentError.value(_maxAllocatedResources, 'maxAllocatedResources',
+          'Must be greater than zero.');
+    }
+
+    if (timeout != null) {
+      // Start the timer canceled since we only want to start counting down once
+      // we've run out of available resources.
+      _timer = RestartableTimer(timeout, _onTimeout)..cancel();
+    }
+  }
+
+  /// Request a [PoolResource].
+  ///
+  /// If the maximum number of resources is already allocated, this will delay
+  /// until one of them is released.
+  Future<PoolResource> request() {
+    if (isClosed) {
+      throw StateError('request() may not be called on a closed Pool.');
+    }
+
+    if (_allocatedResources < _maxAllocatedResources) {
+      _allocatedResources++;
+      return Future.value(PoolResource._(this));
+    } else if (_onReleaseCallbacks.isNotEmpty) {
+      return _runOnRelease(_onReleaseCallbacks.removeFirst());
+    } else {
+      var completer = Completer<PoolResource>();
+      _requestedResources.add(completer);
+      _resetTimer();
+      return completer.future;
+    }
+  }
+
+  /// Requests a resource for the duration of [callback], which may return a
+  /// Future.
+  ///
+  /// The return value of [callback] is piped to the returned Future.
+  Future<T> withResource<T>(FutureOr<T> Function() callback) async {
+    if (isClosed) {
+      throw StateError('withResource() may not be called on a closed Pool.');
+    }
+
+    var resource = await request();
+    try {
+      return await callback();
+    } finally {
+      resource.release();
+    }
+  }
+
+  /// Returns a [Stream] containing the result of [action] applied to each
+  /// element of [elements].
+  ///
+  /// While [action] is invoked on each element of [elements] in order,
+  /// it's possible the return [Stream] may have items out-of-order – especially
+  /// if the completion time of [action] varies.
+  ///
+  /// If [action] throws an error the source item along with the error object
+  /// and [StackTrace] are passed to [onError], if it is provided. If [onError]
+  /// returns `true`, the error is added to the returned [Stream], otherwise
+  /// it is ignored.
+  ///
+  /// Errors thrown from iterating [elements] will not be passed to
+  /// [onError]. They will always be added to the returned stream as an error.
+  ///
+  /// Note: all of the resources of the this [Pool] will be used when the
+  /// returned [Stream] is listened to until it is completed or canceled.
+  ///
+  /// Note: if this [Pool] is closed before the returned [Stream] is listened
+  /// to, a [StateError] is thrown.
+  Stream<T> forEach<S, T>(
+      Iterable<S> elements, FutureOr<T> Function(S source) action,
+      {bool Function(S item, Object error, StackTrace stack)? onError}) {
+    onError ??= (item, e, s) => true;
+
+    var cancelPending = false;
+
+    Completer? resumeCompleter;
+    late StreamController<T> controller;
+
+    late Iterator<S> iterator;
+
+    Future<void> run(int _) async {
+      while (iterator.moveNext()) {
+        // caching `current` is necessary because there are async breaks
+        // in this code and `iterator` is shared across many workers
+        final current = iterator.current;
+
+        _resetTimer();
+
+        if (resumeCompleter != null) {
+          await resumeCompleter!.future;
+        }
+
+        if (cancelPending) {
+          break;
+        }
+
+        T value;
+        try {
+          value = await action(current);
+        } catch (e, stack) {
+          if (onError!(current, e, stack)) {
+            controller.addError(e, stack);
+          }
+          continue;
+        }
+        controller.add(value);
+      }
+    }
+
+    Future<void>? doneFuture;
+
+    void onListen() {
+      iterator = elements.iterator;
+
+      assert(doneFuture == null);
+      var futures = Iterable<Future<void>>.generate(
+          _maxAllocatedResources, (i) => withResource(() => run(i)));
+      doneFuture = Future.wait(futures, eagerError: true)
+          .then<void>((_) {})
+          .catchError(controller.addError);
+
+      doneFuture!.whenComplete(controller.close);
+    }
+
+    controller = StreamController<T>(
+      sync: true,
+      onListen: onListen,
+      onCancel: () async {
+        assert(!cancelPending);
+        cancelPending = true;
+        await doneFuture;
+      },
+      onPause: () {
+        assert(resumeCompleter == null);
+        resumeCompleter = Completer<void>();
+      },
+      onResume: () {
+        assert(resumeCompleter != null);
+        resumeCompleter!.complete();
+        resumeCompleter = null;
+      },
+    );
+
+    return controller.stream;
+  }
+
+  /// Closes the pool so that no more resources are requested.
+  ///
+  /// Existing resource requests remain unchanged.
+  ///
+  /// Any resources that are marked as releasable using
+  /// [PoolResource.allowRelease] are released immediately. Once all resources
+  /// have been released and any `onRelease` callbacks have completed, the
+  /// returned future completes successfully. If any `onRelease` callback throws
+  /// an error, the returned future completes with that error.
+  ///
+  /// This may be called more than once; it returns the same [Future] each time.
+  Future close() => _closeMemo.runOnce(_close);
+
+  Future<void> _close() {
+    if (_closeGroup != null) return _closeGroup!.future;
+
+    _resetTimer();
+
+    _closeGroup = FutureGroup();
+    for (var callback in _onReleaseCallbacks) {
+      _closeGroup!.add(Future.sync(callback));
+    }
+
+    _allocatedResources -= _onReleaseCallbacks.length;
+    _onReleaseCallbacks.clear();
+
+    if (_allocatedResources == 0) _closeGroup!.close();
+    return _closeGroup!.future;
+  }
+
+  final _closeMemo = AsyncMemoizer<void>();
+
+  /// If there are any pending requests, this will fire the oldest one.
+  void _onResourceReleased() {
+    _resetTimer();
+
+    if (_requestedResources.isNotEmpty) {
+      var pending = _requestedResources.removeFirst();
+      pending.complete(PoolResource._(this));
+    } else {
+      _allocatedResources--;
+      if (isClosed && _allocatedResources == 0) _closeGroup!.close();
+    }
+  }
+
+  /// If there are any pending requests, this will fire the oldest one after
+  /// running [onRelease].
+  void _onResourceReleaseAllowed(void Function() onRelease) {
+    _resetTimer();
+
+    if (_requestedResources.isNotEmpty) {
+      var pending = _requestedResources.removeFirst();
+      pending.complete(_runOnRelease(onRelease));
+    } else if (isClosed) {
+      _closeGroup!.add(Future.sync(onRelease));
+      _allocatedResources--;
+      if (_allocatedResources == 0) _closeGroup!.close();
+    } else {
+      var zone = Zone.current;
+      var registered = zone.registerCallback(onRelease);
+      _onReleaseCallbacks.add(() => zone.run(registered));
+    }
+  }
+
+  /// Runs [onRelease] and returns a Future that completes to a resource once an
+  /// [onRelease] callback completes.
+  ///
+  /// Futures returned by [_runOnRelease] always complete in the order they were
+  /// created, even if earlier [onRelease] callbacks take longer to run.
+  Future<PoolResource> _runOnRelease(void Function() onRelease) {
+    Future.sync(onRelease).then((value) {
+      _onReleaseCompleters.removeFirst().complete(PoolResource._(this));
+    }).catchError((Object error, StackTrace stackTrace) {
+      _onReleaseCompleters.removeFirst().completeError(error, stackTrace);
+    });
+
+    var completer = Completer<PoolResource>.sync();
+    _onReleaseCompleters.add(completer);
+    return completer.future;
+  }
+
+  /// A resource has been requested, allocated, or released.
+  void _resetTimer() {
+    if (_timer == null) return;
+
+    if (_requestedResources.isEmpty) {
+      _timer!.cancel();
+    } else {
+      _timer!.reset();
+    }
+  }
+
+  /// Handles [_timer] timing out by causing all pending resource completers to
+  /// emit exceptions.
+  void _onTimeout() {
+    for (var completer in _requestedResources) {
+      completer.completeError(
+          TimeoutException(
+              'Pool deadlock: all resources have been '
+              'allocated for too long.',
+              _timeout),
+          Chain.current());
+    }
+    _requestedResources.clear();
+    _timer = null;
+  }
+}
+
+/// A member of a [Pool].
+///
+/// A [PoolResource] is a token that indicates that a resource is allocated.
+/// When the associated resource is released, the user should call [release].
+class PoolResource {
+  final Pool _pool;
+
+  /// Whether `this` has been released yet.
+  bool _released = false;
+
+  PoolResource._(this._pool);
+
+  /// Tells the parent [Pool] that the resource associated with this resource is
+  /// no longer allocated, and that a new [PoolResource] may be allocated.
+  void release() {
+    if (_released) {
+      throw StateError('A PoolResource may only be released once.');
+    }
+    _released = true;
+    _pool._onResourceReleased();
+  }
+
+  /// Tells the parent [Pool] that the resource associated with this resource is
+  /// no longer necessary, but should remain allocated until more resources are
+  /// needed.
+  ///
+  /// When [Pool.request] is called and there are no remaining available
+  /// resources, the [onRelease] callback is called. It should free the
+  /// resource, and it may return a Future or `null`. Once that completes, the
+  /// [Pool.request] call will complete to a new [PoolResource].
+  ///
+  /// This is useful when a resource's main function is complete, but it may
+  /// produce additional information later on. For example, an isolate's task
+  /// may be complete, but it could still emit asynchronous errors.
+  void allowRelease(FutureOr<void> Function() onRelease) {
+    if (_released) {
+      throw StateError('A PoolResource may only be released once.');
+    }
+    _released = true;
+    _pool._onResourceReleaseAllowed(onRelease);
+  }
+}
diff --git a/pkgs/pool/pubspec.yaml b/pkgs/pool/pubspec.yaml
new file mode 100644
index 0000000..a205b74
--- /dev/null
+++ b/pkgs/pool/pubspec.yaml
@@ -0,0 +1,18 @@
+name: pool
+version: 1.5.2-wip
+description: >-
+  Manage a finite pool of resources.
+  Useful for controlling concurrent file system or network requests.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/pool
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  async: ^2.5.0
+  stack_trace: ^1.10.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  fake_async: ^1.2.0
+  test: ^1.16.6
diff --git a/pkgs/pool/test/pool_test.dart b/pkgs/pool/test/pool_test.dart
new file mode 100644
index 0000000..6334a8a
--- /dev/null
+++ b/pkgs/pool/test/pool_test.dart
@@ -0,0 +1,745 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:fake_async/fake_async.dart';
+import 'package:pool/pool.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('request()', () {
+    test('resources can be requested freely up to the limit', () {
+      var pool = Pool(50);
+      for (var i = 0; i < 50; i++) {
+        expect(pool.request(), completes);
+      }
+    });
+
+    test('resources block past the limit', () {
+      FakeAsync().run((async) {
+        var pool = Pool(50);
+        for (var i = 0; i < 50; i++) {
+          expect(pool.request(), completes);
+        }
+        expect(pool.request(), doesNotComplete);
+
+        async.elapse(const Duration(seconds: 1));
+      });
+    });
+
+    test('a blocked resource is allocated when another is released', () {
+      FakeAsync().run((async) {
+        var pool = Pool(50);
+        for (var i = 0; i < 49; i++) {
+          expect(pool.request(), completes);
+        }
+
+        pool.request().then((lastAllocatedResource) {
+          // This will only complete once [lastAllocatedResource] is released.
+          expect(pool.request(), completes);
+
+          Future<void>.delayed(const Duration(microseconds: 1)).then((_) {
+            lastAllocatedResource.release();
+          });
+        });
+
+        async.elapse(const Duration(seconds: 1));
+      });
+    });
+  });
+
+  group('withResource()', () {
+    test('can be called freely up to the limit', () {
+      var pool = Pool(50);
+      for (var i = 0; i < 50; i++) {
+        pool.withResource(expectAsync0(() => Completer<void>().future));
+      }
+    });
+
+    test('blocks the callback past the limit', () {
+      FakeAsync().run((async) {
+        var pool = Pool(50);
+        for (var i = 0; i < 50; i++) {
+          pool.withResource(expectAsync0(() => Completer<void>().future));
+        }
+        pool.withResource(expectNoAsync());
+
+        async.elapse(const Duration(seconds: 1));
+      });
+    });
+
+    test('a blocked resource is allocated when another is released', () {
+      FakeAsync().run((async) {
+        var pool = Pool(50);
+        for (var i = 0; i < 49; i++) {
+          pool.withResource(expectAsync0(() => Completer<void>().future));
+        }
+
+        var completer = Completer<void>();
+        pool.withResource(() => completer.future);
+        var blockedResourceAllocated = false;
+        pool.withResource(() {
+          blockedResourceAllocated = true;
+        });
+
+        Future<void>.delayed(const Duration(microseconds: 1)).then((_) {
+          expect(blockedResourceAllocated, isFalse);
+          completer.complete();
+          return Future<void>.delayed(const Duration(microseconds: 1));
+        }).then((_) {
+          expect(blockedResourceAllocated, isTrue);
+        });
+
+        async.elapse(const Duration(seconds: 1));
+      });
+    });
+
+    // Regression test for #3.
+    test('can be called immediately before close()', () async {
+      var pool = Pool(1);
+      unawaited(pool.withResource(expectAsync0(() {})));
+      await pool.close();
+    });
+  });
+
+  group('with a timeout', () {
+    test("doesn't time out if there are no pending requests", () {
+      FakeAsync().run((async) {
+        var pool = Pool(50, timeout: const Duration(seconds: 5));
+        for (var i = 0; i < 50; i++) {
+          expect(pool.request(), completes);
+        }
+
+        async.elapse(const Duration(seconds: 6));
+      });
+    });
+
+    test('resets the timer if a resource is returned', () {
+      FakeAsync().run((async) {
+        var pool = Pool(50, timeout: const Duration(seconds: 5));
+        for (var i = 0; i < 49; i++) {
+          expect(pool.request(), completes);
+        }
+
+        pool.request().then((lastAllocatedResource) {
+          // This will only complete once [lastAllocatedResource] is released.
+          expect(pool.request(), completes);
+
+          Future<void>.delayed(const Duration(seconds: 3)).then((_) {
+            lastAllocatedResource.release();
+            expect(pool.request(), doesNotComplete);
+          });
+        });
+
+        async.elapse(const Duration(seconds: 6));
+      });
+    });
+
+    test('resets the timer if a resource is requested', () {
+      FakeAsync().run((async) {
+        var pool = Pool(50, timeout: const Duration(seconds: 5));
+        for (var i = 0; i < 50; i++) {
+          expect(pool.request(), completes);
+        }
+        expect(pool.request(), doesNotComplete);
+
+        Future<void>.delayed(const Duration(seconds: 3)).then((_) {
+          expect(pool.request(), doesNotComplete);
+        });
+
+        async.elapse(const Duration(seconds: 6));
+      });
+    });
+
+    test('times out if nothing happens', () {
+      FakeAsync().run((async) {
+        var pool = Pool(50, timeout: const Duration(seconds: 5));
+        for (var i = 0; i < 50; i++) {
+          expect(pool.request(), completes);
+        }
+        expect(pool.request(), throwsA(const TypeMatcher<TimeoutException>()));
+
+        async.elapse(const Duration(seconds: 6));
+      });
+    });
+  });
+
+  group('allowRelease()', () {
+    test('runs the callback once the resource limit is exceeded', () async {
+      var pool = Pool(50);
+      for (var i = 0; i < 49; i++) {
+        expect(pool.request(), completes);
+      }
+
+      var resource = await pool.request();
+      var onReleaseCalled = false;
+      resource.allowRelease(() => onReleaseCalled = true);
+      await Future<void>.delayed(Duration.zero);
+      expect(onReleaseCalled, isFalse);
+
+      expect(pool.request(), completes);
+      await Future<void>.delayed(Duration.zero);
+      expect(onReleaseCalled, isTrue);
+    });
+
+    test('runs the callback immediately if there are blocked requests',
+        () async {
+      var pool = Pool(1);
+      var resource = await pool.request();
+
+      // This will be blocked until [resource.allowRelease] is called.
+      expect(pool.request(), completes);
+
+      var onReleaseCalled = false;
+      resource.allowRelease(() => onReleaseCalled = true);
+      await Future<void>.delayed(Duration.zero);
+      expect(onReleaseCalled, isTrue);
+    });
+
+    test('blocks the request until the callback completes', () async {
+      var pool = Pool(1);
+      var resource = await pool.request();
+
+      var requestComplete = false;
+      unawaited(pool.request().then((_) => requestComplete = true));
+
+      var completer = Completer<void>();
+      resource.allowRelease(() => completer.future);
+      await Future<void>.delayed(Duration.zero);
+      expect(requestComplete, isFalse);
+
+      completer.complete();
+      await Future<void>.delayed(Duration.zero);
+      expect(requestComplete, isTrue);
+    });
+
+    test('completes requests in request order regardless of callback order',
+        () async {
+      var pool = Pool(2);
+      var resource1 = await pool.request();
+      var resource2 = await pool.request();
+
+      var request1Complete = false;
+      unawaited(pool.request().then((_) => request1Complete = true));
+      var request2Complete = false;
+      unawaited(pool.request().then((_) => request2Complete = true));
+
+      var onRelease1Called = false;
+      var completer1 = Completer<void>();
+      resource1.allowRelease(() {
+        onRelease1Called = true;
+        return completer1.future;
+      });
+      await Future<void>.delayed(Duration.zero);
+      expect(onRelease1Called, isTrue);
+
+      var onRelease2Called = false;
+      var completer2 = Completer<void>();
+      resource2.allowRelease(() {
+        onRelease2Called = true;
+        return completer2.future;
+      });
+      await Future<void>.delayed(Duration.zero);
+      expect(onRelease2Called, isTrue);
+      expect(request1Complete, isFalse);
+      expect(request2Complete, isFalse);
+
+      // Complete the second resource's onRelease callback first. Even though it
+      // was triggered by the second blocking request, it should complete the
+      // first one to preserve ordering.
+      completer2.complete();
+      await Future<void>.delayed(Duration.zero);
+      expect(request1Complete, isTrue);
+      expect(request2Complete, isFalse);
+
+      completer1.complete();
+      await Future<void>.delayed(Duration.zero);
+      expect(request1Complete, isTrue);
+      expect(request2Complete, isTrue);
+    });
+
+    test('runs onRequest in the zone it was created', () async {
+      var pool = Pool(1);
+      var resource = await pool.request();
+
+      var outerZone = Zone.current;
+      runZoned(() {
+        var innerZone = Zone.current;
+        expect(innerZone, isNot(equals(outerZone)));
+
+        resource.allowRelease(expectAsync0(() {
+          expect(Zone.current, equals(innerZone));
+        }));
+      });
+
+      await pool.request();
+    });
+  });
+
+  test("done doesn't complete without close", () async {
+    var pool = Pool(1);
+    unawaited(pool.done.then(expectAsync1((_) {}, count: 0)));
+
+    var resource = await pool.request();
+    resource.release();
+
+    await Future<void>.delayed(Duration.zero);
+  });
+
+  group('close()', () {
+    test('disallows request() and withResource()', () {
+      var pool = Pool(1)..close();
+      expect(pool.request, throwsStateError);
+      expect(() => pool.withResource(() {}), throwsStateError);
+    });
+
+    test('pending requests are fulfilled', () async {
+      var pool = Pool(1);
+      var resource1 = await pool.request();
+      expect(
+          pool.request().then((resource2) {
+            resource2.release();
+          }),
+          completes);
+      expect(pool.done, completes);
+      expect(pool.close(), completes);
+      resource1.release();
+    });
+
+    test('pending requests are fulfilled with allowRelease', () async {
+      var pool = Pool(1);
+      var resource1 = await pool.request();
+
+      var completer = Completer<void>();
+      expect(
+          pool.request().then((resource2) {
+            expect(completer.isCompleted, isTrue);
+            resource2.release();
+          }),
+          completes);
+      expect(pool.close(), completes);
+
+      resource1.allowRelease(() => completer.future);
+      await Future<void>.delayed(Duration.zero);
+
+      completer.complete();
+    });
+
+    test("doesn't complete until all resources are released", () async {
+      var pool = Pool(2);
+      var resource1 = await pool.request();
+      var resource2 = await pool.request();
+      var resource3Future = pool.request();
+
+      var resource1Released = false;
+      var resource2Released = false;
+      var resource3Released = false;
+      expect(
+          pool.close().then((_) {
+            expect(resource1Released, isTrue);
+            expect(resource2Released, isTrue);
+            expect(resource3Released, isTrue);
+          }),
+          completes);
+
+      resource1Released = true;
+      resource1.release();
+      await Future<void>.delayed(Duration.zero);
+
+      resource2Released = true;
+      resource2.release();
+      await Future<void>.delayed(Duration.zero);
+
+      var resource3 = await resource3Future;
+      resource3Released = true;
+      resource3.release();
+    });
+
+    test('active onReleases complete as usual', () async {
+      var pool = Pool(1);
+      var resource = await pool.request();
+
+      // Set up an onRelease callback whose completion is controlled by
+      // [completer].
+      var completer = Completer<void>();
+      resource.allowRelease(() => completer.future);
+      expect(
+          pool.request().then((_) {
+            expect(completer.isCompleted, isTrue);
+          }),
+          completes);
+
+      await Future<void>.delayed(Duration.zero);
+      unawaited(pool.close());
+
+      await Future<void>.delayed(Duration.zero);
+      completer.complete();
+    });
+
+    test('inactive onReleases fire', () async {
+      var pool = Pool(2);
+      var resource1 = await pool.request();
+      var resource2 = await pool.request();
+
+      var completer1 = Completer<void>();
+      resource1.allowRelease(() => completer1.future);
+      var completer2 = Completer<void>();
+      resource2.allowRelease(() => completer2.future);
+
+      expect(
+          pool.close().then((_) {
+            expect(completer1.isCompleted, isTrue);
+            expect(completer2.isCompleted, isTrue);
+          }),
+          completes);
+
+      await Future<void>.delayed(Duration.zero);
+      completer1.complete();
+
+      await Future<void>.delayed(Duration.zero);
+      completer2.complete();
+    });
+
+    test('new allowReleases fire immediately', () async {
+      var pool = Pool(1);
+      var resource = await pool.request();
+
+      var completer = Completer<void>();
+      expect(
+          pool.close().then((_) {
+            expect(completer.isCompleted, isTrue);
+          }),
+          completes);
+
+      await Future<void>.delayed(Duration.zero);
+      resource.allowRelease(() => completer.future);
+
+      await Future<void>.delayed(Duration.zero);
+      completer.complete();
+    });
+
+    test('an onRelease error is piped to the return value', () async {
+      var pool = Pool(1);
+      var resource = await pool.request();
+
+      var completer = Completer<void>();
+      resource.allowRelease(() => completer.future);
+
+      expect(pool.done, throwsA('oh no!'));
+      expect(pool.close(), throwsA('oh no!'));
+
+      await Future<void>.delayed(Duration.zero);
+      completer.completeError('oh no!');
+    });
+  });
+
+  group('forEach', () {
+    late Pool pool;
+
+    tearDown(() async {
+      await pool.close();
+    });
+
+    const delayedToStringDuration = Duration(milliseconds: 10);
+
+    Future<String> delayedToString(int i) =>
+        Future<String>.delayed(delayedToStringDuration, () => i.toString());
+
+    for (var itemCount in [0, 5]) {
+      for (var poolSize in [1, 5, 6]) {
+        test('poolSize: $poolSize, itemCount: $itemCount', () async {
+          pool = Pool(poolSize);
+
+          var finishedItems = 0;
+
+          await for (var item in pool.forEach(
+              Iterable.generate(itemCount, (i) {
+                expect(i, lessThanOrEqualTo(finishedItems + poolSize),
+                    reason: 'the iterator should be called lazily');
+                return i;
+              }),
+              delayedToString)) {
+            expect(int.parse(item), lessThan(itemCount));
+            finishedItems++;
+          }
+
+          expect(finishedItems, itemCount);
+        });
+      }
+    }
+
+    test('pool closed before listen', () async {
+      pool = Pool(2);
+
+      var stream = pool.forEach(Iterable<int>.generate(5), delayedToString);
+
+      await pool.close();
+
+      expect(stream.toList(), throwsStateError);
+    });
+
+    test('completes even if the pool is partially used', () async {
+      pool = Pool(2);
+
+      var resource = await pool.request();
+
+      var stream = pool.forEach(<int>[], delayedToString);
+
+      expect(await stream.length, 0);
+
+      resource.release();
+    });
+
+    test('stream paused longer than timeout', () async {
+      pool = Pool(2, timeout: delayedToStringDuration);
+
+      var resource = await pool.request();
+
+      var stream = pool.forEach<int, int>(
+          Iterable.generate(100, (i) {
+            expect(i, lessThan(20),
+                reason: 'The timeout should happen '
+                    'before the entire iterable is iterated.');
+            return i;
+          }), (i) async {
+        await Future<void>.delayed(Duration(milliseconds: i));
+        return i;
+      });
+
+      await expectLater(
+          stream.toList,
+          throwsA(const TypeMatcher<TimeoutException>().having(
+              (te) => te.message,
+              'message',
+              contains('Pool deadlock: '
+                  'all resources have been allocated for too long.'))));
+
+      resource.release();
+    });
+
+    group('timing and timeout', () {
+      for (var poolSize in [2, 8, 64]) {
+        for (var otherTaskCount
+            in [0, 1, 7, 63].where((otc) => otc < poolSize)) {
+          test('poolSize: $poolSize, otherTaskCount: $otherTaskCount',
+              () async {
+            final itemCount = 128;
+            pool = Pool(poolSize, timeout: const Duration(milliseconds: 20));
+
+            var otherTasks = await Future.wait(
+                Iterable<int>.generate(otherTaskCount)
+                    .map((i) => pool.request()));
+
+            try {
+              var finishedItems = 0;
+
+              var watch = Stopwatch()..start();
+
+              await for (var item in pool.forEach(
+                  Iterable.generate(itemCount, (i) {
+                    expect(i, lessThanOrEqualTo(finishedItems + poolSize),
+                        reason: 'the iterator should be called lazily');
+                    return i;
+                  }),
+                  delayedToString)) {
+                expect(int.parse(item), lessThan(itemCount));
+                finishedItems++;
+              }
+
+              expect(finishedItems, itemCount);
+
+              final expectedElapsed =
+                  delayedToStringDuration.inMicroseconds * 4;
+
+              expect((watch.elapsed ~/ itemCount).inMicroseconds,
+                  lessThan(expectedElapsed / (poolSize - otherTaskCount)),
+                  reason: 'Average time per task should be '
+                      'proportionate to the available pool resources.');
+            } finally {
+              for (var task in otherTasks) {
+                task.release();
+              }
+            }
+          });
+        }
+      }
+    }, testOn: 'vm');
+
+    test('partial iteration', () async {
+      pool = Pool(5);
+      var stream = pool.forEach(Iterable<int>.generate(100), delayedToString);
+      expect(await stream.take(10).toList(), hasLength(10));
+    });
+
+    test('pool close during data with waiting to be done', () async {
+      pool = Pool(5);
+
+      var stream = pool.forEach(Iterable<int>.generate(100), delayedToString);
+
+      var dataCount = 0;
+      var subscription = stream.listen((data) {
+        dataCount++;
+        pool.close();
+      });
+
+      await subscription.asFuture<void>();
+      expect(dataCount, 100);
+      await subscription.cancel();
+    });
+
+    test('pause and resume ', () async {
+      var generatedCount = 0;
+      var dataCount = 0;
+      final poolSize = 5;
+
+      pool = Pool(poolSize);
+
+      var stream = pool.forEach(
+          Iterable<int>.generate(40, (i) {
+            expect(generatedCount, lessThanOrEqualTo(dataCount + 2 * poolSize),
+                reason: 'The iterator should not be called '
+                    'much faster than the data is consumed.');
+            generatedCount++;
+            return i;
+          }),
+          delayedToString);
+
+      // ignore: cancel_subscriptions
+      late StreamSubscription subscription;
+
+      subscription = stream.listen(
+        (data) {
+          dataCount++;
+
+          if (int.parse(data) % 3 == 1) {
+            subscription.pause(Future(() async {
+              await Future<void>.delayed(const Duration(milliseconds: 100));
+            }));
+          }
+        },
+        onError: registerException,
+        onDone: expectAsync0(() {
+          expect(dataCount, 40);
+        }),
+      );
+    });
+
+    group('cancel', () {
+      final dataSize = 32;
+      for (var i = 1; i < 5; i++) {
+        test('with pool size $i', () async {
+          pool = Pool(i);
+
+          var stream =
+              pool.forEach(Iterable<int>.generate(dataSize), delayedToString);
+
+          var cancelCompleter = Completer<void>();
+
+          StreamSubscription subscription;
+
+          var eventCount = 0;
+          subscription = stream.listen((data) {
+            eventCount++;
+            if (int.parse(data) == dataSize ~/ 2) {
+              cancelCompleter.complete();
+            }
+          }, onError: registerException);
+
+          await cancelCompleter.future;
+
+          await subscription.cancel();
+
+          expect(eventCount, 1 + dataSize ~/ 2);
+        });
+      }
+    });
+
+    group('errors', () {
+      Future<void> errorInIterator({
+        bool Function(int item, Object error, StackTrace stack)? onError,
+      }) async {
+        pool = Pool(20);
+
+        var listFuture = pool
+            .forEach(
+                Iterable.generate(100, (i) {
+                  if (i == 50) {
+                    throw StateError('error while generating item in iterator');
+                  }
+
+                  return i;
+                }),
+                delayedToString,
+                onError: onError)
+            .toList();
+
+        await expectLater(() async => listFuture, throwsStateError);
+      }
+
+      test('iteration, no onError', () async {
+        await errorInIterator();
+      });
+      test('iteration, with onError', () async {
+        await errorInIterator(onError: (i, e, s) => false);
+      });
+
+      test('error in action, no onError', () async {
+        pool = Pool(20);
+
+        var listFuture = pool.forEach(Iterable<int>.generate(100), (i) async {
+          await Future<void>.delayed(const Duration(milliseconds: 10));
+          if (i == 10) {
+            throw UnsupportedError('10 is not supported');
+          }
+          return i.toString();
+        }).toList();
+
+        await expectLater(() async => listFuture, throwsUnsupportedError);
+      });
+
+      test('error in action, no onError', () async {
+        pool = Pool(20);
+
+        var list = await pool.forEach(Iterable<int>.generate(100),
+            (int i) async {
+          await Future<void>.delayed(const Duration(milliseconds: 10));
+          if (i % 10 == 0) {
+            throw UnsupportedError('Multiples of 10 not supported');
+          }
+          return i.toString();
+        },
+            onError: (item, error, stack) =>
+                error is! UnsupportedError).toList();
+
+        expect(list, hasLength(90));
+      });
+    });
+  });
+
+  test('throw error when pool limit <= 0', () {
+    expect(() => Pool(-1), throwsArgumentError);
+    expect(() => Pool(0), throwsArgumentError);
+  });
+}
+
+/// Returns a function that will cause the test to fail if it's called.
+///
+/// This should only be called within a [FakeAsync.run] zone.
+void Function() expectNoAsync() {
+  var stack = Trace.current(1);
+  return () => registerException(
+      TestFailure('Expected function not to be called.'), stack);
+}
+
+/// A matcher for Futures that asserts that they don't complete.
+///
+/// This should only be called within a [FakeAsync.run] zone.
+Matcher get doesNotComplete => predicate((Future future) {
+      var stack = Trace.current(1);
+      future.then((_) => registerException(
+          TestFailure('Expected future not to complete.'), stack));
+      return true;
+    });
diff --git a/pkgs/pub_semver/.gitignore b/pkgs/pub_semver/.gitignore
new file mode 100644
index 0000000..49ce72d
--- /dev/null
+++ b/pkgs/pub_semver/.gitignore
@@ -0,0 +1,3 @@
+.dart_tool/
+.packages
+pubspec.lock
diff --git a/pkgs/pub_semver/CHANGELOG.md b/pkgs/pub_semver/CHANGELOG.md
new file mode 100644
index 0000000..a31fbb2
--- /dev/null
+++ b/pkgs/pub_semver/CHANGELOG.md
@@ -0,0 +1,177 @@
+## 2.1.5
+
+- Require Dart `3.4.0`.
+- Move to `dart-lang/tools` monorepo.
+
+## 2.1.4
+
+- Added topics to `pubspec.yaml`.
+
+## 2.1.3
+
+- Add type parameters to the signatures of the `Version.preRelease` and
+  `Version.build` fields (`List` ==> `List<Object>`).
+  [#74](https://github.com/dart-lang/pub_semver/pull/74).
+- Require Dart 2.17.
+
+## 2.1.2
+
+- Add markdown badges to the readme.
+
+## 2.1.1
+
+- Fixed the version parsing pattern to only accept dots between version
+  components.
+
+## 2.1.0
+
+- Added `Version.canonicalizedVersion` to help scrub leading zeros and highlight
+  that `Version.toString()` preserves leading zeros.
+- Annotated `Version` with `@sealed` to discourage users from implementing the
+  interface.
+
+## 2.0.0
+
+- Stable null safety release.
+- `Version.primary` now throws `StateError` if the `versions` argument is empty.
+
+## 1.4.4
+
+- Fix a bug of `VersionRange.union` where ranges bounded at infinity would get
+  combined wrongly.
+
+# 1.4.3
+
+- Update Dart SDK constraint to `>=2.0.0 <3.0.0`.
+- Update `package:collection` constraint to `^1.0.0`.
+
+## 1.4.2
+
+* Set max SDK version to `<3.0.0`.
+
+## 1.4.1
+
+* Fix a bug where there upper bound of a version range with a build identifier
+  could accidentally be rewritten.
+
+## 1.4.0
+
+* Add a `Version.firstPreRelease` getter that returns the first possible
+  pre-release of a version.
+
+* Add a `Version.isFirstPreRelease` getter that returns whether a version is the
+  first possible pre-release.
+
+* `new VersionRange()` with an exclusive maximum now replaces the maximum with
+  its first pre-release version. This matches the existing semantics, where an
+  exclusive maximum would exclude pre-release versions of that maximum.
+
+  Explicitly representing this by changing the maximum version ensures that all
+  operations behave correctly with respect to the special pre-release semantics.
+  In particular, it fixes bugs where, for example,
+  `(>=1.0.0 <2.0.0-dev).union(>=2.0.0-dev <2.0.0)` and
+  `(>=1.0.0 <3.0.0).difference(^1.0.0)` wouldn't include `2.0.0-dev`.
+
+* Add an `alwaysIncludeMaxPreRelease` parameter to `new VersionRange()`, which
+  disables the replacement described above and allows users to create ranges
+  that do include the pre-release versions of an exclusive max version.
+
+## 1.3.7
+
+* Fix more bugs with `VersionRange.intersect()`, `VersionRange.difference()`,
+  and `VersionRange.union()` involving version ranges with pre-release maximums.
+
+## 1.3.6
+
+* Fix a bug where constraints that only allowed pre-release versions would be
+  parsed as empty constraints.
+
+## 1.3.5
+
+* Fix a bug where `VersionRange.intersect()` would return incorrect results for
+  pre-release versions with the same base version number as release versions.
+
+## 1.3.4
+
+* Fix a bug where `VersionRange.allowsAll()`, `VersionRange.allowsAny()`, and
+  `VersionRange.difference()` would return incorrect results for pre-release
+  versions with the same base version number as release versions.
+
+## 1.3.3
+
+* Fix a bug where `VersionRange.difference()` with a union constraint that
+  covered the entire range would crash.
+
+## 1.3.2
+
+* Fix a checked-mode error in `VersionRange.difference()`.
+
+## 1.3.1
+
+* Fix a new strong mode error.
+
+## 1.3.0
+
+* Make the `VersionUnion` class public. This was previously used internally to
+  implement `new VersionConstraint.unionOf()` and `VersionConstraint.union()`.
+  Now it's public so you can use it too.
+
+* Added `VersionConstraint.difference()`. This returns a constraint matching all
+  versions matched by one constraint but not another.
+
+* Make `VersionRange` implement `Comparable<VersionRange>`. Ranges are ordered
+  first by lower bound, then by upper bound.
+
+## 1.2.4
+
+* Fix all remaining strong mode warnings.
+
+## 1.2.3
+
+* Addressed three strong mode warnings.
+
+## 1.2.2
+
+* Make the package analyze under strong mode and compile with the DDC (Dart Dev
+  Compiler). Fix two issues with a private subclass of `VersionConstraint`
+  having different types for overridden methods.
+
+## 1.2.1
+
+* Allow version ranges like `>=1.2.3-dev.1 <1.2.3` to match pre-release versions
+  of `1.2.3`. Previously, these didn't match, since the pre-release versions had
+  the same major, minor, and patch numbers as the max; now an exception has been
+  added if they also have the same major, minor, and patch numbers as the min
+  *and* the min is also a pre-release version.
+
+## 1.2.0
+
+* Add a `VersionConstraint.union()` method and a `new
+  VersionConstraint.unionOf()` constructor. These each return a constraint that
+  matches multiple existing constraints.
+
+* Add a `VersionConstraint.allowsAll()` method, which returns whether one
+  constraint is a superset of another.
+
+* Add a `VersionConstraint.allowsAny()` method, which returns whether one
+  constraint overlaps another.
+
+* `Version` now implements `VersionRange`.
+
+## 1.1.0
+
+* Add support for the `^` operator for compatible versions according to pub's
+  notion of compatibility. `^1.2.3` is equivalent to `>=1.2.3 <2.0.0`; `^0.1.2`
+  is equivalent to `>=0.1.2 <0.2.0`.
+
+* Add `Version.nextBreaking`, which returns the next version that introduces
+  breaking changes after a given version.
+
+* Add `new VersionConstraint.compatibleWith()`, which returns a range covering
+  all versions compatible with a given version.
+
+* Add a custom `VersionRange.hashCode` to make it properly hashable.
+
+## 1.0.0
+
+* Initial release.
diff --git a/pkgs/pub_semver/LICENSE b/pkgs/pub_semver/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/pub_semver/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/pub_semver/README.md b/pkgs/pub_semver/README.md
new file mode 100644
index 0000000..03c92a3
--- /dev/null
+++ b/pkgs/pub_semver/README.md
@@ -0,0 +1,107 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/pub_semver.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/pub_semver.yaml)
+[![pub package](https://img.shields.io/pub/v/pub_semver.svg)](https://pub.dev/packages/pub_semver)
+[![package publisher](https://img.shields.io/pub/publisher/pub_semver.svg)](https://pub.dev/packages/pub_semver/publisher)
+
+Handles version numbers and version constraints in the same way that [pub][]
+does.
+
+## Semantics
+
+The semantics here very closely follow the
+[Semantic Versioning spec version 2.0.0-rc.1][semver]. It differs from semver
+in a few corner cases:
+
+ *  **Version ordering does take build suffixes into account.** This is unlike
+    semver 2.0.0 but like earlier versions of semver. Version `1.2.3+1` is
+    considered a lower number than `1.2.3+2`.
+
+    Since a package may have published multiple versions that differ only by
+    build suffix, pub still has to pick one of them *somehow*. Semver leaves
+    that issue unresolved, so we just say that build numbers are sorted like
+    pre-release suffixes.
+
+ *  **Pre-release versions are excluded from most max ranges.** Let's say a
+    user is depending on "foo" with constraint `>=1.0.0 <2.0.0` and that "foo"
+    has published these versions:
+
+     *  `1.0.0`
+     *  `1.1.0`
+     *  `1.2.0`
+     *  `2.0.0-alpha`
+     *  `2.0.0-beta`
+     *  `2.0.0`
+     *  `2.1.0`
+
+    Versions `2.0.0` and `2.1.0` are excluded by the constraint since neither
+    matches `<2.0.0`. However, since semver specifies that pre-release versions
+    are lower than the non-prerelease version (i.e. `2.0.0-beta < 2.0.0`, then
+    the `<2.0.0` constraint does technically allow those.
+
+    But that's almost never what the user wants. If their package doesn't work
+    with foo `2.0.0`, it's certainly not likely to work with experimental,
+    unstable versions of `2.0.0`'s API, which is what pre-release versions
+    represent.
+
+    To handle that, `<` version ranges don't allow pre-release versions of the
+    maximum unless the max is itself a pre-release, or the min is a pre-release
+    of the same version. In other words, a `<2.0.0` constraint will prohibit not
+    just `2.0.0` but any pre-release of `2.0.0`. However, `<2.0.0-beta` will
+    exclude `2.0.0-beta` but allow `2.0.0-alpha`. Likewise, `>2.0.0-alpha
+    <2.0.0` will exclude `2.0.0-alpha` but allow `2.0.0-beta`.
+
+ *  **Pre-release versions are avoided when possible.** The above case
+    handles pre-release versions at the top of the range, but what about in
+    the middle? What if "foo" has these versions:
+
+     *  `1.0.0`
+     *  `1.2.0-alpha`
+     *  `1.2.0`
+     *  `1.3.0-experimental`
+
+    When a number of versions are valid, pub chooses the best one where "best"
+    usually means "highest numbered". That follows the user's intuition that,
+    all else being equal, they want the latest and greatest. Here, that would
+    mean `1.3.0-experimental`. However, most users don't want to use unstable
+    versions of their dependencies.
+
+    We want pre-releases to be explicitly opt-in so that package consumers
+    don't get unpleasant surprises and so that package maintainers are free to
+    put out pre-releases and get feedback without dragging all of their users
+    onto the bleeding edge.
+
+    To accommodate that, when pub is choosing a version, it uses *priority*
+    order which is different from strict comparison ordering. Any stable
+    version is considered higher priority than any unstable version. The above
+    versions, in priority order, are:
+
+     *  `1.2.0-alpha`
+     *  `1.3.0-experimental`
+     *  `1.0.0`
+     *  `1.2.0`
+
+    This ensures that users only end up with an unstable version when there are
+    no alternatives. Usually this means they've picked a constraint that
+    specifically selects that unstable version -- they've deliberately opted
+    into it.
+
+ *  **There is a notion of compatibility between pre-1.0.0 versions.** Semver
+    deems all pre-1.0.0 versions to be incompatible.  This means that the only
+    way to ensure compatibility when depending on a pre-1.0.0 package is to
+    pin the dependency to an exact version. Pinned version constraints prevent
+    automatic patch and pre-release updates. To avoid this situation, pub
+    defines the "next breaking" version as the version which increments the
+    major version if it's greater than zero, and the minor version otherwise,
+    resets subsequent digits to zero, and strips any pre-release or build
+    suffix.  For example, here are some versions along with their next breaking
+    ones:
+
+    `0.0.3` -> `0.1.0`
+    `0.7.2-alpha` -> `0.8.0`
+    `1.2.3` -> `2.0.0`
+
+    To make use of this, pub defines a "^" operator which yields a version
+    constraint greater than or equal to a given version, but less than its next
+    breaking one.
+
+[pub]: https://pub.dev
+[semver]: https://semver.org/spec/v2.0.0-rc.1.html
diff --git a/pkgs/pub_semver/analysis_options.yaml b/pkgs/pub_semver/analysis_options.yaml
new file mode 100644
index 0000000..76380a0
--- /dev/null
+++ b/pkgs/pub_semver/analysis_options.yaml
@@ -0,0 +1,31 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+    strict-inference: true
+    strict-raw-types: true
+
+linter:
+  rules:
+    - avoid_bool_literals_in_conditional_expressions
+    - avoid_classes_with_only_static_members
+    - avoid_private_typedef_functions
+    - avoid_redundant_argument_values
+    - avoid_returning_this
+    - avoid_unused_constructor_parameters
+    - avoid_void_async
+    - cancel_subscriptions
+    - cascade_invocations
+    - join_return_with_assignment
+    - literal_only_boolean_expressions
+    - missing_whitespace_between_adjacent_strings
+    - no_adjacent_strings_in_list
+    - no_runtimeType_toString
+    - prefer_const_declarations
+    - prefer_expression_function_bodies
+    - unnecessary_await_in_return
+    - use_if_null_to_convert_nulls_to_bools
+    - use_raw_strings
+    - use_string_buffers
diff --git a/pkgs/pub_semver/example/example.dart b/pkgs/pub_semver/example/example.dart
new file mode 100644
index 0000000..890343c
--- /dev/null
+++ b/pkgs/pub_semver/example/example.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2020, 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:pub_semver/pub_semver.dart';
+
+void main() {
+  final range = VersionConstraint.parse('^2.0.0');
+
+  for (var version in [
+    Version.parse('1.2.3-pre'),
+    Version.parse('2.0.0+123'),
+    Version.parse('3.0.0-dev'),
+  ]) {
+    print('$version ${version.isPreRelease} ${range.allows(version)}');
+  }
+}
diff --git a/pkgs/pub_semver/lib/pub_semver.dart b/pkgs/pub_semver/lib/pub_semver.dart
new file mode 100644
index 0000000..4b6487c
--- /dev/null
+++ b/pkgs/pub_semver/lib/pub_semver.dart
@@ -0,0 +1,8 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/version.dart';
+export 'src/version_constraint.dart';
+export 'src/version_range.dart' hide CompatibleWithVersionRange;
+export 'src/version_union.dart';
diff --git a/pkgs/pub_semver/lib/src/patterns.dart b/pkgs/pub_semver/lib/src/patterns.dart
new file mode 100644
index 0000000..03119ac
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/patterns.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Regex that matches a version number at the beginning of a string.
+final startVersion = RegExp(r'^' // Start at beginning.
+    r'(\d+)\.(\d+)\.(\d+)' // Version number.
+    r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release.
+    r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'); // Build.
+
+/// Like [startVersion] but matches the entire string.
+final completeVersion = RegExp('${startVersion.pattern}\$');
+
+/// Parses a comparison operator ("<", ">", "<=", or ">=") at the beginning of
+/// a string.
+final startComparison = RegExp(r'^[<>]=?');
+
+/// The "compatible with" operator.
+const compatibleWithChar = '^';
diff --git a/pkgs/pub_semver/lib/src/utils.dart b/pkgs/pub_semver/lib/src/utils.dart
new file mode 100644
index 0000000..a9f714f
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/utils.dart
@@ -0,0 +1,58 @@
+// 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 'version.dart';
+import 'version_range.dart';
+
+/// Returns whether [range1] is immediately next to, but not overlapping,
+/// [range2].
+bool areAdjacent(VersionRange range1, VersionRange range2) {
+  if (range1.max != range2.min) return false;
+
+  return (range1.includeMax && !range2.includeMin) ||
+      (!range1.includeMax && range2.includeMin);
+}
+
+/// Returns whether [range1] allows lower versions than [range2].
+bool allowsLower(VersionRange range1, VersionRange range2) {
+  if (range1.min == null) return range2.min != null;
+  if (range2.min == null) return false;
+
+  var comparison = range1.min!.compareTo(range2.min!);
+  if (comparison == -1) return true;
+  if (comparison == 1) return false;
+  return range1.includeMin && !range2.includeMin;
+}
+
+/// Returns whether [range1] allows higher versions than [range2].
+bool allowsHigher(VersionRange range1, VersionRange range2) {
+  if (range1.max == null) return range2.max != null;
+  if (range2.max == null) return false;
+
+  var comparison = range1.max!.compareTo(range2.max!);
+  if (comparison == 1) return true;
+  if (comparison == -1) return false;
+  return range1.includeMax && !range2.includeMax;
+}
+
+/// Returns whether [range1] allows only versions lower than those allowed by
+/// [range2].
+bool strictlyLower(VersionRange range1, VersionRange range2) {
+  if (range1.max == null || range2.min == null) return false;
+
+  var comparison = range1.max!.compareTo(range2.min!);
+  if (comparison == -1) return true;
+  if (comparison == 1) return false;
+  return !range1.includeMax || !range2.includeMin;
+}
+
+/// Returns whether [range1] allows only versions higher than those allowed by
+/// [range2].
+bool strictlyHigher(VersionRange range1, VersionRange range2) =>
+    strictlyLower(range2, range1);
+
+bool equalsWithoutPreRelease(Version version1, Version version2) =>
+    version1.major == version2.major &&
+    version1.minor == version2.minor &&
+    version1.patch == version2.patch;
diff --git a/pkgs/pub_semver/lib/src/version.dart b/pkgs/pub_semver/lib/src/version.dart
new file mode 100644
index 0000000..90f3d53
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/version.dart
@@ -0,0 +1,391 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' as math;
+
+import 'package:collection/collection.dart';
+import 'package:meta/meta.dart' show sealed;
+
+import 'patterns.dart';
+import 'version_constraint.dart';
+import 'version_range.dart';
+
+/// The equality operator to use for comparing version components.
+const _equality = IterableEquality<Object>();
+
+/// A parsed semantic version number.
+@sealed
+class Version implements VersionConstraint, VersionRange {
+  /// No released version: i.e. "0.0.0".
+  static Version get none => Version(0, 0, 0);
+
+  /// Compares [a] and [b] to see which takes priority over the other.
+  ///
+  /// Returns `1` if [a] takes priority over [b] and `-1` if vice versa. If
+  /// [a] and [b] are equivalent, returns `0`.
+  ///
+  /// Unlike [compareTo], which *orders* versions, this determines which
+  /// version a user is likely to prefer. In particular, it prioritizes
+  /// pre-release versions lower than stable versions, regardless of their
+  /// version numbers. Pub uses this when determining which version to prefer
+  /// when a number of versions are allowed. In that case, it will always
+  /// choose a stable version when possible.
+  ///
+  /// When used to sort a list, orders in ascending priority so that the
+  /// highest priority version is *last* in the result.
+  static int prioritize(Version a, Version b) {
+    // Sort all prerelease versions after all normal versions. This way
+    // the solver will prefer stable packages over unstable ones.
+    if (a.isPreRelease && !b.isPreRelease) return -1;
+    if (!a.isPreRelease && b.isPreRelease) return 1;
+
+    return a.compareTo(b);
+  }
+
+  /// Like [prioritize], but lower version numbers are considered greater than
+  /// higher version numbers.
+  ///
+  /// This still considers prerelease versions to be lower than non-prerelease
+  /// versions. Pub uses this when downgrading -- it chooses the lowest version
+  /// but still excludes pre-release versions when possible.
+  static int antiprioritize(Version a, Version b) {
+    if (a.isPreRelease && !b.isPreRelease) return -1;
+    if (!a.isPreRelease && b.isPreRelease) return 1;
+
+    return b.compareTo(a);
+  }
+
+  /// The major version number: "1" in "1.2.3".
+  final int major;
+
+  /// The minor version number: "2" in "1.2.3".
+  final int minor;
+
+  /// The patch version number: "3" in "1.2.3".
+  final int patch;
+
+  /// The pre-release identifier: "foo" in "1.2.3-foo".
+  ///
+  /// This is split into a list of components, each of which may be either a
+  /// string or a non-negative integer. It may also be empty, indicating that
+  /// this version has no pre-release identifier.
+  final List<Object> preRelease;
+
+  /// The build identifier: "foo" in "1.2.3+foo".
+  ///
+  /// This is split into a list of components, each of which may be either a
+  /// string or a non-negative integer. It may also be empty, indicating that
+  /// this version has no build identifier.
+  final List<Object> build;
+
+  /// The original string representation of the version number.
+  ///
+  /// This preserves textual artifacts like leading zeros that may be left out
+  /// of the parsed version.
+  final String _text;
+
+  @override
+  Version get min => this;
+  @override
+  Version get max => this;
+  @override
+  bool get includeMin => true;
+  @override
+  bool get includeMax => true;
+
+  Version._(this.major, this.minor, this.patch, String? preRelease,
+      String? build, this._text)
+      : preRelease = preRelease == null ? <Object>[] : _splitParts(preRelease),
+        build = build == null ? [] : _splitParts(build) {
+    if (major < 0) throw ArgumentError('Major version must be non-negative.');
+    if (minor < 0) throw ArgumentError('Minor version must be non-negative.');
+    if (patch < 0) throw ArgumentError('Patch version must be non-negative.');
+  }
+
+  /// Creates a new [Version] object.
+  factory Version(int major, int minor, int patch,
+      {String? pre, String? build}) {
+    var text = '$major.$minor.$patch';
+    if (pre != null) text += '-$pre';
+    if (build != null) text += '+$build';
+
+    return Version._(major, minor, patch, pre, build, text);
+  }
+
+  /// Creates a new [Version] by parsing [text].
+  factory Version.parse(String text) {
+    final match = completeVersion.firstMatch(text);
+    if (match == null) {
+      throw FormatException('Could not parse "$text".');
+    }
+
+    try {
+      var major = int.parse(match[1]!);
+      var minor = int.parse(match[2]!);
+      var patch = int.parse(match[3]!);
+
+      var preRelease = match[5];
+      var build = match[8];
+
+      return Version._(major, minor, patch, preRelease, build, text);
+    } on FormatException {
+      throw FormatException('Could not parse "$text".');
+    }
+  }
+
+  /// Returns the primary version out of [versions].
+  ///
+  /// This is the highest-numbered stable (non-prerelease) version. If there
+  /// are no stable versions, it's just the highest-numbered version.
+  ///
+  /// If [versions] is empty, throws a [StateError].
+  static Version primary(List<Version> versions) {
+    var primary = versions.first;
+    for (var version in versions.skip(1)) {
+      if ((!version.isPreRelease && primary.isPreRelease) ||
+          (version.isPreRelease == primary.isPreRelease && version > primary)) {
+        primary = version;
+      }
+    }
+    return primary;
+  }
+
+  /// Splits a string of dot-delimited identifiers into their component parts.
+  ///
+  /// Identifiers that are numeric are converted to numbers.
+  static List<Object> _splitParts(String text) => text
+      .split('.')
+      .map((part) =>
+          // Return an integer part if possible, otherwise return the string
+          // as-is
+          int.tryParse(part) ?? part)
+      .toList();
+
+  @override
+  bool operator ==(Object other) =>
+      other is Version &&
+      major == other.major &&
+      minor == other.minor &&
+      patch == other.patch &&
+      _equality.equals(preRelease, other.preRelease) &&
+      _equality.equals(build, other.build);
+
+  @override
+  int get hashCode =>
+      major ^
+      minor ^
+      patch ^
+      _equality.hash(preRelease) ^
+      _equality.hash(build);
+
+  bool operator <(Version other) => compareTo(other) < 0;
+  bool operator >(Version other) => compareTo(other) > 0;
+  bool operator <=(Version other) => compareTo(other) <= 0;
+  bool operator >=(Version other) => compareTo(other) >= 0;
+
+  @override
+  bool get isAny => false;
+  @override
+  bool get isEmpty => false;
+
+  /// Whether or not this is a pre-release version.
+  bool get isPreRelease => preRelease.isNotEmpty;
+
+  /// Gets the next major version number that follows this one.
+  ///
+  /// If this version is a pre-release of a major version release (i.e. the
+  /// minor and patch versions are zero), then it just strips the pre-release
+  /// suffix. Otherwise, it increments the major version and resets the minor
+  /// and patch.
+  Version get nextMajor {
+    if (isPreRelease && minor == 0 && patch == 0) {
+      return Version(major, minor, patch);
+    }
+
+    return _incrementMajor();
+  }
+
+  /// Gets the next minor version number that follows this one.
+  ///
+  /// If this version is a pre-release of a minor version release (i.e. the
+  /// patch version is zero), then it just strips the pre-release suffix.
+  /// Otherwise, it increments the minor version and resets the patch.
+  Version get nextMinor {
+    if (isPreRelease && patch == 0) {
+      return Version(major, minor, patch);
+    }
+
+    return _incrementMinor();
+  }
+
+  /// Gets the next patch version number that follows this one.
+  ///
+  /// If this version is a pre-release, then it just strips the pre-release
+  /// suffix. Otherwise, it increments the patch version.
+  Version get nextPatch {
+    if (isPreRelease) {
+      return Version(major, minor, patch);
+    }
+
+    return _incrementPatch();
+  }
+
+  /// Gets the next breaking version number that follows this one.
+  ///
+  /// Increments [major] if it's greater than zero, otherwise [minor], resets
+  /// subsequent digits to zero, and strips any [preRelease] or [build]
+  /// suffix.
+  Version get nextBreaking {
+    if (major == 0) {
+      return _incrementMinor();
+    }
+
+    return _incrementMajor();
+  }
+
+  /// Returns the first possible pre-release of this version.
+  Version get firstPreRelease => Version(major, minor, patch, pre: '0');
+
+  /// Returns whether this is the first possible pre-release of its version.
+  bool get isFirstPreRelease => preRelease.length == 1 && preRelease.first == 0;
+
+  Version _incrementMajor() => Version(major + 1, 0, 0);
+  Version _incrementMinor() => Version(major, minor + 1, 0);
+  Version _incrementPatch() => Version(major, minor, patch + 1);
+
+  /// Tests if [other] matches this version exactly.
+  @override
+  bool allows(Version other) => this == other;
+
+  @override
+  bool allowsAll(VersionConstraint other) => other.isEmpty || other == this;
+
+  @override
+  bool allowsAny(VersionConstraint other) => other.allows(this);
+
+  @override
+  VersionConstraint intersect(VersionConstraint other) =>
+      other.allows(this) ? this : VersionConstraint.empty;
+
+  @override
+  VersionConstraint union(VersionConstraint other) {
+    if (other.allows(this)) return other;
+
+    if (other is VersionRange) {
+      if (other.min == this) {
+        return VersionRange(
+            min: other.min,
+            max: other.max,
+            includeMin: true,
+            includeMax: other.includeMax,
+            alwaysIncludeMaxPreRelease: true);
+      }
+
+      if (other.max == this) {
+        return VersionRange(
+            min: other.min,
+            max: other.max,
+            includeMin: other.includeMin,
+            includeMax: true,
+            alwaysIncludeMaxPreRelease: true);
+      }
+    }
+
+    return VersionConstraint.unionOf([this, other]);
+  }
+
+  @override
+  VersionConstraint difference(VersionConstraint other) =>
+      other.allows(this) ? VersionConstraint.empty : this;
+
+  @override
+  int compareTo(VersionRange other) {
+    if (other is Version) {
+      if (major != other.major) return major.compareTo(other.major);
+      if (minor != other.minor) return minor.compareTo(other.minor);
+      if (patch != other.patch) return patch.compareTo(other.patch);
+
+      // Pre-releases always come before no pre-release string.
+      if (!isPreRelease && other.isPreRelease) return 1;
+      if (!other.isPreRelease && isPreRelease) return -1;
+
+      var comparison = _compareLists(preRelease, other.preRelease);
+      if (comparison != 0) return comparison;
+
+      // Builds always come after no build string.
+      if (build.isEmpty && other.build.isNotEmpty) return -1;
+      if (other.build.isEmpty && build.isNotEmpty) return 1;
+      return _compareLists(build, other.build);
+    } else {
+      return -other.compareTo(this);
+    }
+  }
+
+  /// Get non-canonical string representation of this [Version].
+  ///
+  /// If created with [Version.parse], the string from which the version was
+  /// parsed is returned. Unlike the [canonicalizedVersion] this preserves
+  /// artifacts such as leading zeros.
+  @override
+  String toString() => _text;
+
+  /// Get a canonicalized string representation of this [Version].
+  ///
+  /// Unlike [Version.toString()] this always returns a canonical string
+  /// representation of this [Version].
+  ///
+  /// **Example**
+  /// ```dart
+  /// final v = Version.parse('01.02.03-01.dev+pre.02');
+  ///
+  /// assert(v.toString() == '01.02.03-01.dev+pre.02');
+  /// assert(v.canonicalizedVersion == '1.2.3-1.dev+pre.2');
+  /// assert(Version.parse(v.canonicalizedVersion) == v);
+  /// ```
+  String get canonicalizedVersion => Version(
+        major,
+        minor,
+        patch,
+        pre: preRelease.isNotEmpty ? preRelease.join('.') : null,
+        build: build.isNotEmpty ? build.join('.') : null,
+      ).toString();
+
+  /// Compares a dot-separated component of two versions.
+  ///
+  /// This is used for the pre-release and build version parts. This follows
+  /// Rule 12 of the Semantic Versioning spec (v2.0.0-rc.1).
+  int _compareLists(List<Object> a, List<Object> b) {
+    for (var i = 0; i < math.max(a.length, b.length); i++) {
+      var aPart = (i < a.length) ? a[i] : null;
+      var bPart = (i < b.length) ? b[i] : null;
+
+      if (aPart == bPart) continue;
+
+      // Missing parts come before present ones.
+      if (aPart == null) return -1;
+      if (bPart == null) return 1;
+
+      if (aPart is num) {
+        if (bPart is num) {
+          // Compare two numbers.
+          return aPart.compareTo(bPart);
+        } else {
+          // Numbers come before strings.
+          return -1;
+        }
+      } else {
+        if (bPart is num) {
+          // Strings come after numbers.
+          return 1;
+        } else {
+          // Compare two strings.
+          return (aPart as String).compareTo(bPart as String);
+        }
+      }
+    }
+
+    // The lists are entirely equal.
+    return 0;
+  }
+}
diff --git a/pkgs/pub_semver/lib/src/version_constraint.dart b/pkgs/pub_semver/lib/src/version_constraint.dart
new file mode 100644
index 0000000..948118e
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/version_constraint.dart
@@ -0,0 +1,287 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'patterns.dart';
+import 'utils.dart';
+import 'version.dart';
+import 'version_range.dart';
+import 'version_union.dart';
+
+/// A [VersionConstraint] is a predicate that can determine whether a given
+/// version is valid or not.
+///
+/// For example, a ">= 2.0.0" constraint allows any version that is "2.0.0" or
+/// greater. Version objects themselves implement this to match a specific
+/// version.
+abstract class VersionConstraint {
+  /// A [VersionConstraint] that allows all versions.
+  static VersionConstraint any = VersionRange();
+
+  /// A [VersionConstraint] that allows no versions -- the empty set.
+  static VersionConstraint empty = const _EmptyVersion();
+
+  /// Parses a version constraint.
+  ///
+  /// This string is one of:
+  ///
+  ///   * "any". [any] version.
+  ///   * "^" followed by a version string. Versions compatible with
+  ///     ([VersionConstraint.compatibleWith]) the version.
+  ///   * a series of version parts. Each part can be one of:
+  ///     * A version string like `1.2.3`. In other words, anything that can be
+  ///       parsed by [Version.parse()].
+  ///     * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a
+  ///       version string.
+  ///
+  /// Whitespace is ignored.
+  ///
+  /// Examples:
+  ///
+  ///     any
+  ///     ^0.7.2
+  ///     ^1.0.0-alpha
+  ///     1.2.3-alpha
+  ///     <=5.1.4
+  ///     >2.0.4 <= 2.4.6
+  factory VersionConstraint.parse(String text) {
+    var originalText = text;
+
+    void skipWhitespace() {
+      text = text.trim();
+    }
+
+    skipWhitespace();
+
+    // Handle the "any" constraint.
+    if (text == 'any') return any;
+
+    // Try to parse and consume a version number.
+    Version? matchVersion() {
+      var version = startVersion.firstMatch(text);
+      if (version == null) return null;
+
+      text = text.substring(version.end);
+      return Version.parse(version[0]!);
+    }
+
+    // Try to parse and consume a comparison operator followed by a version.
+    VersionRange? matchComparison() {
+      var comparison = startComparison.firstMatch(text);
+      if (comparison == null) return null;
+
+      var op = comparison[0]!;
+      text = text.substring(comparison.end);
+      skipWhitespace();
+
+      var version = matchVersion();
+      if (version == null) {
+        throw FormatException('Expected version number after "$op" in '
+            '"$originalText", got "$text".');
+      }
+
+      return switch (op) {
+        '<=' => VersionRange(max: version, includeMax: true),
+        '<' => VersionRange(max: version, alwaysIncludeMaxPreRelease: true),
+        '>=' => VersionRange(min: version, includeMin: true),
+        '>' => VersionRange(min: version),
+        _ => throw UnsupportedError(op),
+      };
+    }
+
+    // Try to parse the "^" operator followed by a version.
+    VersionConstraint? matchCompatibleWith() {
+      if (!text.startsWith(compatibleWithChar)) return null;
+
+      text = text.substring(compatibleWithChar.length);
+      skipWhitespace();
+
+      var version = matchVersion();
+      if (version == null) {
+        throw FormatException('Expected version number after '
+            '"$compatibleWithChar" in "$originalText", got "$text".');
+      }
+
+      if (text.isNotEmpty) {
+        throw FormatException('Cannot include other constraints with '
+            '"$compatibleWithChar" constraint in "$originalText".');
+      }
+
+      return VersionConstraint.compatibleWith(version);
+    }
+
+    var compatibleWith = matchCompatibleWith();
+    if (compatibleWith != null) return compatibleWith;
+
+    Version? min;
+    var includeMin = false;
+    Version? max;
+    var includeMax = false;
+
+    for (;;) {
+      skipWhitespace();
+
+      if (text.isEmpty) break;
+
+      var newRange = matchVersion() ?? matchComparison();
+      if (newRange == null) {
+        throw FormatException('Could not parse version "$originalText". '
+            'Unknown text at "$text".');
+      }
+
+      if (newRange.min != null) {
+        if (min == null || newRange.min! > min) {
+          min = newRange.min;
+          includeMin = newRange.includeMin;
+        } else if (newRange.min == min && !newRange.includeMin) {
+          includeMin = false;
+        }
+      }
+
+      if (newRange.max != null) {
+        if (max == null || newRange.max! < max) {
+          max = newRange.max;
+          includeMax = newRange.includeMax;
+        } else if (newRange.max == max && !newRange.includeMax) {
+          includeMax = false;
+        }
+      }
+    }
+
+    if (min == null && max == null) {
+      throw const FormatException('Cannot parse an empty string.');
+    }
+
+    if (min != null && max != null) {
+      if (min > max) return VersionConstraint.empty;
+      if (min == max) {
+        if (includeMin && includeMax) return min;
+        return VersionConstraint.empty;
+      }
+    }
+
+    return VersionRange(
+        min: min, includeMin: includeMin, max: max, includeMax: includeMax);
+  }
+
+  /// Creates a version constraint which allows all versions that are
+  /// backward compatible with [version].
+  ///
+  /// Versions are considered backward compatible with [version] if they
+  /// are greater than or equal to [version], but less than the next breaking
+  /// version ([Version.nextBreaking]) of [version].
+  factory VersionConstraint.compatibleWith(Version version) =>
+      CompatibleWithVersionRange(version);
+
+  /// Creates a new version constraint that is the intersection of
+  /// [constraints].
+  ///
+  /// It only allows versions that all of those constraints allow. If
+  /// constraints is empty, then it returns a VersionConstraint that allows
+  /// all versions.
+  factory VersionConstraint.intersection(
+      Iterable<VersionConstraint> constraints) {
+    var constraint = VersionRange();
+    for (var other in constraints) {
+      constraint = constraint.intersect(other) as VersionRange;
+    }
+    return constraint;
+  }
+
+  /// Creates a new version constraint that is the union of [constraints].
+  ///
+  /// It allows any versions that any of those constraints allows. If
+  /// [constraints] is empty, this returns a constraint that allows no versions.
+  factory VersionConstraint.unionOf(Iterable<VersionConstraint> constraints) {
+    var flattened = constraints.expand((constraint) {
+      if (constraint.isEmpty) return <VersionRange>[];
+      if (constraint is VersionUnion) return constraint.ranges;
+      if (constraint is VersionRange) return [constraint];
+      throw ArgumentError('Unknown VersionConstraint type $constraint.');
+    }).toList();
+
+    if (flattened.isEmpty) return VersionConstraint.empty;
+
+    if (flattened.any((constraint) => constraint.isAny)) {
+      return VersionConstraint.any;
+    }
+
+    flattened.sort();
+
+    var merged = <VersionRange>[];
+    for (var constraint in flattened) {
+      // Merge this constraint with the previous one, but only if they touch.
+      if (merged.isEmpty ||
+          (!merged.last.allowsAny(constraint) &&
+              !areAdjacent(merged.last, constraint))) {
+        merged.add(constraint);
+      } else {
+        merged[merged.length - 1] =
+            merged.last.union(constraint) as VersionRange;
+      }
+    }
+
+    if (merged.length == 1) return merged.single;
+    return VersionUnion.fromRanges(merged);
+  }
+
+  /// Returns `true` if this constraint allows no versions.
+  bool get isEmpty;
+
+  /// Returns `true` if this constraint allows all versions.
+  bool get isAny;
+
+  /// Returns `true` if this constraint allows [version].
+  bool allows(Version version);
+
+  /// Returns `true` if this constraint allows all the versions that [other]
+  /// allows.
+  bool allowsAll(VersionConstraint other);
+
+  /// Returns `true` if this constraint allows any of the versions that [other]
+  /// allows.
+  bool allowsAny(VersionConstraint other);
+
+  /// Returns a [VersionConstraint] that only allows [Version]s allowed by both
+  /// this and [other].
+  VersionConstraint intersect(VersionConstraint other);
+
+  /// Returns a [VersionConstraint] that allows [Version]s allowed by either
+  /// this or [other].
+  VersionConstraint union(VersionConstraint other);
+
+  /// Returns a [VersionConstraint] that allows [Version]s allowed by this but
+  /// not [other].
+  VersionConstraint difference(VersionConstraint other);
+}
+
+class _EmptyVersion implements VersionConstraint {
+  const _EmptyVersion();
+
+  @override
+  bool get isEmpty => true;
+
+  @override
+  bool get isAny => false;
+
+  @override
+  bool allows(Version other) => false;
+
+  @override
+  bool allowsAll(VersionConstraint other) => other.isEmpty;
+
+  @override
+  bool allowsAny(VersionConstraint other) => false;
+
+  @override
+  VersionConstraint intersect(VersionConstraint other) => this;
+
+  @override
+  VersionConstraint union(VersionConstraint other) => other;
+
+  @override
+  VersionConstraint difference(VersionConstraint other) => this;
+
+  @override
+  String toString() => '<empty>';
+}
diff --git a/pkgs/pub_semver/lib/src/version_range.dart b/pkgs/pub_semver/lib/src/version_range.dart
new file mode 100644
index 0000000..6f2ed54
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/version_range.dart
@@ -0,0 +1,476 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'utils.dart';
+import 'version.dart';
+import 'version_constraint.dart';
+import 'version_union.dart';
+
+/// Constrains versions to a fall within a given range.
+///
+/// If there is a minimum, then this only allows versions that are at that
+/// minimum or greater. If there is a maximum, then only versions less than
+/// that are allowed. In other words, this allows `>= min, < max`.
+///
+/// Version ranges are ordered first by their lower bounds, then by their upper
+/// bounds. For example, `>=1.0.0 <2.0.0` is before `>=1.5.0 <2.0.0` is before
+/// `>=1.5.0 <3.0.0`.
+class VersionRange implements Comparable<VersionRange>, VersionConstraint {
+  /// The minimum end of the range.
+  ///
+  /// If [includeMin] is `true`, this will be the minimum allowed version.
+  /// Otherwise, it will be the highest version below the range that is not
+  /// allowed.
+  ///
+  /// This may be `null` in which case the range has no minimum end and allows
+  /// any version less than the maximum.
+  final Version? min;
+
+  /// The maximum end of the range.
+  ///
+  /// If [includeMax] is `true`, this will be the maximum allowed version.
+  /// Otherwise, it will be the lowest version above the range that is not
+  /// allowed.
+  ///
+  /// This may be `null` in which case the range has no maximum end and allows
+  /// any version greater than the minimum.
+  final Version? max;
+
+  /// If `true` then [min] is allowed by the range.
+  final bool includeMin;
+
+  /// If `true`, then [max] is allowed by the range.
+  final bool includeMax;
+
+  /// Creates a new version range from [min] to [max], either inclusive or
+  /// exclusive.
+  ///
+  /// If it is an error if [min] is greater than [max].
+  ///
+  /// Either [max] or [min] may be omitted to not clamp the range at that end.
+  /// If both are omitted, the range allows all versions.
+  ///
+  /// If [includeMin] is `true`, then the minimum end of the range is inclusive.
+  /// Likewise, passing [includeMax] as `true` makes the upper end inclusive.
+  ///
+  /// If [alwaysIncludeMaxPreRelease] is `true`, this will always include
+  /// pre-release versions of an exclusive [max]. Otherwise, it will use the
+  /// default behavior for pre-release versions of [max].
+  factory VersionRange(
+      {Version? min,
+      Version? max,
+      bool includeMin = false,
+      bool includeMax = false,
+      bool alwaysIncludeMaxPreRelease = false}) {
+    if (min != null && max != null && min > max) {
+      throw ArgumentError(
+          'Minimum version ("$min") must be less than maximum ("$max").');
+    }
+
+    if (!alwaysIncludeMaxPreRelease &&
+        !includeMax &&
+        max != null &&
+        !max.isPreRelease &&
+        max.build.isEmpty &&
+        (min == null ||
+            !min.isPreRelease ||
+            !equalsWithoutPreRelease(min, max))) {
+      max = max.firstPreRelease;
+    }
+
+    return VersionRange._(min, max, includeMin, includeMax);
+  }
+
+  VersionRange._(this.min, this.max, this.includeMin, this.includeMax);
+
+  @override
+  bool operator ==(Object other) {
+    if (other is! VersionRange) return false;
+
+    return min == other.min &&
+        max == other.max &&
+        includeMin == other.includeMin &&
+        includeMax == other.includeMax;
+  }
+
+  @override
+  int get hashCode =>
+      min.hashCode ^
+      (max.hashCode * 3) ^
+      (includeMin.hashCode * 5) ^
+      (includeMax.hashCode * 7);
+
+  @override
+  bool get isEmpty => false;
+
+  @override
+  bool get isAny => min == null && max == null;
+
+  /// Tests if [other] falls within this version range.
+  @override
+  bool allows(Version other) {
+    if (min != null) {
+      if (other < min!) return false;
+      if (!includeMin && other == min) return false;
+    }
+
+    if (max != null) {
+      if (other > max!) return false;
+      if (!includeMax && other == max) return false;
+    }
+
+    return true;
+  }
+
+  @override
+  bool allowsAll(VersionConstraint other) {
+    if (other.isEmpty) return true;
+    if (other is Version) return allows(other);
+
+    if (other is VersionUnion) {
+      return other.ranges.every(allowsAll);
+    }
+
+    if (other is VersionRange) {
+      return !allowsLower(other, this) && !allowsHigher(other, this);
+    }
+
+    throw ArgumentError('Unknown VersionConstraint type $other.');
+  }
+
+  @override
+  bool allowsAny(VersionConstraint other) {
+    if (other.isEmpty) return false;
+    if (other is Version) return allows(other);
+
+    if (other is VersionUnion) {
+      return other.ranges.any(allowsAny);
+    }
+
+    if (other is VersionRange) {
+      return !strictlyLower(other, this) && !strictlyHigher(other, this);
+    }
+
+    throw ArgumentError('Unknown VersionConstraint type $other.');
+  }
+
+  @override
+  VersionConstraint intersect(VersionConstraint other) {
+    if (other.isEmpty) return other;
+    if (other is VersionUnion) return other.intersect(this);
+
+    // A range and a Version just yields the version if it's in the range.
+    if (other is Version) {
+      return allows(other) ? other : VersionConstraint.empty;
+    }
+
+    if (other is VersionRange) {
+      // Intersect the two ranges.
+      Version? intersectMin;
+      bool intersectIncludeMin;
+      if (allowsLower(this, other)) {
+        if (strictlyLower(this, other)) return VersionConstraint.empty;
+        intersectMin = other.min;
+        intersectIncludeMin = other.includeMin;
+      } else {
+        if (strictlyLower(other, this)) return VersionConstraint.empty;
+        intersectMin = min;
+        intersectIncludeMin = includeMin;
+      }
+
+      Version? intersectMax;
+      bool intersectIncludeMax;
+      if (allowsHigher(this, other)) {
+        intersectMax = other.max;
+        intersectIncludeMax = other.includeMax;
+      } else {
+        intersectMax = max;
+        intersectIncludeMax = includeMax;
+      }
+
+      if (intersectMin == null && intersectMax == null) {
+        // Open range.
+        return VersionRange();
+      }
+
+      // If the range is just a single version.
+      if (intersectMin == intersectMax) {
+        // Because we already verified that the lower range isn't strictly
+        // lower, there must be some overlap.
+        assert(intersectIncludeMin && intersectIncludeMax);
+        return intersectMin!;
+      }
+
+      // If we got here, there is an actual range.
+      return VersionRange(
+          min: intersectMin,
+          max: intersectMax,
+          includeMin: intersectIncludeMin,
+          includeMax: intersectIncludeMax,
+          alwaysIncludeMaxPreRelease: true);
+    }
+
+    throw ArgumentError('Unknown VersionConstraint type $other.');
+  }
+
+  @override
+  VersionConstraint union(VersionConstraint other) {
+    if (other is Version) {
+      if (allows(other)) return this;
+
+      if (other == min) {
+        return VersionRange(
+            min: min,
+            max: max,
+            includeMin: true,
+            includeMax: includeMax,
+            alwaysIncludeMaxPreRelease: true);
+      }
+
+      if (other == max) {
+        return VersionRange(
+            min: min,
+            max: max,
+            includeMin: includeMin,
+            includeMax: true,
+            alwaysIncludeMaxPreRelease: true);
+      }
+
+      return VersionConstraint.unionOf([this, other]);
+    }
+
+    if (other is VersionRange) {
+      // If the two ranges don't overlap, we won't be able to create a single
+      // VersionRange for both of them.
+      var edgesTouch = (max != null &&
+              max == other.min &&
+              (includeMax || other.includeMin)) ||
+          (min != null && min == other.max && (includeMin || other.includeMax));
+      if (!edgesTouch && !allowsAny(other)) {
+        return VersionConstraint.unionOf([this, other]);
+      }
+
+      Version? unionMin;
+      bool unionIncludeMin;
+      if (allowsLower(this, other)) {
+        unionMin = min;
+        unionIncludeMin = includeMin;
+      } else {
+        unionMin = other.min;
+        unionIncludeMin = other.includeMin;
+      }
+
+      Version? unionMax;
+      bool unionIncludeMax;
+      if (allowsHigher(this, other)) {
+        unionMax = max;
+        unionIncludeMax = includeMax;
+      } else {
+        unionMax = other.max;
+        unionIncludeMax = other.includeMax;
+      }
+
+      return VersionRange(
+          min: unionMin,
+          max: unionMax,
+          includeMin: unionIncludeMin,
+          includeMax: unionIncludeMax,
+          alwaysIncludeMaxPreRelease: true);
+    }
+
+    return VersionConstraint.unionOf([this, other]);
+  }
+
+  @override
+  VersionConstraint difference(VersionConstraint other) {
+    if (other.isEmpty) return this;
+
+    if (other is Version) {
+      if (!allows(other)) return this;
+
+      if (other == min) {
+        if (!includeMin) return this;
+        return VersionRange(
+            min: min,
+            max: max,
+            includeMax: includeMax,
+            alwaysIncludeMaxPreRelease: true);
+      }
+
+      if (other == max) {
+        if (!includeMax) return this;
+        return VersionRange(
+            min: min,
+            max: max,
+            includeMin: includeMin,
+            alwaysIncludeMaxPreRelease: true);
+      }
+
+      return VersionUnion.fromRanges([
+        VersionRange(
+            min: min,
+            max: other,
+            includeMin: includeMin,
+            alwaysIncludeMaxPreRelease: true),
+        VersionRange(
+            min: other,
+            max: max,
+            includeMax: includeMax,
+            alwaysIncludeMaxPreRelease: true)
+      ]);
+    } else if (other is VersionRange) {
+      if (!allowsAny(other)) return this;
+
+      VersionRange? before;
+      if (!allowsLower(this, other)) {
+        before = null;
+      } else if (min == other.min) {
+        assert(includeMin && !other.includeMin);
+        assert(min != null);
+        before = min;
+      } else {
+        before = VersionRange(
+            min: min,
+            max: other.min,
+            includeMin: includeMin,
+            includeMax: !other.includeMin,
+            alwaysIncludeMaxPreRelease: true);
+      }
+
+      VersionRange? after;
+      if (!allowsHigher(this, other)) {
+        after = null;
+      } else if (max == other.max) {
+        assert(includeMax && !other.includeMax);
+        assert(max != null);
+        after = max;
+      } else {
+        after = VersionRange(
+            min: other.max,
+            max: max,
+            includeMin: !other.includeMax,
+            includeMax: includeMax,
+            alwaysIncludeMaxPreRelease: true);
+      }
+
+      if (before == null && after == null) return VersionConstraint.empty;
+      if (before == null) return after!;
+      if (after == null) return before;
+      return VersionUnion.fromRanges([before, after]);
+    } else if (other is VersionUnion) {
+      var ranges = <VersionRange>[];
+      var current = this;
+
+      for (var range in other.ranges) {
+        // Skip any ranges that are strictly lower than [current].
+        if (strictlyLower(range, current)) continue;
+
+        // If we reach a range strictly higher than [current], no more ranges
+        // will be relevant so we can bail early.
+        if (strictlyHigher(range, current)) break;
+
+        var difference = current.difference(range);
+        if (difference.isEmpty) {
+          return VersionConstraint.empty;
+        } else if (difference is VersionUnion) {
+          // If [range] split [current] in half, we only need to continue
+          // checking future ranges against the latter half.
+          assert(difference.ranges.length == 2);
+          ranges.add(difference.ranges.first);
+          current = difference.ranges.last;
+        } else {
+          current = difference as VersionRange;
+        }
+      }
+
+      if (ranges.isEmpty) return current;
+      return VersionUnion.fromRanges(ranges..add(current));
+    }
+
+    throw ArgumentError('Unknown VersionConstraint type $other.');
+  }
+
+  @override
+  int compareTo(VersionRange other) {
+    if (min == null) {
+      if (other.min == null) return _compareMax(other);
+      return -1;
+    } else if (other.min == null) {
+      return 1;
+    }
+
+    var result = min!.compareTo(other.min!);
+    if (result != 0) return result;
+    if (includeMin != other.includeMin) return includeMin ? -1 : 1;
+
+    return _compareMax(other);
+  }
+
+  /// Compares the maximum values of `this` and [other].
+  int _compareMax(VersionRange other) {
+    if (max == null) {
+      if (other.max == null) return 0;
+      return 1;
+    } else if (other.max == null) {
+      return -1;
+    }
+
+    var result = max!.compareTo(other.max!);
+    if (result != 0) return result;
+    if (includeMax != other.includeMax) return includeMax ? 1 : -1;
+    return 0;
+  }
+
+  @override
+  String toString() {
+    var buffer = StringBuffer();
+
+    final min = this.min;
+    if (min != null) {
+      buffer
+        ..write(includeMin ? '>=' : '>')
+        ..write(min);
+    }
+
+    final max = this.max;
+
+    if (max != null) {
+      if (min != null) buffer.write(' ');
+      if (includeMax) {
+        buffer
+          ..write('<=')
+          ..write(max);
+      } else {
+        buffer.write('<');
+        if (max.isFirstPreRelease) {
+          // Since `"<$max"` would parse the same as `"<$max-0"`, we just emit
+          // `<$max` to avoid confusing "-0" suffixes.
+          buffer.write('${max.major}.${max.minor}.${max.patch}');
+        } else {
+          buffer.write(max);
+
+          // If `">=$min <$max"` would parse as `">=$min <$max-0"`, add `-*` to
+          // indicate that actually does allow pre-release versions.
+          var minIsPreReleaseOfMax = min != null &&
+              min.isPreRelease &&
+              equalsWithoutPreRelease(min, max);
+          if (!max.isPreRelease && max.build.isEmpty && !minIsPreReleaseOfMax) {
+            buffer.write('-∞');
+          }
+        }
+      }
+    }
+
+    if (min == null && max == null) buffer.write('any');
+    return buffer.toString();
+  }
+}
+
+class CompatibleWithVersionRange extends VersionRange {
+  CompatibleWithVersionRange(Version version)
+      : super._(version, version.nextBreaking.firstPreRelease, true, false);
+
+  @override
+  String toString() => '^$min';
+}
diff --git a/pkgs/pub_semver/lib/src/version_union.dart b/pkgs/pub_semver/lib/src/version_union.dart
new file mode 100644
index 0000000..844d3b8
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/version_union.dart
@@ -0,0 +1,224 @@
+// 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:collection/collection.dart';
+
+import 'utils.dart';
+import 'version.dart';
+import 'version_constraint.dart';
+import 'version_range.dart';
+
+/// A version constraint representing a union of multiple disjoint version
+/// ranges.
+///
+/// An instance of this will only be created if the version can't be represented
+/// as a non-compound value.
+class VersionUnion implements VersionConstraint {
+  /// The constraints that compose this union.
+  ///
+  /// This list has two invariants:
+  ///
+  /// * Its contents are sorted using the standard ordering of [VersionRange]s.
+  /// * Its contents are disjoint and non-adjacent. In other words, for any two
+  ///   constraints next to each other in the list, there's some version between
+  ///   those constraints that they don't match.
+  final List<VersionRange> ranges;
+
+  @override
+  bool get isEmpty => false;
+
+  @override
+  bool get isAny => false;
+
+  /// Creates a union from a list of ranges with no pre-processing.
+  ///
+  /// It's up to the caller to ensure that the invariants described in [ranges]
+  /// are maintained. They are not verified by this constructor. To
+  /// automatically ensure that they're maintained, use
+  /// [VersionConstraint.unionOf] instead.
+  VersionUnion.fromRanges(this.ranges);
+
+  @override
+  bool allows(Version version) =>
+      ranges.any((constraint) => constraint.allows(version));
+
+  @override
+  bool allowsAll(VersionConstraint other) {
+    var ourRanges = ranges.iterator;
+    var theirRanges = _rangesFor(other).iterator;
+
+    // Because both lists of ranges are ordered by minimum version, we can
+    // safely move through them linearly here.
+    var ourRangesMoved = ourRanges.moveNext();
+    var theirRangesMoved = theirRanges.moveNext();
+    while (ourRangesMoved && theirRangesMoved) {
+      if (ourRanges.current.allowsAll(theirRanges.current)) {
+        theirRangesMoved = theirRanges.moveNext();
+      } else {
+        ourRangesMoved = ourRanges.moveNext();
+      }
+    }
+
+    // If our ranges have allowed all of their ranges, we'll have consumed all
+    // of them.
+    return !theirRangesMoved;
+  }
+
+  @override
+  bool allowsAny(VersionConstraint other) {
+    var ourRanges = ranges.iterator;
+    var theirRanges = _rangesFor(other).iterator;
+
+    // Because both lists of ranges are ordered by minimum version, we can
+    // safely move through them linearly here.
+    var ourRangesMoved = ourRanges.moveNext();
+    var theirRangesMoved = theirRanges.moveNext();
+    while (ourRangesMoved && theirRangesMoved) {
+      if (ourRanges.current.allowsAny(theirRanges.current)) {
+        return true;
+      }
+
+      // Move the constraint with the lower max value forward. This ensures that
+      // we keep both lists in sync as much as possible.
+      if (allowsHigher(theirRanges.current, ourRanges.current)) {
+        ourRangesMoved = ourRanges.moveNext();
+      } else {
+        theirRangesMoved = theirRanges.moveNext();
+      }
+    }
+
+    return false;
+  }
+
+  @override
+  VersionConstraint intersect(VersionConstraint other) {
+    var ourRanges = ranges.iterator;
+    var theirRanges = _rangesFor(other).iterator;
+
+    // Because both lists of ranges are ordered by minimum version, we can
+    // safely move through them linearly here.
+    var newRanges = <VersionRange>[];
+    var ourRangesMoved = ourRanges.moveNext();
+    var theirRangesMoved = theirRanges.moveNext();
+    while (ourRangesMoved && theirRangesMoved) {
+      var intersection = ourRanges.current.intersect(theirRanges.current);
+
+      if (!intersection.isEmpty) newRanges.add(intersection as VersionRange);
+
+      // Move the constraint with the lower max value forward. This ensures that
+      // we keep both lists in sync as much as possible, and that large ranges
+      // have a chance to match multiple small ranges that they contain.
+      if (allowsHigher(theirRanges.current, ourRanges.current)) {
+        ourRangesMoved = ourRanges.moveNext();
+      } else {
+        theirRangesMoved = theirRanges.moveNext();
+      }
+    }
+
+    if (newRanges.isEmpty) return VersionConstraint.empty;
+    if (newRanges.length == 1) return newRanges.single;
+
+    return VersionUnion.fromRanges(newRanges);
+  }
+
+  @override
+  VersionConstraint difference(VersionConstraint other) {
+    var ourRanges = ranges.iterator;
+    var theirRanges = _rangesFor(other).iterator;
+
+    var newRanges = <VersionRange>[];
+    ourRanges.moveNext();
+    theirRanges.moveNext();
+    var current = ourRanges.current;
+
+    bool theirNextRange() {
+      if (theirRanges.moveNext()) return true;
+
+      // If there are no more of their ranges, none of the rest of our ranges
+      // need to be subtracted so we can add them as-is.
+      newRanges.add(current);
+      while (ourRanges.moveNext()) {
+        newRanges.add(ourRanges.current);
+      }
+      return false;
+    }
+
+    bool ourNextRange({bool includeCurrent = true}) {
+      if (includeCurrent) newRanges.add(current);
+      if (!ourRanges.moveNext()) return false;
+      current = ourRanges.current;
+      return true;
+    }
+
+    for (;;) {
+      // If the current ranges are disjoint, move the lowest one forward.
+      if (strictlyLower(theirRanges.current, current)) {
+        if (!theirNextRange()) break;
+        continue;
+      }
+
+      if (strictlyHigher(theirRanges.current, current)) {
+        if (!ourNextRange()) break;
+        continue;
+      }
+
+      // If we're here, we know [theirRanges.current] overlaps [current].
+      var difference = current.difference(theirRanges.current);
+      if (difference is VersionUnion) {
+        // If their range split [current] in half, we only need to continue
+        // checking future ranges against the latter half.
+        assert(difference.ranges.length == 2);
+        newRanges.add(difference.ranges.first);
+        current = difference.ranges.last;
+
+        // Since their range split [current], it definitely doesn't allow higher
+        // versions, so we should move their ranges forward.
+        if (!theirNextRange()) break;
+      } else if (difference.isEmpty) {
+        if (!ourNextRange(includeCurrent: false)) break;
+      } else {
+        current = difference as VersionRange;
+
+        // Move the constraint with the lower max value forward. This ensures
+        // that we keep both lists in sync as much as possible, and that large
+        // ranges have a chance to subtract or be subtracted by multiple small
+        // ranges that they contain.
+        if (allowsHigher(current, theirRanges.current)) {
+          if (!theirNextRange()) break;
+        } else {
+          if (!ourNextRange()) break;
+        }
+      }
+    }
+
+    if (newRanges.isEmpty) return VersionConstraint.empty;
+    if (newRanges.length == 1) return newRanges.single;
+    return VersionUnion.fromRanges(newRanges);
+  }
+
+  /// Returns [constraint] as a list of ranges.
+  ///
+  /// This is used to normalize ranges of various types.
+  List<VersionRange> _rangesFor(VersionConstraint constraint) {
+    if (constraint.isEmpty) return [];
+    if (constraint is VersionUnion) return constraint.ranges;
+    if (constraint is VersionRange) return [constraint];
+    throw ArgumentError('Unknown VersionConstraint type $constraint.');
+  }
+
+  @override
+  VersionConstraint union(VersionConstraint other) =>
+      VersionConstraint.unionOf([this, other]);
+
+  @override
+  bool operator ==(Object other) =>
+      other is VersionUnion &&
+      const ListEquality<VersionRange>().equals(ranges, other.ranges);
+
+  @override
+  int get hashCode => const ListEquality<VersionRange>().hash(ranges);
+
+  @override
+  String toString() => ranges.join(' or ');
+}
diff --git a/pkgs/pub_semver/pubspec.yaml b/pkgs/pub_semver/pubspec.yaml
new file mode 100644
index 0000000..290fb92
--- /dev/null
+++ b/pkgs/pub_semver/pubspec.yaml
@@ -0,0 +1,20 @@
+name: pub_semver
+version: 2.1.5
+description: >-
+ Versions and version constraints implementing pub's versioning policy. This
+ is very similar to vanilla semver, with a few corner cases.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/pub_semver
+topics:
+ - dart-pub
+ - semver
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  collection: ^1.15.0
+  meta: ^1.3.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.0
diff --git a/pkgs/pub_semver/test/utils.dart b/pkgs/pub_semver/test/utils.dart
new file mode 100644
index 0000000..bd7aa8f
--- /dev/null
+++ b/pkgs/pub_semver/test/utils.dart
@@ -0,0 +1,123 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+/// Some stock example versions to use in tests.
+final v003 = Version.parse('0.0.3');
+final v010 = Version.parse('0.1.0');
+final v072 = Version.parse('0.7.2');
+final v080 = Version.parse('0.8.0');
+final v114 = Version.parse('1.1.4');
+final v123 = Version.parse('1.2.3');
+final v124 = Version.parse('1.2.4');
+final v130 = Version.parse('1.3.0');
+final v140 = Version.parse('1.4.0');
+final v200 = Version.parse('2.0.0');
+final v201 = Version.parse('2.0.1');
+final v234 = Version.parse('2.3.4');
+final v250 = Version.parse('2.5.0');
+final v300 = Version.parse('3.0.0');
+
+/// A range that allows pre-release versions of its max version.
+final includeMaxPreReleaseRange =
+    VersionRange(max: v200, alwaysIncludeMaxPreRelease: true);
+
+/// A [Matcher] that tests if a [VersionConstraint] allows or does not allow a
+/// given list of [Version]s.
+class _VersionConstraintMatcher implements Matcher {
+  final List<Version> _expected;
+  final bool _allow;
+
+  _VersionConstraintMatcher(this._expected, this._allow);
+
+  @override
+  bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
+      (item is VersionConstraint) &&
+      _expected.every((version) => item.allows(version) == _allow);
+
+  @override
+  Description describe(Description description) {
+    description.addAll(' ${_allow ? "allows" : "does not allow"} versions ',
+        ', ', '', _expected);
+    return description;
+  }
+
+  @override
+  Description describeMismatch(dynamic item, Description mismatchDescription,
+      Map<dynamic, dynamic> matchState, bool verbose) {
+    if (item is! VersionConstraint) {
+      mismatchDescription.add('was not a VersionConstraint');
+      return mismatchDescription;
+    }
+
+    var first = true;
+    for (var version in _expected) {
+      if (item.allows(version) != _allow) {
+        if (first) {
+          if (_allow) {
+            mismatchDescription.addDescriptionOf(item).add(' did not allow ');
+          } else {
+            mismatchDescription.addDescriptionOf(item).add(' allowed ');
+          }
+        } else {
+          mismatchDescription.add(' and ');
+        }
+        first = false;
+
+        mismatchDescription.add(version.toString());
+      }
+    }
+
+    return mismatchDescription;
+  }
+}
+
+/// Gets a [Matcher] that validates that a [VersionConstraint] allows all
+/// given versions.
+Matcher allows(Version v1,
+    [Version? v2,
+    Version? v3,
+    Version? v4,
+    Version? v5,
+    Version? v6,
+    Version? v7,
+    Version? v8]) {
+  var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8);
+  return _VersionConstraintMatcher(versions, true);
+}
+
+/// Gets a [Matcher] that validates that a [VersionConstraint] allows none of
+/// the given versions.
+Matcher doesNotAllow(Version v1,
+    [Version? v2,
+    Version? v3,
+    Version? v4,
+    Version? v5,
+    Version? v6,
+    Version? v7,
+    Version? v8]) {
+  var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8);
+  return _VersionConstraintMatcher(versions, false);
+}
+
+List<Version> _makeVersionList(Version v1,
+    [Version? v2,
+    Version? v3,
+    Version? v4,
+    Version? v5,
+    Version? v6,
+    Version? v7,
+    Version? v8]) {
+  var versions = [v1];
+  if (v2 != null) versions.add(v2);
+  if (v3 != null) versions.add(v3);
+  if (v4 != null) versions.add(v4);
+  if (v5 != null) versions.add(v5);
+  if (v6 != null) versions.add(v6);
+  if (v7 != null) versions.add(v7);
+  if (v8 != null) versions.add(v8);
+  return versions;
+}
diff --git a/pkgs/pub_semver/test/version_constraint_test.dart b/pkgs/pub_semver/test/version_constraint_test.dart
new file mode 100644
index 0000000..4fbcbe0
--- /dev/null
+++ b/pkgs/pub_semver/test/version_constraint_test.dart
@@ -0,0 +1,185 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  test('any', () {
+    expect(VersionConstraint.any.isAny, isTrue);
+    expect(
+        VersionConstraint.any,
+        allows(Version.parse('0.0.0-blah'), Version.parse('1.2.3'),
+            Version.parse('12345.678.90')));
+  });
+
+  test('empty', () {
+    expect(VersionConstraint.empty.isEmpty, isTrue);
+    expect(VersionConstraint.empty.isAny, isFalse);
+    expect(
+        VersionConstraint.empty,
+        doesNotAllow(Version.parse('0.0.0-blah'), Version.parse('1.2.3'),
+            Version.parse('12345.678.90')));
+  });
+
+  group('parse()', () {
+    test('parses an exact version', () {
+      var constraint = VersionConstraint.parse('1.2.3-alpha');
+
+      expect(constraint is Version, isTrue);
+      expect(constraint, equals(Version(1, 2, 3, pre: 'alpha')));
+    });
+
+    test('parses "any"', () {
+      var constraint = VersionConstraint.parse('any');
+
+      expect(
+          constraint,
+          allows(Version.parse('0.0.0'), Version.parse('1.2.3'),
+              Version.parse('12345.678.90')));
+    });
+
+    test('parses a ">" minimum version', () {
+      var constraint = VersionConstraint.parse('>1.2.3');
+
+      expect(constraint,
+          allows(Version.parse('1.2.3+foo'), Version.parse('1.2.4')));
+      expect(
+          constraint,
+          doesNotAllow(Version.parse('1.2.1'), Version.parse('1.2.3-build'),
+              Version.parse('1.2.3')));
+    });
+
+    test('parses a ">=" minimum version', () {
+      var constraint = VersionConstraint.parse('>=1.2.3');
+
+      expect(
+          constraint,
+          allows(Version.parse('1.2.3'), Version.parse('1.2.3+foo'),
+              Version.parse('1.2.4')));
+      expect(constraint,
+          doesNotAllow(Version.parse('1.2.1'), Version.parse('1.2.3-build')));
+    });
+
+    test('parses a "<" maximum version', () {
+      var constraint = VersionConstraint.parse('<1.2.3');
+
+      expect(constraint,
+          allows(Version.parse('1.2.1'), Version.parse('1.2.2+foo')));
+      expect(
+          constraint,
+          doesNotAllow(Version.parse('1.2.3'), Version.parse('1.2.3+foo'),
+              Version.parse('1.2.4')));
+    });
+
+    test('parses a "<=" maximum version', () {
+      var constraint = VersionConstraint.parse('<=1.2.3');
+
+      expect(
+          constraint,
+          allows(Version.parse('1.2.1'), Version.parse('1.2.3-build'),
+              Version.parse('1.2.3')));
+      expect(constraint,
+          doesNotAllow(Version.parse('1.2.3+foo'), Version.parse('1.2.4')));
+    });
+
+    test('parses a series of space-separated constraints', () {
+      var constraint = VersionConstraint.parse('>1.0.0 >=1.2.3 <1.3.0');
+
+      expect(
+          constraint, allows(Version.parse('1.2.3'), Version.parse('1.2.5')));
+      expect(
+          constraint,
+          doesNotAllow(Version.parse('1.2.3-pre'), Version.parse('1.3.0'),
+              Version.parse('3.4.5')));
+    });
+
+    test('parses a pre-release-only constraint', () {
+      var constraint = VersionConstraint.parse('>=1.0.0-dev.2 <1.0.0');
+      expect(constraint,
+          allows(Version.parse('1.0.0-dev.2'), Version.parse('1.0.0-dev.3')));
+      expect(constraint,
+          doesNotAllow(Version.parse('1.0.0-dev.1'), Version.parse('1.0.0')));
+    });
+
+    test('ignores whitespace around comparison operators', () {
+      var constraint = VersionConstraint.parse(' >1.0.0>=1.2.3 < 1.3.0');
+
+      expect(
+          constraint, allows(Version.parse('1.2.3'), Version.parse('1.2.5')));
+      expect(
+          constraint,
+          doesNotAllow(Version.parse('1.2.3-pre'), Version.parse('1.3.0'),
+              Version.parse('3.4.5')));
+    });
+
+    test('does not allow "any" to be mixed with other constraints', () {
+      expect(() => VersionConstraint.parse('any 1.0.0'), throwsFormatException);
+    });
+
+    test('parses a "^" version', () {
+      expect(VersionConstraint.parse('^0.0.3'),
+          equals(VersionConstraint.compatibleWith(v003)));
+
+      expect(VersionConstraint.parse('^0.7.2'),
+          equals(VersionConstraint.compatibleWith(v072)));
+
+      expect(VersionConstraint.parse('^1.2.3'),
+          equals(VersionConstraint.compatibleWith(v123)));
+
+      var min = Version.parse('0.7.2-pre+1');
+      expect(VersionConstraint.parse('^0.7.2-pre+1'),
+          equals(VersionConstraint.compatibleWith(min)));
+    });
+
+    test('does not allow "^" to be mixed with other constraints', () {
+      expect(() => VersionConstraint.parse('>=1.2.3 ^1.0.0'),
+          throwsFormatException);
+      expect(() => VersionConstraint.parse('^1.0.0 <1.2.3'),
+          throwsFormatException);
+    });
+
+    test('ignores whitespace around "^"', () {
+      var constraint = VersionConstraint.parse(' ^ 1.2.3 ');
+
+      expect(constraint, equals(VersionConstraint.compatibleWith(v123)));
+    });
+
+    test('throws FormatException on a bad string', () {
+      var bad = [
+        '', '   ', // Empty string.
+        'foo', // Bad text.
+        '>foo', // Bad text after operator.
+        '^foo', // Bad text after "^".
+        '1.0.0 foo', '1.0.0foo', // Bad text after version.
+        'anything', // Bad text after "any".
+        '<>1.0.0', // Multiple operators.
+        '1.0.0<' // Trailing operator.
+      ];
+
+      for (var text in bad) {
+        expect(() => VersionConstraint.parse(text), throwsFormatException);
+      }
+    });
+  });
+
+  group('compatibleWith()', () {
+    test('returns the range of compatible versions', () {
+      var constraint = VersionConstraint.compatibleWith(v072);
+
+      expect(
+          constraint,
+          equals(VersionRange(
+              min: v072, includeMin: true, max: v072.nextBreaking)));
+    });
+
+    test('toString() uses "^"', () {
+      var constraint = VersionConstraint.compatibleWith(v072);
+
+      expect(constraint.toString(), equals('^0.7.2'));
+    });
+  });
+}
diff --git a/pkgs/pub_semver/test/version_range_test.dart b/pkgs/pub_semver/test/version_range_test.dart
new file mode 100644
index 0000000..5978df0
--- /dev/null
+++ b/pkgs/pub_semver/test/version_range_test.dart
@@ -0,0 +1,998 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  group('constructor', () {
+    test('takes a min and max', () {
+      var range = VersionRange(min: v123, max: v124);
+      expect(range.isAny, isFalse);
+      expect(range.min, equals(v123));
+      expect(range.max, equals(v124.firstPreRelease));
+    });
+
+    group("doesn't make the max a pre-release if", () {
+      test("it's already a pre-release", () {
+        expect(VersionRange(max: Version.parse('1.2.4-pre')).max,
+            equals(Version.parse('1.2.4-pre')));
+      });
+
+      test('includeMax is true', () {
+        expect(VersionRange(max: v124, includeMax: true).max, equals(v124));
+      });
+
+      test('min is a prerelease of max', () {
+        expect(VersionRange(min: Version.parse('1.2.4-pre'), max: v124).max,
+            equals(v124));
+      });
+
+      test('max has a build identifier', () {
+        expect(VersionRange(max: Version.parse('1.2.4+1')).max,
+            equals(Version.parse('1.2.4+1')));
+      });
+    });
+
+    test('allows omitting max', () {
+      var range = VersionRange(min: v123);
+      expect(range.isAny, isFalse);
+      expect(range.min, equals(v123));
+      expect(range.max, isNull);
+    });
+
+    test('allows omitting min and max', () {
+      var range = VersionRange();
+      expect(range.isAny, isTrue);
+      expect(range.min, isNull);
+      expect(range.max, isNull);
+    });
+
+    test('takes includeMin', () {
+      var range = VersionRange(min: v123, includeMin: true);
+      expect(range.includeMin, isTrue);
+    });
+
+    test('includeMin defaults to false if omitted', () {
+      var range = VersionRange(min: v123);
+      expect(range.includeMin, isFalse);
+    });
+
+    test('takes includeMax', () {
+      var range = VersionRange(max: v123, includeMax: true);
+      expect(range.includeMax, isTrue);
+    });
+
+    test('includeMax defaults to false if omitted', () {
+      var range = VersionRange(max: v123);
+      expect(range.includeMax, isFalse);
+    });
+
+    test('throws if min > max', () {
+      expect(() => VersionRange(min: v124, max: v123), throwsArgumentError);
+    });
+  });
+
+  group('allows()', () {
+    test('version must be greater than min', () {
+      var range = VersionRange(min: v123);
+
+      expect(range, allows(Version.parse('1.3.3'), Version.parse('2.3.3')));
+      expect(
+          range, doesNotAllow(Version.parse('1.2.2'), Version.parse('1.2.3')));
+    });
+
+    test('version must be min or greater if includeMin', () {
+      var range = VersionRange(min: v123, includeMin: true);
+
+      expect(
+          range,
+          allows(Version.parse('1.2.3'), Version.parse('1.3.3'),
+              Version.parse('2.3.3')));
+      expect(range, doesNotAllow(Version.parse('1.2.2')));
+    });
+
+    test('pre-release versions of inclusive min are excluded', () {
+      var range = VersionRange(min: v123, includeMin: true);
+
+      expect(range, allows(Version.parse('1.2.4-dev')));
+      expect(range, doesNotAllow(Version.parse('1.2.3-dev')));
+    });
+
+    test('version must be less than max', () {
+      var range = VersionRange(max: v234);
+
+      expect(range, allows(Version.parse('2.3.3')));
+      expect(
+          range, doesNotAllow(Version.parse('2.3.4'), Version.parse('2.4.3')));
+    });
+
+    test('pre-release versions of non-pre-release max are excluded', () {
+      var range = VersionRange(max: v234);
+
+      expect(range, allows(Version.parse('2.3.3')));
+      expect(range,
+          doesNotAllow(Version.parse('2.3.4-dev'), Version.parse('2.3.4')));
+    });
+
+    test(
+        'pre-release versions of non-pre-release max are included if min is a '
+        'pre-release of the same version', () {
+      var range = VersionRange(min: Version.parse('2.3.4-dev.0'), max: v234);
+
+      expect(range, allows(Version.parse('2.3.4-dev.1')));
+      expect(
+          range,
+          doesNotAllow(Version.parse('2.3.3'), Version.parse('2.3.4-dev'),
+              Version.parse('2.3.4')));
+    });
+
+    test('pre-release versions of pre-release max are included', () {
+      var range = VersionRange(max: Version.parse('2.3.4-dev.2'));
+
+      expect(range, allows(Version.parse('2.3.4-dev.1')));
+      expect(
+          range,
+          doesNotAllow(
+              Version.parse('2.3.4-dev.2'), Version.parse('2.3.4-dev.3')));
+    });
+
+    test('version must be max or less if includeMax', () {
+      var range = VersionRange(min: v123, max: v234, includeMax: true);
+
+      expect(
+          range,
+          allows(
+              Version.parse('2.3.3'),
+              Version.parse('2.3.4'),
+              // Pre-releases of the max are allowed.
+              Version.parse('2.3.4-dev')));
+      expect(range, doesNotAllow(Version.parse('2.4.3')));
+    });
+
+    test('has no min if one was not set', () {
+      var range = VersionRange(max: v123);
+
+      expect(range, allows(Version.parse('0.0.0')));
+      expect(range, doesNotAllow(Version.parse('1.2.3')));
+    });
+
+    test('has no max if one was not set', () {
+      var range = VersionRange(min: v123);
+
+      expect(range, allows(Version.parse('1.3.3'), Version.parse('999.3.3')));
+      expect(range, doesNotAllow(Version.parse('1.2.3')));
+    });
+
+    test('allows any version if there is no min or max', () {
+      var range = VersionRange();
+
+      expect(range, allows(Version.parse('0.0.0'), Version.parse('999.99.9')));
+    });
+
+    test('allows pre-releases of the max with includeMaxPreRelease', () {
+      expect(includeMaxPreReleaseRange, allows(Version.parse('2.0.0-dev')));
+    });
+  });
+
+  group('allowsAll()', () {
+    test('allows an empty constraint', () {
+      expect(
+          VersionRange(min: v123, max: v250).allowsAll(VersionConstraint.empty),
+          isTrue);
+    });
+
+    test('allows allowed versions', () {
+      var range = VersionRange(min: v123, max: v250, includeMax: true);
+      expect(range.allowsAll(v123), isFalse);
+      expect(range.allowsAll(v124), isTrue);
+      expect(range.allowsAll(v250), isTrue);
+      expect(range.allowsAll(v300), isFalse);
+    });
+
+    test('with no min', () {
+      var range = VersionRange(max: v250);
+      expect(range.allowsAll(VersionRange(min: v080, max: v140)), isTrue);
+      expect(range.allowsAll(VersionRange(min: v080, max: v300)), isFalse);
+      expect(range.allowsAll(VersionRange(max: v140)), isTrue);
+      expect(range.allowsAll(VersionRange(max: v300)), isFalse);
+      expect(range.allowsAll(range), isTrue);
+      expect(range.allowsAll(VersionConstraint.any), isFalse);
+    });
+
+    test('with no max', () {
+      var range = VersionRange(min: v010);
+      expect(range.allowsAll(VersionRange(min: v080, max: v140)), isTrue);
+      expect(range.allowsAll(VersionRange(min: v003, max: v140)), isFalse);
+      expect(range.allowsAll(VersionRange(min: v080)), isTrue);
+      expect(range.allowsAll(VersionRange(min: v003)), isFalse);
+      expect(range.allowsAll(range), isTrue);
+      expect(range.allowsAll(VersionConstraint.any), isFalse);
+    });
+
+    test('with a min and max', () {
+      var range = VersionRange(min: v010, max: v250);
+      expect(range.allowsAll(VersionRange(min: v080, max: v140)), isTrue);
+      expect(range.allowsAll(VersionRange(min: v080, max: v300)), isFalse);
+      expect(range.allowsAll(VersionRange(min: v003, max: v140)), isFalse);
+      expect(range.allowsAll(VersionRange(min: v080)), isFalse);
+      expect(range.allowsAll(VersionRange(max: v140)), isFalse);
+      expect(range.allowsAll(range), isTrue);
+    });
+
+    test("allows a bordering range that's not more inclusive", () {
+      var exclusive = VersionRange(min: v010, max: v250);
+      var inclusive = VersionRange(
+          min: v010, includeMin: true, max: v250, includeMax: true);
+      expect(inclusive.allowsAll(exclusive), isTrue);
+      expect(inclusive.allowsAll(inclusive), isTrue);
+      expect(exclusive.allowsAll(inclusive), isFalse);
+      expect(exclusive.allowsAll(exclusive), isTrue);
+    });
+
+    test('allows unions that are completely contained', () {
+      var range = VersionRange(min: v114, max: v200);
+      expect(range.allowsAll(VersionRange(min: v123, max: v124).union(v140)),
+          isTrue);
+      expect(range.allowsAll(VersionRange(min: v010, max: v124).union(v140)),
+          isFalse);
+      expect(range.allowsAll(VersionRange(min: v123, max: v234).union(v140)),
+          isFalse);
+    });
+
+    group('pre-release versions', () {
+      test('of inclusive min are excluded', () {
+        var range = VersionRange(min: v123, includeMin: true);
+
+        expect(range.allowsAll(VersionConstraint.parse('>1.2.4-dev')), isTrue);
+        expect(range.allowsAll(VersionConstraint.parse('>1.2.3-dev')), isFalse);
+      });
+
+      test('of non-pre-release max are excluded', () {
+        var range = VersionRange(max: v234);
+
+        expect(range.allowsAll(VersionConstraint.parse('<2.3.3')), isTrue);
+        expect(range.allowsAll(VersionConstraint.parse('<2.3.4-dev')), isFalse);
+      });
+
+      test('of non-pre-release max are included with includeMaxPreRelease', () {
+        expect(
+            includeMaxPreReleaseRange
+                .allowsAll(VersionConstraint.parse('<2.0.0-dev')),
+            isTrue);
+      });
+
+      test(
+          'of non-pre-release max are included if min is a pre-release of the '
+          'same version', () {
+        var range = VersionRange(min: Version.parse('2.3.4-dev.0'), max: v234);
+
+        expect(
+            range.allowsAll(
+                VersionConstraint.parse('>2.3.4-dev.0 <2.3.4-dev.1')),
+            isTrue);
+      });
+
+      test('of pre-release max are included', () {
+        var range = VersionRange(max: Version.parse('2.3.4-dev.2'));
+
+        expect(
+            range.allowsAll(VersionConstraint.parse('<2.3.4-dev.1')), isTrue);
+        expect(
+            range.allowsAll(VersionConstraint.parse('<2.3.4-dev.2')), isTrue);
+        expect(
+            range.allowsAll(VersionConstraint.parse('<=2.3.4-dev.2')), isFalse);
+        expect(
+            range.allowsAll(VersionConstraint.parse('<2.3.4-dev.3')), isFalse);
+      });
+    });
+  });
+
+  group('allowsAny()', () {
+    test('disallows an empty constraint', () {
+      expect(
+          VersionRange(min: v123, max: v250).allowsAny(VersionConstraint.empty),
+          isFalse);
+    });
+
+    test('allows allowed versions', () {
+      var range = VersionRange(min: v123, max: v250, includeMax: true);
+      expect(range.allowsAny(v123), isFalse);
+      expect(range.allowsAny(v124), isTrue);
+      expect(range.allowsAny(v250), isTrue);
+      expect(range.allowsAny(v300), isFalse);
+    });
+
+    test('with no min', () {
+      var range = VersionRange(max: v200);
+      expect(range.allowsAny(VersionRange(min: v140, max: v300)), isTrue);
+      expect(range.allowsAny(VersionRange(min: v234, max: v300)), isFalse);
+      expect(range.allowsAny(VersionRange(min: v140)), isTrue);
+      expect(range.allowsAny(VersionRange(min: v234)), isFalse);
+      expect(range.allowsAny(range), isTrue);
+    });
+
+    test('with no max', () {
+      var range = VersionRange(min: v072);
+      expect(range.allowsAny(VersionRange(min: v003, max: v140)), isTrue);
+      expect(range.allowsAny(VersionRange(min: v003, max: v010)), isFalse);
+      expect(range.allowsAny(VersionRange(max: v080)), isTrue);
+      expect(range.allowsAny(VersionRange(max: v003)), isFalse);
+      expect(range.allowsAny(range), isTrue);
+    });
+
+    test('with a min and max', () {
+      var range = VersionRange(min: v072, max: v200);
+      expect(range.allowsAny(VersionRange(min: v003, max: v140)), isTrue);
+      expect(range.allowsAny(VersionRange(min: v140, max: v300)), isTrue);
+      expect(range.allowsAny(VersionRange(min: v003, max: v010)), isFalse);
+      expect(range.allowsAny(VersionRange(min: v234, max: v300)), isFalse);
+      expect(range.allowsAny(VersionRange(max: v010)), isFalse);
+      expect(range.allowsAny(VersionRange(min: v234)), isFalse);
+      expect(range.allowsAny(range), isTrue);
+    });
+
+    test('allows a bordering range when both are inclusive', () {
+      expect(
+          VersionRange(max: v250).allowsAny(VersionRange(min: v250)), isFalse);
+
+      expect(
+          VersionRange(max: v250, includeMax: true)
+              .allowsAny(VersionRange(min: v250)),
+          isFalse);
+
+      expect(
+          VersionRange(max: v250)
+              .allowsAny(VersionRange(min: v250, includeMin: true)),
+          isFalse);
+
+      expect(
+          VersionRange(max: v250, includeMax: true)
+              .allowsAny(VersionRange(min: v250, includeMin: true)),
+          isTrue);
+
+      expect(
+          VersionRange(min: v250).allowsAny(VersionRange(max: v250)), isFalse);
+
+      expect(
+          VersionRange(min: v250, includeMin: true)
+              .allowsAny(VersionRange(max: v250)),
+          isFalse);
+
+      expect(
+          VersionRange(min: v250)
+              .allowsAny(VersionRange(max: v250, includeMax: true)),
+          isFalse);
+
+      expect(
+          VersionRange(min: v250, includeMin: true)
+              .allowsAny(VersionRange(max: v250, includeMax: true)),
+          isTrue);
+    });
+
+    test('allows unions that are partially contained', () {
+      var range = VersionRange(min: v114, max: v200);
+      expect(range.allowsAny(VersionRange(min: v010, max: v080).union(v140)),
+          isTrue);
+      expect(range.allowsAny(VersionRange(min: v123, max: v234).union(v300)),
+          isTrue);
+      expect(range.allowsAny(VersionRange(min: v234, max: v300).union(v010)),
+          isFalse);
+    });
+
+    group('pre-release versions', () {
+      test('of inclusive min are excluded', () {
+        var range = VersionRange(min: v123, includeMin: true);
+
+        expect(range.allowsAny(VersionConstraint.parse('<1.2.4-dev')), isTrue);
+        expect(range.allowsAny(VersionConstraint.parse('<1.2.3-dev')), isFalse);
+      });
+
+      test('of non-pre-release max are excluded', () {
+        var range = VersionRange(max: v234);
+
+        expect(range.allowsAny(VersionConstraint.parse('>2.3.3')), isTrue);
+        expect(range.allowsAny(VersionConstraint.parse('>2.3.4-dev')), isFalse);
+      });
+
+      test('of non-pre-release max are included with includeMaxPreRelease', () {
+        expect(
+            includeMaxPreReleaseRange
+                .allowsAny(VersionConstraint.parse('>2.0.0-dev')),
+            isTrue);
+      });
+
+      test(
+          'of non-pre-release max are included if min is a pre-release of the '
+          'same version', () {
+        var range = VersionRange(min: Version.parse('2.3.4-dev.0'), max: v234);
+
+        expect(
+            range.allowsAny(VersionConstraint.parse('>2.3.4-dev.1')), isTrue);
+        expect(range.allowsAny(VersionConstraint.parse('>2.3.4')), isFalse);
+
+        expect(
+            range.allowsAny(VersionConstraint.parse('<2.3.4-dev.1')), isTrue);
+        expect(range.allowsAny(VersionConstraint.parse('<2.3.4-dev')), isFalse);
+      });
+
+      test('of pre-release max are included', () {
+        var range = VersionConstraint.parse('<2.3.4-dev.2');
+
+        expect(
+            range.allowsAny(VersionConstraint.parse('>2.3.4-dev.1')), isTrue);
+        expect(
+            range.allowsAny(VersionConstraint.parse('>2.3.4-dev.2')), isFalse);
+        expect(
+            range.allowsAny(VersionConstraint.parse('>2.3.4-dev.3')), isFalse);
+      });
+    });
+  });
+
+  group('intersect()', () {
+    test('two overlapping ranges', () {
+      expect(
+          VersionRange(min: v123, max: v250)
+              .intersect(VersionRange(min: v200, max: v300)),
+          equals(VersionRange(min: v200, max: v250)));
+    });
+
+    test('a non-overlapping range allows no versions', () {
+      var a = VersionRange(min: v114, max: v124);
+      var b = VersionRange(min: v200, max: v250);
+      expect(a.intersect(b).isEmpty, isTrue);
+    });
+
+    test('adjacent ranges allow no versions if exclusive', () {
+      var a = VersionRange(min: v114, max: v124);
+      var b = VersionRange(min: v124, max: v200);
+      expect(a.intersect(b).isEmpty, isTrue);
+    });
+
+    test('adjacent ranges allow version if inclusive', () {
+      var a = VersionRange(min: v114, max: v124, includeMax: true);
+      var b = VersionRange(min: v124, max: v200, includeMin: true);
+      expect(a.intersect(b), equals(v124));
+    });
+
+    test('with an open range', () {
+      var open = VersionRange();
+      var a = VersionRange(min: v114, max: v124);
+      expect(open.intersect(open), equals(open));
+      expect(a.intersect(open), equals(a));
+    });
+
+    test('returns the version if the range allows it', () {
+      expect(VersionRange(min: v114, max: v124).intersect(v123), equals(v123));
+      expect(
+          VersionRange(min: v123, max: v124).intersect(v114).isEmpty, isTrue);
+    });
+
+    test('with a range with a pre-release min, returns an empty constraint',
+        () {
+      expect(
+          VersionRange(max: v200)
+              .intersect(VersionConstraint.parse('>=2.0.0-dev')),
+          equals(VersionConstraint.empty));
+    });
+
+    test('with a range with a pre-release max, returns the original', () {
+      expect(
+          VersionRange(max: v200)
+              .intersect(VersionConstraint.parse('<2.0.0-dev')),
+          equals(VersionRange(max: v200)));
+    });
+
+    group('with includeMaxPreRelease', () {
+      test('preserves includeMaxPreRelease if the max version is included', () {
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(VersionConstraint.parse('<1.0.0')),
+            equals(VersionConstraint.parse('<1.0.0')));
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(VersionConstraint.parse('<2.0.0')),
+            equals(VersionConstraint.parse('<2.0.0')));
+        expect(includeMaxPreReleaseRange.intersect(includeMaxPreReleaseRange),
+            equals(includeMaxPreReleaseRange));
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(VersionConstraint.parse('<3.0.0')),
+            equals(includeMaxPreReleaseRange));
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(VersionConstraint.parse('>1.1.4')),
+            equals(VersionRange(
+                min: v114, max: v200, alwaysIncludeMaxPreRelease: true)));
+      });
+
+      test(
+          'and a range with a pre-release min, returns '
+          'an intersection', () {
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(VersionConstraint.parse('>=2.0.0-dev')),
+            equals(VersionConstraint.parse('>=2.0.0-dev <2.0.0')));
+      });
+
+      test(
+          'and a range with a pre-release max, returns '
+          'the narrower constraint', () {
+        expect(
+            includeMaxPreReleaseRange
+                .intersect(VersionConstraint.parse('<2.0.0-dev')),
+            equals(VersionConstraint.parse('<2.0.0-dev')));
+      });
+    });
+  });
+
+  group('union()', () {
+    test('with a version returns the range if it contains the version', () {
+      var range = VersionRange(min: v114, max: v124);
+      expect(range.union(v123), equals(range));
+    });
+
+    test('with a version on the edge of the range, expands the range', () {
+      expect(
+          VersionRange(min: v114, max: v124, alwaysIncludeMaxPreRelease: true)
+              .union(v124),
+          equals(VersionRange(min: v114, max: v124, includeMax: true)));
+      expect(VersionRange(min: v114, max: v124).union(v114),
+          equals(VersionRange(min: v114, max: v124, includeMin: true)));
+    });
+
+    test(
+        'with a version allows both the range and the version if the range '
+        "doesn't contain the version", () {
+      var result = VersionRange(min: v003, max: v114).union(v124);
+      expect(result, allows(v010));
+      expect(result, doesNotAllow(v123));
+      expect(result, allows(v124));
+    });
+
+    test('returns a VersionUnion for a disjoint range', () {
+      var result = VersionRange(min: v003, max: v114)
+          .union(VersionRange(min: v130, max: v200));
+      expect(result, allows(v080));
+      expect(result, doesNotAllow(v123));
+      expect(result, allows(v140));
+    });
+
+    test('returns a VersionUnion for a disjoint range with infinite end', () {
+      void isVersionUnion(VersionConstraint constraint) {
+        expect(constraint, allows(v080));
+        expect(constraint, doesNotAllow(v123));
+        expect(constraint, allows(v140));
+      }
+
+      for (final includeAMin in [true, false]) {
+        for (final includeAMax in [true, false]) {
+          for (final includeBMin in [true, false]) {
+            for (final includeBMax in [true, false]) {
+              final a = VersionRange(
+                  min: v130, includeMin: includeAMin, includeMax: includeAMax);
+              final b = VersionRange(
+                  max: v114, includeMin: includeBMin, includeMax: includeBMax);
+              isVersionUnion(a.union(b));
+              isVersionUnion(b.union(a));
+            }
+          }
+        }
+      }
+    });
+
+    test('considers open ranges disjoint', () {
+      var result = VersionRange(min: v003, max: v114)
+          .union(VersionRange(min: v114, max: v200));
+      expect(result, allows(v080));
+      expect(result, doesNotAllow(v114));
+      expect(result, allows(v140));
+
+      result = VersionRange(min: v114, max: v200)
+          .union(VersionRange(min: v003, max: v114));
+      expect(result, allows(v080));
+      expect(result, doesNotAllow(v114));
+      expect(result, allows(v140));
+    });
+
+    test('returns a merged range for an overlapping range', () {
+      var result = VersionRange(min: v003, max: v114)
+          .union(VersionRange(min: v080, max: v200));
+      expect(result, equals(VersionRange(min: v003, max: v200)));
+    });
+
+    test('considers closed ranges overlapping', () {
+      var result = VersionRange(min: v003, max: v114, includeMax: true)
+          .union(VersionRange(min: v114, max: v200));
+      expect(result, equals(VersionRange(min: v003, max: v200)));
+
+      result =
+          VersionRange(min: v003, max: v114, alwaysIncludeMaxPreRelease: true)
+              .union(VersionRange(min: v114, max: v200, includeMin: true));
+      expect(result, equals(VersionRange(min: v003, max: v200)));
+
+      result = VersionRange(min: v114, max: v200)
+          .union(VersionRange(min: v003, max: v114, includeMax: true));
+      expect(result, equals(VersionRange(min: v003, max: v200)));
+
+      result = VersionRange(min: v114, max: v200, includeMin: true).union(
+          VersionRange(min: v003, max: v114, alwaysIncludeMaxPreRelease: true));
+      expect(result, equals(VersionRange(min: v003, max: v200)));
+    });
+
+    test('includes edges if either range does', () {
+      var result = VersionRange(min: v003, max: v114, includeMin: true)
+          .union(VersionRange(min: v003, max: v114, includeMax: true));
+      expect(
+          result,
+          equals(VersionRange(
+              min: v003, max: v114, includeMin: true, includeMax: true)));
+    });
+
+    test('with a range with a pre-release min, returns a constraint with a gap',
+        () {
+      var result =
+          VersionRange(max: v200).union(VersionConstraint.parse('>=2.0.0-dev'));
+      expect(result, allows(v140));
+      expect(result, doesNotAllow(Version.parse('2.0.0-alpha')));
+      expect(result, allows(Version.parse('2.0.0-dev')));
+      expect(result, allows(Version.parse('2.0.0-dev.1')));
+      expect(result, allows(Version.parse('2.0.0')));
+    });
+
+    test('with a range with a pre-release max, returns the larger constraint',
+        () {
+      expect(
+          VersionRange(max: v200).union(VersionConstraint.parse('<2.0.0-dev')),
+          equals(VersionConstraint.parse('<2.0.0-dev')));
+    });
+
+    group('with includeMaxPreRelease', () {
+      test('adds includeMaxPreRelease if the max version is included', () {
+        expect(
+            includeMaxPreReleaseRange.union(VersionConstraint.parse('<1.0.0')),
+            equals(includeMaxPreReleaseRange));
+        expect(includeMaxPreReleaseRange.union(includeMaxPreReleaseRange),
+            equals(includeMaxPreReleaseRange));
+        expect(
+            includeMaxPreReleaseRange.union(VersionConstraint.parse('<2.0.0')),
+            equals(includeMaxPreReleaseRange));
+        expect(
+            includeMaxPreReleaseRange.union(VersionConstraint.parse('<3.0.0')),
+            equals(VersionConstraint.parse('<3.0.0')));
+      });
+
+      test('and a range with a pre-release min, returns any', () {
+        expect(
+            includeMaxPreReleaseRange
+                .union(VersionConstraint.parse('>=2.0.0-dev')),
+            equals(VersionConstraint.any));
+      });
+
+      test('and a range with a pre-release max, returns the original', () {
+        expect(
+            includeMaxPreReleaseRange
+                .union(VersionConstraint.parse('<2.0.0-dev')),
+            equals(includeMaxPreReleaseRange));
+      });
+    });
+  });
+
+  group('difference()', () {
+    test('with an empty range returns the original range', () {
+      expect(
+          VersionRange(min: v003, max: v114)
+              .difference(VersionConstraint.empty),
+          equals(VersionRange(min: v003, max: v114)));
+    });
+
+    test('with a version outside the range returns the original range', () {
+      expect(VersionRange(min: v003, max: v114).difference(v200),
+          equals(VersionRange(min: v003, max: v114)));
+    });
+
+    test('with a version in the range splits the range', () {
+      expect(
+          VersionRange(min: v003, max: v114).difference(v072),
+          equals(VersionConstraint.unionOf([
+            VersionRange(
+                min: v003, max: v072, alwaysIncludeMaxPreRelease: true),
+            VersionRange(min: v072, max: v114)
+          ])));
+    });
+
+    test('with the max version makes the max exclusive', () {
+      expect(
+          VersionRange(min: v003, max: v114, includeMax: true).difference(v114),
+          equals(VersionRange(
+              min: v003, max: v114, alwaysIncludeMaxPreRelease: true)));
+    });
+
+    test('with the min version makes the min exclusive', () {
+      expect(
+          VersionRange(min: v003, max: v114, includeMin: true).difference(v003),
+          equals(VersionRange(min: v003, max: v114)));
+    });
+
+    test('with a disjoint range returns the original', () {
+      expect(
+          VersionRange(min: v003, max: v114)
+              .difference(VersionRange(min: v123, max: v140)),
+          equals(VersionRange(min: v003, max: v114)));
+    });
+
+    test('with an adjacent range returns the original', () {
+      expect(
+          VersionRange(min: v003, max: v114, includeMax: true)
+              .difference(VersionRange(min: v114, max: v140)),
+          equals(VersionRange(min: v003, max: v114, includeMax: true)));
+    });
+
+    test('with a range at the beginning cuts off the beginning of the range',
+        () {
+      expect(
+          VersionRange(min: v080, max: v130)
+              .difference(VersionRange(min: v010, max: v114)),
+          equals(VersionConstraint.parse('>=1.1.4-0 <1.3.0')));
+      expect(
+          VersionRange(min: v080, max: v130)
+              .difference(VersionRange(max: v114)),
+          equals(VersionConstraint.parse('>=1.1.4-0 <1.3.0')));
+      expect(
+          VersionRange(min: v080, max: v130)
+              .difference(VersionRange(min: v010, max: v114, includeMax: true)),
+          equals(VersionRange(min: v114, max: v130)));
+      expect(
+          VersionRange(min: v080, max: v130, includeMin: true)
+              .difference(VersionRange(min: v010, max: v080, includeMax: true)),
+          equals(VersionRange(min: v080, max: v130)));
+      expect(
+          VersionRange(min: v080, max: v130, includeMax: true)
+              .difference(VersionRange(min: v080, max: v130)),
+          equals(VersionConstraint.parse('>=1.3.0-0 <=1.3.0')));
+    });
+
+    test('with a range at the end cuts off the end of the range', () {
+      expect(
+          VersionRange(min: v080, max: v130)
+              .difference(VersionRange(min: v114, max: v140)),
+          equals(VersionRange(min: v080, max: v114, includeMax: true)));
+      expect(
+          VersionRange(min: v080, max: v130)
+              .difference(VersionRange(min: v114)),
+          equals(VersionRange(min: v080, max: v114, includeMax: true)));
+      expect(
+          VersionRange(min: v080, max: v130)
+              .difference(VersionRange(min: v114, max: v140, includeMin: true)),
+          equals(VersionRange(
+              min: v080, max: v114, alwaysIncludeMaxPreRelease: true)));
+      expect(
+          VersionRange(min: v080, max: v130, includeMax: true)
+              .difference(VersionRange(min: v130, max: v140, includeMin: true)),
+          equals(VersionRange(
+              min: v080, max: v130, alwaysIncludeMaxPreRelease: true)));
+      expect(
+          VersionRange(min: v080, max: v130, includeMin: true)
+              .difference(VersionRange(min: v080, max: v130)),
+          equals(v080));
+    });
+
+    test('with a range in the middle cuts the range in half', () {
+      expect(
+          VersionRange(min: v003, max: v130)
+              .difference(VersionRange(min: v072, max: v114)),
+          equals(VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v072, includeMax: true),
+            VersionConstraint.parse('>=1.1.4-0 <1.3.0')
+          ])));
+    });
+
+    test('with a totally covering range returns empty', () {
+      expect(
+          VersionRange(min: v114, max: v200)
+              .difference(VersionRange(min: v072, max: v300)),
+          isEmpty);
+      expect(
+          VersionRange(min: v003, max: v114)
+              .difference(VersionRange(min: v003, max: v114)),
+          isEmpty);
+      expect(
+          VersionRange(min: v003, max: v114, includeMin: true, includeMax: true)
+              .difference(VersionRange(
+                  min: v003, max: v114, includeMin: true, includeMax: true)),
+          isEmpty);
+    });
+
+    test(
+        "with a version union that doesn't cover the range, returns the "
+        'original', () {
+      expect(
+          VersionRange(min: v114, max: v140)
+              .difference(VersionConstraint.unionOf([v010, v200])),
+          equals(VersionRange(min: v114, max: v140)));
+    });
+
+    test('with a version union that intersects the ends, chops them off', () {
+      expect(
+          VersionRange(min: v114, max: v140).difference(
+              VersionConstraint.unionOf([
+            VersionRange(min: v080, max: v123),
+            VersionRange(min: v130, max: v200)
+          ])),
+          equals(VersionConstraint.parse('>=1.2.3-0 <=1.3.0')));
+    });
+
+    test('with a version union that intersects the middle, chops it up', () {
+      expect(
+          VersionRange(min: v114, max: v140)
+              .difference(VersionConstraint.unionOf([v123, v124, v130])),
+          equals(VersionConstraint.unionOf([
+            VersionRange(
+                min: v114, max: v123, alwaysIncludeMaxPreRelease: true),
+            VersionRange(
+                min: v123, max: v124, alwaysIncludeMaxPreRelease: true),
+            VersionRange(
+                min: v124, max: v130, alwaysIncludeMaxPreRelease: true),
+            VersionRange(min: v130, max: v140)
+          ])));
+    });
+
+    test('with a version union that covers the whole range, returns empty', () {
+      expect(
+          VersionRange(min: v114, max: v140).difference(
+              VersionConstraint.unionOf([v003, VersionRange(min: v010)])),
+          equals(VersionConstraint.empty));
+    });
+
+    test('with a range with a pre-release min, returns the original', () {
+      expect(
+          VersionRange(max: v200)
+              .difference(VersionConstraint.parse('>=2.0.0-dev')),
+          equals(VersionRange(max: v200)));
+    });
+
+    test('with a range with a pre-release max, returns null', () {
+      expect(
+          VersionRange(max: v200)
+              .difference(VersionConstraint.parse('<2.0.0-dev')),
+          equals(VersionConstraint.empty));
+    });
+
+    group('with includeMaxPreRelease', () {
+      group('for the minuend', () {
+        test('preserves includeMaxPreRelease if the max version is included',
+            () {
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(VersionConstraint.parse('<1.0.0')),
+              equals(VersionRange(
+                  min: Version.parse('1.0.0-0'),
+                  max: v200,
+                  includeMin: true,
+                  alwaysIncludeMaxPreRelease: true)));
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(VersionConstraint.parse('<2.0.0')),
+              equals(VersionRange(
+                  min: v200.firstPreRelease,
+                  max: v200,
+                  includeMin: true,
+                  alwaysIncludeMaxPreRelease: true)));
+          expect(
+              includeMaxPreReleaseRange.difference(includeMaxPreReleaseRange),
+              equals(VersionConstraint.empty));
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(VersionConstraint.parse('<3.0.0')),
+              equals(VersionConstraint.empty));
+        });
+
+        test('with a range with a pre-release min, adjusts the max', () {
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(VersionConstraint.parse('>=2.0.0-dev')),
+              equals(VersionConstraint.parse('<2.0.0-dev')));
+        });
+
+        test('with a range with a pre-release max, adjusts the min', () {
+          expect(
+              includeMaxPreReleaseRange
+                  .difference(VersionConstraint.parse('<2.0.0-dev')),
+              equals(VersionConstraint.parse('>=2.0.0-dev <2.0.0')));
+        });
+      });
+
+      group('for the subtrahend', () {
+        group("doesn't create a pre-release minimum", () {
+          test('when cutting off the bottom', () {
+            expect(
+                VersionConstraint.parse('<3.0.0')
+                    .difference(includeMaxPreReleaseRange),
+                equals(VersionRange(min: v200, max: v300, includeMin: true)));
+          });
+
+          test('with splitting down the middle', () {
+            expect(
+                VersionConstraint.parse('<4.0.0').difference(VersionRange(
+                    min: v200,
+                    max: v300,
+                    includeMin: true,
+                    alwaysIncludeMaxPreRelease: true)),
+                equals(VersionConstraint.unionOf([
+                  VersionRange(max: v200, alwaysIncludeMaxPreRelease: true),
+                  VersionConstraint.parse('>=3.0.0 <4.0.0')
+                ])));
+          });
+
+          test('can leave a single version', () {
+            expect(
+                VersionConstraint.parse('<=2.0.0')
+                    .difference(includeMaxPreReleaseRange),
+                equals(v200));
+          });
+        });
+      });
+    });
+  });
+
+  test('isEmpty', () {
+    expect(VersionRange().isEmpty, isFalse);
+    expect(VersionRange(min: v123, max: v124).isEmpty, isFalse);
+  });
+
+  group('compareTo()', () {
+    test('orders by minimum first', () {
+      _expectComparesSmaller(VersionRange(min: v003, max: v080),
+          VersionRange(min: v010, max: v072));
+      _expectComparesSmaller(VersionRange(min: v003, max: v080),
+          VersionRange(min: v010, max: v080));
+      _expectComparesSmaller(VersionRange(min: v003, max: v080),
+          VersionRange(min: v010, max: v114));
+    });
+
+    test('orders by maximum second', () {
+      _expectComparesSmaller(VersionRange(min: v003, max: v010),
+          VersionRange(min: v003, max: v072));
+    });
+
+    test('includeMin comes before !includeMin', () {
+      _expectComparesSmaller(
+          VersionRange(min: v003, max: v080, includeMin: true),
+          VersionRange(min: v003, max: v080));
+    });
+
+    test('includeMax comes after !includeMax', () {
+      _expectComparesSmaller(VersionRange(min: v003, max: v080),
+          VersionRange(min: v003, max: v080, includeMax: true));
+    });
+
+    test('includeMaxPreRelease comes after !includeMaxPreRelease', () {
+      _expectComparesSmaller(
+          VersionRange(max: v200), includeMaxPreReleaseRange);
+    });
+
+    test('no minimum comes before small minimum', () {
+      _expectComparesSmaller(
+          VersionRange(max: v010), VersionRange(min: v003, max: v010));
+      _expectComparesSmaller(VersionRange(max: v010, includeMin: true),
+          VersionRange(min: v003, max: v010));
+    });
+
+    test('no maximium comes after large maximum', () {
+      _expectComparesSmaller(
+          VersionRange(min: v003, max: v300), VersionRange(min: v003));
+      _expectComparesSmaller(VersionRange(min: v003, max: v300),
+          VersionRange(min: v003, includeMax: true));
+    });
+  });
+}
+
+void _expectComparesSmaller(VersionRange smaller, VersionRange larger) {
+  expect(smaller.compareTo(larger), lessThan(0),
+      reason: 'expected $smaller to sort below $larger');
+  expect(larger.compareTo(smaller), greaterThan(0),
+      reason: 'expected $larger to sort above $smaller');
+}
diff --git a/pkgs/pub_semver/test/version_test.dart b/pkgs/pub_semver/test/version_test.dart
new file mode 100644
index 0000000..d7f1197
--- /dev/null
+++ b/pkgs/pub_semver/test/version_test.dart
@@ -0,0 +1,411 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  test('none', () {
+    expect(Version.none.toString(), equals('0.0.0'));
+  });
+
+  test('prioritize()', () {
+    // A correctly sorted list of versions in order of increasing priority.
+    var versions = [
+      '1.0.0-alpha',
+      '2.0.0-alpha',
+      '1.0.0',
+      '1.0.0+build',
+      '1.0.1',
+      '1.1.0',
+      '2.0.0'
+    ];
+
+    // Ensure that every pair of versions is prioritized in the order that it
+    // appears in the list.
+    for (var i = 0; i < versions.length; i++) {
+      for (var j = 0; j < versions.length; j++) {
+        var a = Version.parse(versions[i]);
+        var b = Version.parse(versions[j]);
+        expect(Version.prioritize(a, b), equals(i.compareTo(j)));
+      }
+    }
+  });
+
+  test('antiprioritize()', () {
+    // A correctly sorted list of versions in order of increasing antipriority.
+    var versions = [
+      '2.0.0-alpha',
+      '1.0.0-alpha',
+      '2.0.0',
+      '1.1.0',
+      '1.0.1',
+      '1.0.0+build',
+      '1.0.0'
+    ];
+
+    // Ensure that every pair of versions is prioritized in the order that it
+    // appears in the list.
+    for (var i = 0; i < versions.length; i++) {
+      for (var j = 0; j < versions.length; j++) {
+        var a = Version.parse(versions[i]);
+        var b = Version.parse(versions[j]);
+        expect(Version.antiprioritize(a, b), equals(i.compareTo(j)));
+      }
+    }
+  });
+
+  group('constructor', () {
+    test('throws on negative numbers', () {
+      expect(() => Version(-1, 1, 1), throwsArgumentError);
+      expect(() => Version(1, -1, 1), throwsArgumentError);
+      expect(() => Version(1, 1, -1), throwsArgumentError);
+    });
+  });
+
+  group('comparison', () {
+    // A correctly sorted list of versions.
+    var versions = [
+      '1.0.0-alpha',
+      '1.0.0-alpha.1',
+      '1.0.0-beta.2',
+      '1.0.0-beta.11',
+      '1.0.0-rc.1',
+      '1.0.0-rc.1+build.1',
+      '1.0.0',
+      '1.0.0+0.3.7',
+      '1.3.7+build',
+      '1.3.7+build.2.b8f12d7',
+      '1.3.7+build.11.e0f985a',
+      '2.0.0',
+      '2.1.0',
+      '2.2.0',
+      '2.11.0',
+      '2.11.1'
+    ];
+
+    test('compareTo()', () {
+      // Ensure that every pair of versions compares in the order that it
+      // appears in the list.
+      for (var i = 0; i < versions.length; i++) {
+        for (var j = 0; j < versions.length; j++) {
+          var a = Version.parse(versions[i]);
+          var b = Version.parse(versions[j]);
+          expect(a.compareTo(b), equals(i.compareTo(j)));
+        }
+      }
+    });
+
+    test('operators', () {
+      for (var i = 0; i < versions.length; i++) {
+        for (var j = 0; j < versions.length; j++) {
+          var a = Version.parse(versions[i]);
+          var b = Version.parse(versions[j]);
+          expect(a < b, equals(i < j));
+          expect(a > b, equals(i > j));
+          expect(a <= b, equals(i <= j));
+          expect(a >= b, equals(i >= j));
+          expect(a == b, equals(i == j));
+          expect(a != b, equals(i != j));
+        }
+      }
+    });
+
+    test('equality', () {
+      expect(Version.parse('01.2.3'), equals(Version.parse('1.2.3')));
+      expect(Version.parse('1.02.3'), equals(Version.parse('1.2.3')));
+      expect(Version.parse('1.2.03'), equals(Version.parse('1.2.3')));
+      expect(Version.parse('1.2.3-01'), equals(Version.parse('1.2.3-1')));
+      expect(Version.parse('1.2.3+01'), equals(Version.parse('1.2.3+1')));
+    });
+  });
+
+  test('allows()', () {
+    expect(v123, allows(v123));
+    expect(
+        v123,
+        doesNotAllow(
+            Version.parse('2.2.3'),
+            Version.parse('1.3.3'),
+            Version.parse('1.2.4'),
+            Version.parse('1.2.3-dev'),
+            Version.parse('1.2.3+build')));
+  });
+
+  test('allowsAll()', () {
+    expect(v123.allowsAll(v123), isTrue);
+    expect(v123.allowsAll(v003), isFalse);
+    expect(v123.allowsAll(VersionRange(min: v114, max: v124)), isFalse);
+    expect(v123.allowsAll(VersionConstraint.any), isFalse);
+    expect(v123.allowsAll(VersionConstraint.empty), isTrue);
+  });
+
+  test('allowsAny()', () {
+    expect(v123.allowsAny(v123), isTrue);
+    expect(v123.allowsAny(v003), isFalse);
+    expect(v123.allowsAny(VersionRange(min: v114, max: v124)), isTrue);
+    expect(v123.allowsAny(VersionConstraint.any), isTrue);
+    expect(v123.allowsAny(VersionConstraint.empty), isFalse);
+  });
+
+  test('intersect()', () {
+    // Intersecting the same version returns the version.
+    expect(v123.intersect(v123), equals(v123));
+
+    // Intersecting a different version allows no versions.
+    expect(v123.intersect(v114).isEmpty, isTrue);
+
+    // Intersecting a range returns the version if the range allows it.
+    expect(v123.intersect(VersionRange(min: v114, max: v124)), equals(v123));
+
+    // Intersecting a range allows no versions if the range doesn't allow it.
+    expect(v114.intersect(VersionRange(min: v123, max: v124)).isEmpty, isTrue);
+  });
+
+  group('union()', () {
+    test('with the same version returns the version', () {
+      expect(v123.union(v123), equals(v123));
+    });
+
+    test('with a different version returns a version that matches both', () {
+      var result = v123.union(v080);
+      expect(result, allows(v123));
+      expect(result, allows(v080));
+
+      // Nothing in between should match.
+      expect(result, doesNotAllow(v114));
+    });
+
+    test('with a range returns the range if it contains the version', () {
+      var range = VersionRange(min: v114, max: v124);
+      expect(v123.union(range), equals(range));
+    });
+
+    test('with a range with the version on the edge, expands the range', () {
+      expect(
+          v124.union(VersionRange(
+              min: v114, max: v124, alwaysIncludeMaxPreRelease: true)),
+          equals(VersionRange(min: v114, max: v124, includeMax: true)));
+      expect(
+          v124.firstPreRelease.union(VersionRange(min: v114, max: v124)),
+          equals(VersionRange(
+              min: v114, max: v124.firstPreRelease, includeMax: true)));
+      expect(v114.union(VersionRange(min: v114, max: v124)),
+          equals(VersionRange(min: v114, max: v124, includeMin: true)));
+    });
+
+    test(
+        'with a range allows both the range and the version if the range '
+        "doesn't contain the version", () {
+      var result = v123.union(VersionRange(min: v003, max: v114));
+      expect(result, allows(v123));
+      expect(result, allows(v010));
+    });
+  });
+
+  group('difference()', () {
+    test('with the same version returns an empty constraint', () {
+      expect(v123.difference(v123), isEmpty);
+    });
+
+    test('with a different version returns the original version', () {
+      expect(v123.difference(v080), equals(v123));
+    });
+
+    test('returns an empty constraint with a range that contains the version',
+        () {
+      expect(v123.difference(VersionRange(min: v114, max: v124)), isEmpty);
+    });
+
+    test("returns the version constraint with a range that doesn't contain it",
+        () {
+      expect(v123.difference(VersionRange(min: v140, max: v300)), equals(v123));
+    });
+  });
+
+  test('isEmpty', () {
+    expect(v123.isEmpty, isFalse);
+  });
+
+  test('nextMajor', () {
+    expect(v123.nextMajor, equals(v200));
+    expect(v114.nextMajor, equals(v200));
+    expect(v200.nextMajor, equals(v300));
+
+    // Ignores pre-release if not on a major version.
+    expect(Version.parse('1.2.3-dev').nextMajor, equals(v200));
+
+    // Just removes it if on a major version.
+    expect(Version.parse('2.0.0-dev').nextMajor, equals(v200));
+
+    // Strips build suffix.
+    expect(Version.parse('1.2.3+patch').nextMajor, equals(v200));
+  });
+
+  test('nextMinor', () {
+    expect(v123.nextMinor, equals(v130));
+    expect(v130.nextMinor, equals(v140));
+
+    // Ignores pre-release if not on a minor version.
+    expect(Version.parse('1.2.3-dev').nextMinor, equals(v130));
+
+    // Just removes it if on a minor version.
+    expect(Version.parse('1.3.0-dev').nextMinor, equals(v130));
+
+    // Strips build suffix.
+    expect(Version.parse('1.2.3+patch').nextMinor, equals(v130));
+  });
+
+  test('nextPatch', () {
+    expect(v123.nextPatch, equals(v124));
+    expect(v200.nextPatch, equals(v201));
+
+    // Just removes pre-release version if present.
+    expect(Version.parse('1.2.4-dev').nextPatch, equals(v124));
+
+    // Strips build suffix.
+    expect(Version.parse('1.2.3+patch').nextPatch, equals(v124));
+  });
+
+  test('nextBreaking', () {
+    expect(v123.nextBreaking, equals(v200));
+    expect(v072.nextBreaking, equals(v080));
+    expect(v003.nextBreaking, equals(v010));
+
+    // Removes pre-release version if present.
+    expect(Version.parse('1.2.3-dev').nextBreaking, equals(v200));
+
+    // Strips build suffix.
+    expect(Version.parse('1.2.3+patch').nextBreaking, equals(v200));
+  });
+
+  test('parse()', () {
+    expect(Version.parse('0.0.0'), equals(Version(0, 0, 0)));
+    expect(Version.parse('12.34.56'), equals(Version(12, 34, 56)));
+
+    expect(Version.parse('1.2.3-alpha.1'),
+        equals(Version(1, 2, 3, pre: 'alpha.1')));
+    expect(Version.parse('1.2.3-x.7.z-92'),
+        equals(Version(1, 2, 3, pre: 'x.7.z-92')));
+
+    expect(Version.parse('1.2.3+build.1'),
+        equals(Version(1, 2, 3, build: 'build.1')));
+    expect(Version.parse('1.2.3+x.7.z-92'),
+        equals(Version(1, 2, 3, build: 'x.7.z-92')));
+
+    expect(Version.parse('1.0.0-rc-1+build-1'),
+        equals(Version(1, 0, 0, pre: 'rc-1', build: 'build-1')));
+
+    expect(() => Version.parse('1.0'), throwsFormatException);
+    expect(() => Version.parse('1a2b3'), throwsFormatException);
+    expect(() => Version.parse('1.2.3.4'), throwsFormatException);
+    expect(() => Version.parse('1234'), throwsFormatException);
+    expect(() => Version.parse('-2.3.4'), throwsFormatException);
+    expect(() => Version.parse('1.3-pre'), throwsFormatException);
+    expect(() => Version.parse('1.3+build'), throwsFormatException);
+    expect(() => Version.parse('1.3+bu?!3ild'), throwsFormatException);
+  });
+
+  group('toString()', () {
+    test('returns the version string', () {
+      expect(Version(0, 0, 0).toString(), equals('0.0.0'));
+      expect(Version(12, 34, 56).toString(), equals('12.34.56'));
+
+      expect(
+          Version(1, 2, 3, pre: 'alpha.1').toString(), equals('1.2.3-alpha.1'));
+      expect(Version(1, 2, 3, pre: 'x.7.z-92').toString(),
+          equals('1.2.3-x.7.z-92'));
+
+      expect(Version(1, 2, 3, build: 'build.1').toString(),
+          equals('1.2.3+build.1'));
+      expect(Version(1, 2, 3, pre: 'pre', build: 'bui').toString(),
+          equals('1.2.3-pre+bui'));
+    });
+
+    test('preserves leading zeroes', () {
+      expect(Version.parse('001.02.0003-01.dev+pre.002').toString(),
+          equals('001.02.0003-01.dev+pre.002'));
+    });
+  });
+
+  group('canonicalizedVersion', () {
+    test('returns version string', () {
+      expect(Version(0, 0, 0).canonicalizedVersion, equals('0.0.0'));
+      expect(Version(12, 34, 56).canonicalizedVersion, equals('12.34.56'));
+
+      expect(Version(1, 2, 3, pre: 'alpha.1').canonicalizedVersion,
+          equals('1.2.3-alpha.1'));
+      expect(Version(1, 2, 3, pre: 'x.7.z-92').canonicalizedVersion,
+          equals('1.2.3-x.7.z-92'));
+
+      expect(Version(1, 2, 3, build: 'build.1').canonicalizedVersion,
+          equals('1.2.3+build.1'));
+      expect(Version(1, 2, 3, pre: 'pre', build: 'bui').canonicalizedVersion,
+          equals('1.2.3-pre+bui'));
+    });
+
+    test('discards leading zeroes', () {
+      expect(Version.parse('001.02.0003-01.dev+pre.002').canonicalizedVersion,
+          equals('1.2.3-1.dev+pre.2'));
+    });
+
+    test('example from documentation', () {
+      final v = Version.parse('01.02.03-01.dev+pre.02');
+
+      assert(v.toString() == '01.02.03-01.dev+pre.02');
+      assert(v.canonicalizedVersion == '1.2.3-1.dev+pre.2');
+      assert(Version.parse(v.canonicalizedVersion) == v);
+    });
+  });
+
+  group('primary', () {
+    test('single', () {
+      expect(
+        _primary([
+          '1.2.3',
+        ]).toString(),
+        '1.2.3',
+      );
+    });
+
+    test('normal', () {
+      expect(
+        _primary([
+          '1.2.3',
+          '1.2.2',
+        ]).toString(),
+        '1.2.3',
+      );
+    });
+
+    test('all prerelease', () {
+      expect(
+        _primary([
+          '1.2.2-dev.1',
+          '1.2.2-dev.2',
+        ]).toString(),
+        '1.2.2-dev.2',
+      );
+    });
+
+    test('later prerelease', () {
+      expect(
+        _primary([
+          '1.2.3',
+          '1.2.3-dev',
+        ]).toString(),
+        '1.2.3',
+      );
+    });
+
+    test('empty', () {
+      expect(() => Version.primary([]), throwsStateError);
+    });
+  });
+}
+
+Version _primary(List<String> input) =>
+    Version.primary(input.map(Version.parse).toList());
diff --git a/pkgs/pub_semver/test/version_union_test.dart b/pkgs/pub_semver/test/version_union_test.dart
new file mode 100644
index 0000000..857f10e
--- /dev/null
+++ b/pkgs/pub_semver/test/version_union_test.dart
@@ -0,0 +1,482 @@
+// 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:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  group('factory', () {
+    test('ignores empty constraints', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionConstraint.empty,
+            VersionConstraint.empty,
+            v123,
+            VersionConstraint.empty
+          ]),
+          equals(v123));
+
+      expect(
+          VersionConstraint.unionOf(
+              [VersionConstraint.empty, VersionConstraint.empty]),
+          isEmpty);
+    });
+
+    test('returns an empty constraint for an empty list', () {
+      expect(VersionConstraint.unionOf([]), isEmpty);
+    });
+
+    test('any constraints override everything', () {
+      expect(
+          VersionConstraint.unionOf([
+            v123,
+            VersionConstraint.any,
+            v200,
+            VersionRange(min: v234, max: v250)
+          ]),
+          equals(VersionConstraint.any));
+    });
+
+    test('flattens other unions', () {
+      expect(
+          VersionConstraint.unionOf([
+            v072,
+            VersionConstraint.unionOf([v123, v124]),
+            v250
+          ]),
+          equals(VersionConstraint.unionOf([v072, v123, v124, v250])));
+    });
+
+    test('returns a single merged range as-is', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v080, max: v140),
+            VersionRange(min: v123, max: v200)
+          ]),
+          equals(VersionRange(min: v080, max: v200)));
+    });
+  });
+
+  group('equality', () {
+    test("doesn't depend on original order", () {
+      expect(
+          VersionConstraint.unionOf([
+            v250,
+            VersionRange(min: v201, max: v234),
+            v124,
+            v072,
+            VersionRange(min: v080, max: v114),
+            v123
+          ]),
+          equals(VersionConstraint.unionOf([
+            v072,
+            VersionRange(min: v080, max: v114),
+            v123,
+            v124,
+            VersionRange(min: v201, max: v234),
+            v250
+          ])));
+    });
+
+    test('merges overlapping ranges', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v072),
+            VersionRange(min: v010, max: v080),
+            VersionRange(min: v114, max: v124),
+            VersionRange(min: v123, max: v130)
+          ]),
+          equals(VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v080),
+            VersionRange(min: v114, max: v130)
+          ])));
+    });
+
+    test('merges adjacent ranges', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v072, includeMax: true),
+            VersionRange(min: v072, max: v080),
+            VersionRange(
+                min: v114, max: v124, alwaysIncludeMaxPreRelease: true),
+            VersionRange(min: v124, max: v130, includeMin: true),
+            VersionRange(min: v130.firstPreRelease, max: v200, includeMin: true)
+          ]),
+          equals(VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v080),
+            VersionRange(min: v114, max: v200)
+          ])));
+    });
+
+    test("doesn't merge not-quite-adjacent ranges", () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v114, max: v124),
+            VersionRange(min: v124, max: v130, includeMin: true)
+          ]),
+          isNot(equals(VersionRange(min: v114, max: v130))));
+
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v072),
+            VersionRange(min: v072, max: v080)
+          ]),
+          isNot(equals(VersionRange(min: v003, max: v080))));
+    });
+
+    test('merges version numbers into ranges', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v072),
+            v010,
+            VersionRange(min: v114, max: v124),
+            v123
+          ]),
+          equals(VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v072),
+            VersionRange(min: v114, max: v124)
+          ])));
+    });
+
+    test('merges adjacent version numbers into ranges', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(
+                min: v003, max: v072, alwaysIncludeMaxPreRelease: true),
+            v072,
+            v114,
+            VersionRange(min: v114, max: v124),
+            v124.firstPreRelease
+          ]),
+          equals(VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v072, includeMax: true),
+            VersionRange(
+                min: v114,
+                max: v124.firstPreRelease,
+                includeMin: true,
+                includeMax: true)
+          ])));
+    });
+
+    test("doesn't merge not-quite-adjacent version numbers into ranges", () {
+      expect(
+          VersionConstraint.unionOf([VersionRange(min: v003, max: v072), v072]),
+          isNot(equals(VersionRange(min: v003, max: v072, includeMax: true))));
+    });
+  });
+
+  test('isEmpty returns false', () {
+    expect(
+        VersionConstraint.unionOf([
+          VersionRange(min: v003, max: v080),
+          VersionRange(min: v123, max: v130),
+        ]),
+        isNot(isEmpty));
+  });
+
+  test('isAny returns false', () {
+    expect(
+        VersionConstraint.unionOf([
+          VersionRange(min: v003, max: v080),
+          VersionRange(min: v123, max: v130),
+        ]).isAny,
+        isFalse);
+  });
+
+  test('allows() allows anything the components allow', () {
+    var union = VersionConstraint.unionOf([
+      VersionRange(min: v003, max: v080),
+      VersionRange(min: v123, max: v130),
+      v200
+    ]);
+
+    expect(union, allows(v010));
+    expect(union, doesNotAllow(v080));
+    expect(union, allows(v124));
+    expect(union, doesNotAllow(v140));
+    expect(union, allows(v200));
+  });
+
+  group('allowsAll()', () {
+    test('for a version, returns true if any component allows the version', () {
+      var union = VersionConstraint.unionOf([
+        VersionRange(min: v003, max: v080),
+        VersionRange(min: v123, max: v130),
+        v200
+      ]);
+
+      expect(union.allowsAll(v010), isTrue);
+      expect(union.allowsAll(v080), isFalse);
+      expect(union.allowsAll(v124), isTrue);
+      expect(union.allowsAll(v140), isFalse);
+      expect(union.allowsAll(v200), isTrue);
+    });
+
+    test(
+        'for a version range, returns true if any component allows the whole '
+        'range', () {
+      var union = VersionConstraint.unionOf([
+        VersionRange(min: v003, max: v080),
+        VersionRange(min: v123, max: v130)
+      ]);
+
+      expect(union.allowsAll(VersionRange(min: v003, max: v080)), isTrue);
+      expect(union.allowsAll(VersionRange(min: v010, max: v072)), isTrue);
+      expect(union.allowsAll(VersionRange(min: v010, max: v124)), isFalse);
+    });
+
+    group('for a union,', () {
+      var union = VersionConstraint.unionOf([
+        VersionRange(min: v003, max: v080),
+        VersionRange(min: v123, max: v130)
+      ]);
+
+      test('returns true if every constraint matches a different constraint',
+          () {
+        expect(
+            union.allowsAll(VersionConstraint.unionOf([
+              VersionRange(min: v010, max: v072),
+              VersionRange(min: v124, max: v130)
+            ])),
+            isTrue);
+      });
+
+      test('returns true if every constraint matches the same constraint', () {
+        expect(
+            union.allowsAll(VersionConstraint.unionOf([
+              VersionRange(min: v003, max: v010),
+              VersionRange(min: v072, max: v080)
+            ])),
+            isTrue);
+      });
+
+      test("returns false if there's an unmatched constraint", () {
+        expect(
+            union.allowsAll(VersionConstraint.unionOf([
+              VersionRange(min: v010, max: v072),
+              VersionRange(min: v124, max: v130),
+              VersionRange(min: v140, max: v200)
+            ])),
+            isFalse);
+      });
+
+      test("returns false if a constraint isn't fully matched", () {
+        expect(
+            union.allowsAll(VersionConstraint.unionOf([
+              VersionRange(min: v010, max: v114),
+              VersionRange(min: v124, max: v130)
+            ])),
+            isFalse);
+      });
+    });
+  });
+
+  group('allowsAny()', () {
+    test('for a version, returns true if any component allows the version', () {
+      var union = VersionConstraint.unionOf([
+        VersionRange(min: v003, max: v080),
+        VersionRange(min: v123, max: v130),
+        v200
+      ]);
+
+      expect(union.allowsAny(v010), isTrue);
+      expect(union.allowsAny(v080), isFalse);
+      expect(union.allowsAny(v124), isTrue);
+      expect(union.allowsAny(v140), isFalse);
+      expect(union.allowsAny(v200), isTrue);
+    });
+
+    test(
+        'for a version range, returns true if any component allows part of '
+        'the range', () {
+      var union =
+          VersionConstraint.unionOf([VersionRange(min: v003, max: v080), v123]);
+
+      expect(union.allowsAny(VersionRange(min: v010, max: v114)), isTrue);
+      expect(union.allowsAny(VersionRange(min: v114, max: v124)), isTrue);
+      expect(union.allowsAny(VersionRange(min: v124, max: v130)), isFalse);
+    });
+
+    group('for a union,', () {
+      var union = VersionConstraint.unionOf([
+        VersionRange(min: v010, max: v080),
+        VersionRange(min: v123, max: v130)
+      ]);
+
+      test('returns true if any constraint matches', () {
+        expect(
+            union.allowsAny(VersionConstraint.unionOf(
+                [v072, VersionRange(min: v200, max: v300)])),
+            isTrue);
+
+        expect(
+            union.allowsAny(VersionConstraint.unionOf(
+                [v003, VersionRange(min: v124, max: v300)])),
+            isTrue);
+      });
+
+      test('returns false if no constraint matches', () {
+        expect(
+            union.allowsAny(VersionConstraint.unionOf([
+              v003,
+              VersionRange(min: v130, max: v140),
+              VersionRange(min: v140, max: v200)
+            ])),
+            isFalse);
+      });
+    });
+  });
+
+  group('intersect()', () {
+    test('with an overlapping version, returns that version', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v010, max: v080),
+            VersionRange(min: v123, max: v140)
+          ]).intersect(v072),
+          equals(v072));
+    });
+
+    test('with a non-overlapping version, returns an empty constraint', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v010, max: v080),
+            VersionRange(min: v123, max: v140)
+          ]).intersect(v300),
+          isEmpty);
+    });
+
+    test('with an overlapping range, returns that range', () {
+      var range = VersionRange(min: v072, max: v080);
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v010, max: v080),
+            VersionRange(min: v123, max: v140)
+          ]).intersect(range),
+          equals(range));
+    });
+
+    test('with a non-overlapping range, returns an empty constraint', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v010, max: v080),
+            VersionRange(min: v123, max: v140)
+          ]).intersect(VersionRange(min: v080, max: v123)),
+          isEmpty);
+    });
+
+    test('with a parially-overlapping range, returns the overlapping parts',
+        () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v010, max: v080),
+            VersionRange(min: v123, max: v140)
+          ]).intersect(VersionRange(min: v072, max: v130)),
+          equals(VersionConstraint.unionOf([
+            VersionRange(min: v072, max: v080),
+            VersionRange(min: v123, max: v130)
+          ])));
+    });
+
+    group('for a union,', () {
+      var union = VersionConstraint.unionOf([
+        VersionRange(min: v003, max: v080),
+        VersionRange(min: v123, max: v130)
+      ]);
+
+      test('returns the overlapping parts', () {
+        expect(
+            union.intersect(VersionConstraint.unionOf([
+              v010,
+              VersionRange(min: v072, max: v124),
+              VersionRange(min: v124, max: v130)
+            ])),
+            equals(VersionConstraint.unionOf([
+              v010,
+              VersionRange(min: v072, max: v080),
+              VersionRange(min: v123, max: v124),
+              VersionRange(min: v124, max: v130)
+            ])));
+      });
+
+      test("drops parts that don't match", () {
+        expect(
+            union.intersect(VersionConstraint.unionOf([
+              v003,
+              VersionRange(min: v072, max: v080),
+              VersionRange(min: v080, max: v123)
+            ])),
+            equals(VersionRange(min: v072, max: v080)));
+      });
+    });
+  });
+
+  group('difference()', () {
+    test("ignores ranges that don't intersect", () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v072, max: v080),
+            VersionRange(min: v123, max: v130)
+          ]).difference(VersionConstraint.unionOf([
+            VersionRange(min: v003, max: v010),
+            VersionRange(min: v080, max: v123),
+            VersionRange(min: v140)
+          ])),
+          equals(VersionConstraint.unionOf([
+            VersionRange(min: v072, max: v080),
+            VersionRange(min: v123, max: v130)
+          ])));
+    });
+
+    test('removes overlapping portions', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v010, max: v080),
+            VersionRange(min: v123, max: v130)
+          ]).difference(VersionConstraint.unionOf(
+              [VersionRange(min: v003, max: v072), VersionRange(min: v124)])),
+          equals(VersionConstraint.unionOf([
+            VersionRange(
+                min: v072.firstPreRelease, max: v080, includeMin: true),
+            VersionRange(min: v123, max: v124, includeMax: true)
+          ])));
+    });
+
+    test('removes multiple portions from the same range', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v010, max: v114),
+            VersionRange(min: v130, max: v200)
+          ]).difference(VersionConstraint.unionOf([v072, v080])),
+          equals(VersionConstraint.unionOf([
+            VersionRange(
+                min: v010, max: v072, alwaysIncludeMaxPreRelease: true),
+            VersionRange(
+                min: v072, max: v080, alwaysIncludeMaxPreRelease: true),
+            VersionRange(min: v080, max: v114),
+            VersionRange(min: v130, max: v200)
+          ])));
+    });
+
+    test('removes the same range from multiple ranges', () {
+      expect(
+          VersionConstraint.unionOf([
+            VersionRange(min: v010, max: v072),
+            VersionRange(min: v080, max: v123),
+            VersionRange(min: v124, max: v130),
+            VersionRange(min: v200, max: v234),
+            VersionRange(min: v250, max: v300)
+          ]).difference(VersionRange(min: v114, max: v201)),
+          equals(VersionConstraint.unionOf([
+            VersionRange(min: v010, max: v072),
+            VersionRange(min: v080, max: v114, includeMax: true),
+            VersionRange(
+                min: v201.firstPreRelease, max: v234, includeMin: true),
+            VersionRange(min: v250, max: v300)
+          ])));
+    });
+  });
+}