Merge `package:markdown` (#1208)

- [x] Move and fix workflow files, labeler.yaml, and badges in the
README.md
- [x] Rev the version of the package, so that pub.dev points to the
correct site
- [x] Add a line to the changelog:
```
* Move to `dart-lang/tools` monorepo.
```

- [x] Add the package to the top-level readme of the monorepo:
```
| [markdown](pkgs/markdown/) | A portable Markdown library written in Dart that can parse Markdown into HTML. | [![pub package](https://img.shields.io/pub/v/markdown.svg)](https://pub.dev/packages/markdown) |
```

- [ ] **Important!** Merge the PR with 'Create a merge commit' (enabling
then disabling the `Allow merge commits` admin setting)
- [x] Update the auto-publishing settings on
https://pub.dev/packages/markdown/admin
- [x] Add the following text to https://github.com/dart-lang/markdown/:'

```
> [!IMPORTANT]  
> This repo has moved to https://github.com/dart-lang/tools/tree/main/pkgs/markdown
```

- [ ] Publish using the autopublish workflow
- [ ] Push tags to GitHub using
```git tag --list 'markdown*' | xargs git push origin```

- [x] Close open PRs in dart-lang/markdown with the following message:

```
Closing as the
[dart-lang/markdown](https://github.com/dart-lang/markdown) repository
is merged into the [dart-lang/tools](https://github.com/dart-lang/tools)
monorepo. Please re-open this PR there!
```
      
- [x] Transfer issues by running
```dart run pkgs/repo_manage/bin/report.dart transfer-issues
--source-repo dart-lang/markdown --target-repo dart-lang/tools
--add-label package:markdown --apply-changes```

- [ ] Archive https://github.com/dart-lang/markdown/


---

- [x] I’ve reviewed the contributor guide and applied the relevant
portions to this PR.

<details>
  <summary>Contribution guidelines:</summary><br>

- See our [contributor
guide](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md)
for general expectations for PRs.
- Larger or significant changes should be discussed in an issue before
creating a PR.
- Contributions to our repos should follow the [Dart style
guide](https://dart.dev/guides/language/effective-dart) and use `dart
format`.
- Most changes should add an entry to the changelog and may need to [rev
the pubspec package
version](https://github.com/dart-lang/sdk/blob/main/docs/External-Package-Maintenance.md#making-a-change).
- Changes to packages require [corresponding
tests](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md#Testing).

Note that many Dart repos have a weekly cadence for reviewing PRs -
please allow for some latency before initial review feedback.
</details>
diff --git a/.github/ISSUE_TEMPLATE/io.md b/.github/ISSUE_TEMPLATE/io.md
new file mode 100644
index 0000000..5646f0f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/io.md
@@ -0,0 +1,5 @@
+---
+name: "package:io"
+about: "Create a bug or file a feature request against package:io."
+labels: "package:io"
+---
\ No newline at end of file
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/ISSUE_TEMPLATE/pubspec_parse.md b/.github/ISSUE_TEMPLATE/pubspec_parse.md
new file mode 100644
index 0000000..2d65881
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/pubspec_parse.md
@@ -0,0 +1,5 @@
+---
+name: "package:pubspec_parse"
+about: "Create a bug or file a feature request against package:pubspec_parse."
+labels: "package:pubspec_parse"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/source_maps.md b/.github/ISSUE_TEMPLATE/source_maps.md
new file mode 100644
index 0000000..a1e390a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/source_maps.md
@@ -0,0 +1,5 @@
+---
+name: "package:source_maps"
+about: "Create a bug or file a feature request against package:source_maps."
+labels: "package:source_maps"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/source_span.md b/.github/ISSUE_TEMPLATE/source_span.md
new file mode 100644
index 0000000..7dbb3c4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/source_span.md
@@ -0,0 +1,5 @@
+---
+name: "package:source_span"
+about: "Create a bug or file a feature request against package:source_span."
+labels: "package:source_span"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/sse.md b/.github/ISSUE_TEMPLATE/sse.md
new file mode 100644
index 0000000..17cc488
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/sse.md
@@ -0,0 +1,5 @@
+---
+name: "package:sse"
+about: "Create a bug or file a feature request against package:sse."
+labels: "package:sse"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/stack_trace.md b/.github/ISSUE_TEMPLATE/stack_trace.md
new file mode 100644
index 0000000..417362b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/stack_trace.md
@@ -0,0 +1,5 @@
+---
+name: "package:stack_trace"
+about: "Create a bug or file a feature request against package:stack_trace."
+labels: "package:stack_trace"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/stream_channel.md b/.github/ISSUE_TEMPLATE/stream_channel.md
new file mode 100644
index 0000000..76b5994
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/stream_channel.md
@@ -0,0 +1,5 @@
+---
+name: "package:stream_channel"
+about: "Create a bug or file a feature request against package:stream_channel."
+labels: "package:stream_channel"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/stream_transform.md b/.github/ISSUE_TEMPLATE/stream_transform.md
new file mode 100644
index 0000000..475bd83
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/stream_transform.md
@@ -0,0 +1,5 @@
+---
+name: "package:stream_transform"
+about: "Create a bug or file a feature request against package:stream_transform."
+labels: "package:stream_transform"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/string_scanner.md b/.github/ISSUE_TEMPLATE/string_scanner.md
new file mode 100644
index 0000000..ad89f1b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/string_scanner.md
@@ -0,0 +1,5 @@
+---
+name: "package:string_scanner"
+about: "Create a bug or file a feature request against package:string_scanner."
+labels: "package:string_scanner"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/term_glyph.md b/.github/ISSUE_TEMPLATE/term_glyph.md
new file mode 100644
index 0000000..b6a4766
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/term_glyph.md
@@ -0,0 +1,5 @@
+---
+name: "package:term_glyph"
+about: "Create a bug or file a feature request against package:term_glyph."
+labels: "package:term_glyph"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/test_reflective_loader.md b/.github/ISSUE_TEMPLATE/test_reflective_loader.md
new file mode 100644
index 0000000..bde03fe
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/test_reflective_loader.md
@@ -0,0 +1,5 @@
+---
+name: "package:test_reflective_loader"
+about: "Create a bug or file a feature request against package:test_reflective_loader."
+labels: "package:test_reflective_loader"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/timing.md b/.github/ISSUE_TEMPLATE/timing.md
new file mode 100644
index 0000000..38a0015
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/timing.md
@@ -0,0 +1,5 @@
+---
+name: "package:timing"
+about: "Create a bug or file a feature request against package:timing."
+labels: "package:timing"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/watcher.md b/.github/ISSUE_TEMPLATE/watcher.md
new file mode 100644
index 0000000..2578819
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/watcher.md
@@ -0,0 +1,5 @@
+---
+name: "package:watcher"
+about: "Create a bug or file a feature request against package:watcher."
+labels: "package:watcher"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/yaml.md b/.github/ISSUE_TEMPLATE/yaml.md
new file mode 100644
index 0000000..d6a7c7f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/yaml.md
@@ -0,0 +1,5 @@
+---
+name: "package:yaml"
+about: "Create a bug or file a feature request against package:yaml."
+labels: "package:yaml"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/yaml_edit.md b/.github/ISSUE_TEMPLATE/yaml_edit.md
new file mode 100644
index 0000000..d1122a9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/yaml_edit.md
@@ -0,0 +1,5 @@
+---
+name: "package:yaml_edit"
+about: "Create a bug or file a feature request against package:yaml_edit."
+labels: "package:yaml_edit"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index e158737..0bb7feb 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -64,6 +64,10 @@
   - changed-files:
     - any-glob-to-any-file: 'pkgs/html/**'
 
+'package:io':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/io/**'
+
 'package:json_rpc_2':
   - changed-files:
     - any-glob-to-any-file: 'pkgs/json_rpc_2/**'
@@ -80,10 +84,74 @@
   - 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:pubspec_parse':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/pubspec_parse/**'
+
 'package:source_map_stack_trace':
   - changed-files:
     - any-glob-to-any-file: 'pkgs/source_map_stack_trace/**'
 
+'package:source_maps':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/source_maps/**'
+
+'package:source_span':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/source_span/**'
+
+'package:sse':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/sse/**'
+
+'package:stack_trace':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/stack_trace/**'
+
+'package:stream_channel':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/stream_channel/**'
+
+'package:stream_transform':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/stream_transform/**'
+
+'package:term_glyph':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/term_glyph/**'
+
+'package:test_reflective_loader':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/test_reflective_loader/**'
+
+'package:timing':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/timing/**'
+
 'package:unified_analytics':
   - changed-files:
     - any-glob-to-any-file: 'pkgs/unified_analytics/**'
+
+'package:watcher':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/watcher/**'
+
+'package:yaml':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/yaml/**'
+
+'package:yaml_edit':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/yaml_edit/**'
diff --git a/.github/workflows/bazel_worker.yaml b/.github/workflows/bazel_worker.yaml
index 0eb06da..b448219 100644
--- a/.github/workflows/bazel_worker.yaml
+++ b/.github/workflows/bazel_worker.yaml
@@ -36,6 +36,8 @@
       - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
         with:
           sdk: ${{ matrix.sdk }}
+      - run: dart pub get
       - run: "dart format --output=none --set-exit-if-changed ."
+        if: ${{ matrix.sdk == 'dev' }}
       - name: Test
         run: ./tool/travis.sh
diff --git a/.github/workflows/io.yaml b/.github/workflows/io.yaml
new file mode 100644
index 0000000..0c719a6
--- /dev/null
+++ b/.github/workflows/io.yaml
@@ -0,0 +1,72 @@
+name: package:io
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/io.yml'
+      - 'pkgs/io/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/io.yml'
+      - 'pkgs/io/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/io/
+
+
+jobs:
+  # Check code formatting and static analysis on a single OS (linux)
+  # against Dart dev and stable.
+  analyze:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev, 3.4]
+    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, stable
+  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: [dev, 3.4]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - run: dart test
+        if: always() && steps.install.outcome == 'success'
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/.github/workflows/pubspec_parse.yaml b/.github/workflows/pubspec_parse.yaml
new file mode 100644
index 0000000..9cf6257
--- /dev/null
+++ b/.github/workflows/pubspec_parse.yaml
@@ -0,0 +1,71 @@
+name: package:pubspec_parse
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/pubspec_parse.yaml'
+      - 'pkgs/pubspec_parse/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/pubspec_parse.yaml'
+      - 'pkgs/pubspec_parse/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/pubspec_parse/
+
+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]
+        sdk: [3.6, 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 --run-skipped
+        if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/source_maps.yaml b/.github/workflows/source_maps.yaml
new file mode 100644
index 0000000..2ae0f20
--- /dev/null
+++ b/.github/workflows/source_maps.yaml
@@ -0,0 +1,72 @@
+name: package:source_maps
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/source_maps.yaml'
+      - 'pkgs/source_maps/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/source_maps.yaml'
+      - 'pkgs/source_maps/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/source_maps/
+
+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.3.0, 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'
diff --git a/.github/workflows/source_span.yaml b/.github/workflows/source_span.yaml
new file mode 100644
index 0000000..2c2ba05
--- /dev/null
+++ b/.github/workflows/source_span.yaml
@@ -0,0 +1,75 @@
+name: package:source_span
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/source_span.yml'
+      - 'pkgs/source_span/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/source_span.yml'
+      - 'pkgs/source_span/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/source_span/
+
+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.1.0, 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'
diff --git a/.github/workflows/sse.yaml b/.github/workflows/sse.yaml
new file mode 100644
index 0000000..9e2f212
--- /dev/null
+++ b/.github/workflows/sse.yaml
@@ -0,0 +1,73 @@
+name: package:sse
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/sse.yaml'
+      - 'pkgs/sse/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/sse.yaml'
+      - 'pkgs/sse/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/sse/
+
+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.3, dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - uses: nanasess/setup-chromedriver@42cc2998329f041de87dc3cfa33a930eacd57eaa
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Run VM tests
+        run: dart test --platform vm --test-randomize-ordering-seed=random -j 1
+        if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/stack_trace.yaml b/.github/workflows/stack_trace.yaml
new file mode 100644
index 0000000..7435967
--- /dev/null
+++ b/.github/workflows/stack_trace.yaml
@@ -0,0 +1,75 @@
+name: package:stack_trace
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/stack_trace.yaml'
+      - 'pkgs/stack_trace/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/stack_trace.yaml'
+      - 'pkgs/stack_trace/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/stack_trace/
+
+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 browser tests
+        run: dart test --platform chrome
+        if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/stream_channel.yaml b/.github/workflows/stream_channel.yaml
new file mode 100644
index 0000000..c39424d
--- /dev/null
+++ b/.github/workflows/stream_channel.yaml
@@ -0,0 +1,74 @@
+name: package:stream_channel
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/stream_channel.yaml'
+      - 'pkgs/stream_channel/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/stream_channel.yaml'
+      - 'pkgs/stream_channel/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+defaults:
+  run:
+    working-directory: pkgs/stream_channel/
+
+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.3, 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'
diff --git a/.github/workflows/stream_transform.yaml b/.github/workflows/stream_transform.yaml
new file mode 100644
index 0000000..38be5cc
--- /dev/null
+++ b/.github/workflows/stream_transform.yaml
@@ -0,0 +1,73 @@
+name: package:stream_transform
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/stream_transform.yaml'
+      - 'pkgs/stream_transform/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/stream_transform.yaml'
+      - 'pkgs/stream_transform/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/stream_transform/
+
+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]
+        # Bump SDK for Legacy tests when changing min SDK.
+        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 --test-randomize-ordering-seed=random
+        if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/term_glyph.yaml b/.github/workflows/term_glyph.yaml
new file mode 100644
index 0000000..5b3b320
--- /dev/null
+++ b/.github/workflows/term_glyph.yaml
@@ -0,0 +1,72 @@
+name: package:term_glyph
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/term_glyph.yaml'
+      - 'pkgs/term_glyph/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/term_glyph.yaml'
+      - 'pkgs/term_glyph/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/term_glyph/
+
+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.1, 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'
diff --git a/.github/workflows/test_reflective_loader.yaml b/.github/workflows/test_reflective_loader.yaml
new file mode 100644
index 0000000..975c970
--- /dev/null
+++ b/.github/workflows/test_reflective_loader.yaml
@@ -0,0 +1,43 @@
+name: package:test_reflective_loader
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/test_reflective_loader.yaml'
+      - 'pkgs/test_reflective_loader/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/test_reflective_loader.yaml'
+      - 'pkgs/test_reflective_loader/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+defaults:
+  run:
+    working-directory: pkgs/test_reflective_loader/
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev, 3.1]
+
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+          
+      - run: dart pub get
+      - name: dart format
+        run: dart format --output=none --set-exit-if-changed .
+      - run: dart analyze --fatal-infos
+      - run: dart test
diff --git a/.github/workflows/timing.yaml b/.github/workflows/timing.yaml
new file mode 100644
index 0000000..df77b13
--- /dev/null
+++ b/.github/workflows/timing.yaml
@@ -0,0 +1,67 @@
+name: package:timing
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/timing.yaml'
+      - 'pkgs/timing/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/timing.yaml'
+      - 'pkgs/timing/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/timing/
+
+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: [3.4, dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        run: dart pub get
+      - run: dart format --output=none --set-exit-if-changed .
+        if: always() && steps.install.outcome == 'success'
+      - 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, 2.2.0
+  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
+        run: dart pub get
+      - run: dart test --platform vm
+        if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/watcher.yaml b/.github/workflows/watcher.yaml
new file mode 100644
index 0000000..a04483c
--- /dev/null
+++ b/.github/workflows/watcher.yaml
@@ -0,0 +1,71 @@
+name: package:watcher
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/watcher.yaml'
+      - 'pkgs/watcher/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/watcher.yaml'
+      - 'pkgs/watcher/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/watcher/
+
+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, macos-latest, windows-latest]
+        sdk: [3.1, 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'
diff --git a/.github/workflows/yaml.yaml b/.github/workflows/yaml.yaml
new file mode 100644
index 0000000..735461e
--- /dev/null
+++ b/.github/workflows/yaml.yaml
@@ -0,0 +1,75 @@
+name: package:yaml
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/yaml.yaml'
+      - 'pkgs/yaml/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/yaml.yaml'
+      - 'pkgs/yaml/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/yaml/
+
+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'
diff --git a/.github/workflows/yaml_edit.yaml b/.github/workflows/yaml_edit.yaml
new file mode 100644
index 0000000..ffea62c
--- /dev/null
+++ b/.github/workflows/yaml_edit.yaml
@@ -0,0 +1,91 @@
+name: package:yaml_edit
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/yaml_edit.yaml'
+      - 'pkgs/yaml_edit/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/yaml_edit.yaml'
+      - 'pkgs/yaml_edit/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/yaml_edit/
+
+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.1', stable, dev]
+        platform: [vm, chrome]
+    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 on ${{ matrix.platform }}
+        run: dart test --platform ${{ matrix.platform }} --coverage=./coverage
+        if: always() && steps.install.outcome == 'success'
+      # We don't collect code coverage from 2.12.0, because it doesn't work
+      - name: Convert coverage to lcov
+        run: dart run coverage:format_coverage -i ./coverage -o ./coverage/lcov.info --lcov --report-on lib/
+        if: always() && steps.install.outcome == 'success' && matrix.sdk != '2.12.0'
+      - uses: coverallsapp/github-action@cfd0633edbd2411b532b808ba7a8b5e04f76d2c8
+        if: always() && steps.install.outcome == 'success' && matrix.sdk != '2.12.0'
+        with:
+          flag-name: os:${{ matrix.os }}/dart:${{ matrix.sdk }}/platform:${{ matrix.platform }}
+          parallel: true
+
+  report-coverage:
+    needs: test
+    if: ${{ always() }}
+    runs-on: ubuntu-latest
+    steps:
+    - uses: coverallsapp/github-action@cfd0633edbd2411b532b808ba7a8b5e04f76d2c8
+      with:
+        parallel-finished: true
diff --git a/README.md b/README.md
index 91c85e5..f3281a8 100644
--- a/README.md
+++ b/README.md
@@ -29,12 +29,29 @@
 | [file_testing](pkgs/file_testing/) | Testing utilities for package:file. | [![package issues](https://img.shields.io/badge/package:file_testing-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Afile_testing) | [![pub package](https://img.shields.io/pub/v/file_testing.svg)](https://pub.dev/packages/file_testing) |
 | [graphs](pkgs/graphs/) | Graph algorithms that operate on graphs in any representation. | [![package issues](https://img.shields.io/badge/package:graphs-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Agraphs) | [![pub package](https://img.shields.io/pub/v/graphs.svg)](https://pub.dev/packages/graphs) |
 | [html](pkgs/html/) | APIs for parsing and manipulating HTML content outside the browser. | [![package issues](https://img.shields.io/badge/package:html-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ahtml) | [![pub package](https://img.shields.io/pub/v/html.svg)](https://pub.dev/packages/html) |
+| [io](pkgs/io/) | Utilities for the Dart VM Runtime including support for ANSI colors, file copying, and standard exit code values. | [![pub package](https://img.shields.io/pub/v/io.svg)](https://pub.dev/packages/io) |
 | [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) |
 | [markdown](pkgs/markdown/) | A portable Markdown library written in Dart that can parse Markdown into HTML. | [![pub package](https://img.shields.io/pub/v/markdown.svg)](https://pub.dev/packages/markdown) |
 | [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) |
+| [pubspec_parse](pkgs/pubspec_parse/) | Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. | [![package issues](https://img.shields.io/badge/package:pubspec_parse-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apubspec_parse) | [![pub package](https://img.shields.io/pub/v/pubspec_parse.svg)](https://pub.dev/packages/pubspec_parse) |
 | [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) |
+| [source_span](pkgs/source_span/) | Provides a standard representation for source code locations and spans. | [![package issues](https://img.shields.io/badge/package:source_span-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_span) | [![pub package](https://img.shields.io/pub/v/source_span.svg)](https://pub.dev/packages/source_span) |
+| [sse](pkgs/sse/) | Provides client and server functionality for setting up bi-directional communication through Server Sent Events (SSE) and corresponding POST requests. | [![package issues](https://img.shields.io/badge/package:sse-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asse) | [![pub package](https://img.shields.io/pub/v/sse.svg)](https://pub.dev/packages/sse) |
+| [stack_trace](pkgs/stack_trace/) | A package for manipulating stack traces and printing them readably. | [![package issues](https://img.shields.io/badge/package:stack_trace-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astack_trace) | [![pub package](https://img.shields.io/pub/v/stack_trace.svg)](https://pub.dev/packages/stack_trace) |
+| [stream_channel](pkgs/stream_channel/) | An abstraction for two-way communication channels based on the Dart Stream class. | [![package issues](https://img.shields.io/badge/package:stream_channel-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astream_channel) | [![pub package](https://img.shields.io/pub/v/stream_channel.svg)](https://pub.dev/packages/stream_channel) |
+| [stream_transform](pkgs/stream_transform/) | A collection of utilities to transform and manipulate streams. | [![package issues](https://img.shields.io/badge/package:stream_transform-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astream_transform) | [![pub package](https://img.shields.io/pub/v/stream_transform.svg)](https://pub.dev/packages/stream_transform) |
+| [term_glyph](pkgs/term_glyph/) | Useful Unicode glyphs and ASCII substitutes. | [![package issues](https://img.shields.io/badge/package:term_glyph-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aterm_glyph) | [![pub package](https://img.shields.io/pub/v/term_glyph.svg)](https://pub.dev/packages/term_glyph) |
+| [test_reflective_loader](pkgs/test_reflective_loader/) | Support for discovering tests and test suites using reflection. | [![package issues](https://img.shields.io/badge/package:test_reflective_loader-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atest_reflective_loader) | [![pub package](https://img.shields.io/pub/v/test_reflective_loader.svg)](https://pub.dev/packages/test_reflective_loader) |
+| [timing](pkgs/timing/) | A simple package for tracking the performance of synchronous and asynchronous actions. | [![package issues](https://img.shields.io/badge/package:timing-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atiming) | [![pub package](https://img.shields.io/pub/v/timing.svg)](https://pub.dev/packages/timing) |
 | [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) |
+| [watcher](pkgs/watcher/) | Monitor directories and send notifications when the contents change. | [![package issues](https://img.shields.io/badge/package:watcher-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Awatcher) | [![pub package](https://img.shields.io/pub/v/watcher.svg)](https://pub.dev/packages/watcher) |
+| [yaml](pkgs/yaml/) | A parser for YAML, a human-friendly data serialization standard | [![package issues](https://img.shields.io/badge/package:yaml-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ayaml) | [![pub package](https://img.shields.io/pub/v/yaml.svg)](https://pub.dev/packages/yaml) |
+| [yaml_edit](pkgs/yaml_edit/) | A library for YAML manipulation with comment and whitespace preservation. | [![package issues](https://img.shields.io/badge/package:yaml_edit-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ayaml_edit) | [![pub package](https://img.shields.io/pub/v/yaml_edit.svg)](https://pub.dev/packages/yaml_edit) |
 
 ## Publishing automation
 
diff --git a/pkgs/bazel_worker/benchmark/benchmark.dart b/pkgs/bazel_worker/benchmark/benchmark.dart
index 035e2b8..0a03122 100644
--- a/pkgs/bazel_worker/benchmark/benchmark.dart
+++ b/pkgs/bazel_worker/benchmark/benchmark.dart
@@ -12,10 +12,7 @@
     var path = 'blaze-bin/some/path/to/a/file/that/is/an/input/$i';
     workRequest
       ..arguments.add('--input=$path')
-      ..inputs.add(Input(
-        path: '',
-        digest: List.filled(70, 0x11),
-      ));
+      ..inputs.add(Input(path: '', digest: List.filled(70, 0x11)));
   }
 
   // Serialize it.
@@ -24,14 +21,20 @@
   print('Request has $length requestBytes.');
 
   // Add the length in front base 128 encoded as in the worker protocol.
-  requestBytes =
-      Uint8List.fromList(requestBytes.toList()..insertAll(0, _varInt(length)));
+  requestBytes = Uint8List.fromList(
+    requestBytes.toList()..insertAll(0, _varInt(length)),
+  );
 
   // Split into 10000 byte chunks.
   var lists = <Uint8List>[];
   for (var i = 0; i < requestBytes.length; i += 10000) {
-    lists.add(Uint8List.sublistView(
-        requestBytes, i, min(i + 10000, requestBytes.length)));
+    lists.add(
+      Uint8List.sublistView(
+        requestBytes,
+        i,
+        min(i + 10000, requestBytes.length),
+      ),
+    );
   }
 
   // Time `AsyncMessageGrouper` and deserialization.
diff --git a/pkgs/bazel_worker/e2e_test/bin/async_worker_in_isolate.dart b/pkgs/bazel_worker/e2e_test/bin/async_worker_in_isolate.dart
index a94875d..285b03d 100644
--- a/pkgs/bazel_worker/e2e_test/bin/async_worker_in_isolate.dart
+++ b/pkgs/bazel_worker/e2e_test/bin/async_worker_in_isolate.dart
@@ -17,7 +17,10 @@
 Future main(List<String> args, [SendPort? message]) async {
   var receivePort = ReceivePort();
   await Isolate.spawnUri(
-      Uri.file('async_worker.dart'), [], receivePort.sendPort);
+    Uri.file('async_worker.dart'),
+    [],
+    receivePort.sendPort,
+  );
 
   var worker = await ForwardsToIsolateAsyncWorker.create(receivePort);
   await worker.run();
diff --git a/pkgs/bazel_worker/e2e_test/lib/async_worker.dart b/pkgs/bazel_worker/e2e_test/lib/async_worker.dart
index d48d87c..55f5171 100644
--- a/pkgs/bazel_worker/e2e_test/lib/async_worker.dart
+++ b/pkgs/bazel_worker/e2e_test/lib/async_worker.dart
@@ -16,9 +16,6 @@
 
   @override
   Future<WorkResponse> performRequest(WorkRequest request) async {
-    return WorkResponse(
-      exitCode: 0,
-      output: request.arguments.join('\n'),
-    );
+    return WorkResponse(exitCode: 0, output: request.arguments.join('\n'));
   }
 }
diff --git a/pkgs/bazel_worker/e2e_test/lib/forwards_to_isolate_async_worker.dart b/pkgs/bazel_worker/e2e_test/lib/forwards_to_isolate_async_worker.dart
index bb937b2..a4845cf 100644
--- a/pkgs/bazel_worker/e2e_test/lib/forwards_to_isolate_async_worker.dart
+++ b/pkgs/bazel_worker/e2e_test/lib/forwards_to_isolate_async_worker.dart
@@ -13,9 +13,11 @@
   final IsolateDriverConnection _isolateDriverConnection;
 
   static Future<ForwardsToIsolateAsyncWorker> create(
-      ReceivePort receivePort) async {
+    ReceivePort receivePort,
+  ) async {
     return ForwardsToIsolateAsyncWorker(
-        await IsolateDriverConnection.create(receivePort));
+      await IsolateDriverConnection.create(receivePort),
+    );
   }
 
   ForwardsToIsolateAsyncWorker(this._isolateDriverConnection);
diff --git a/pkgs/bazel_worker/e2e_test/pubspec.yaml b/pkgs/bazel_worker/e2e_test/pubspec.yaml
index 56f00cd..7eaa89a 100644
--- a/pkgs/bazel_worker/e2e_test/pubspec.yaml
+++ b/pkgs/bazel_worker/e2e_test/pubspec.yaml
@@ -10,6 +10,6 @@
 
 dev_dependencies:
   cli_util: ^0.4.2
-  dart_flutter_team_lints: ^1.0.0
+  dart_flutter_team_lints: ^3.0.0
   path: ^1.8.0
   test: ^1.16.0
diff --git a/pkgs/bazel_worker/e2e_test/test/e2e_test.dart b/pkgs/bazel_worker/e2e_test/test/e2e_test.dart
index caa813a..6b79b5e 100644
--- a/pkgs/bazel_worker/e2e_test/test/e2e_test.dart
+++ b/pkgs/bazel_worker/e2e_test/test/e2e_test.dart
@@ -12,14 +12,18 @@
 
 void main() {
   var dart = p.join(sdkPath, 'bin', 'dart');
-  runE2eTestForWorker('sync worker',
-      () => Process.start(dart, [p.join('bin', 'sync_worker.dart')]));
-  runE2eTestForWorker('async worker',
-      () => Process.start(dart, [p.join('bin', 'async_worker.dart')]));
   runE2eTestForWorker(
-      'async worker in isolate',
-      () =>
-          Process.start(dart, [p.join('bin', 'async_worker_in_isolate.dart')]));
+    'sync worker',
+    () => Process.start(dart, [p.join('bin', 'sync_worker.dart')]),
+  );
+  runE2eTestForWorker(
+    'async worker',
+    () => Process.start(dart, [p.join('bin', 'async_worker.dart')]),
+  );
+  runE2eTestForWorker(
+    'async worker in isolate',
+    () => Process.start(dart, [p.join('bin', 'async_worker_in_isolate.dart')]),
+  );
 }
 
 void runE2eTestForWorker(String groupName, SpawnWorker spawnWorker) {
diff --git a/pkgs/bazel_worker/example/client.dart b/pkgs/bazel_worker/example/client.dart
index 7147fcb..326bb18 100644
--- a/pkgs/bazel_worker/example/client.dart
+++ b/pkgs/bazel_worker/example/client.dart
@@ -5,10 +5,14 @@
 void main() async {
   var scratchSpace = await Directory.systemTemp.createTemp();
   var driver = BazelWorkerDriver(
-      () => Process.start(Platform.resolvedExecutable,
-          [Platform.script.resolve('worker.dart').toFilePath()],
-          workingDirectory: scratchSpace.path),
-      maxWorkers: 4);
+    () => Process.start(
+        Platform.resolvedExecutable,
+        [
+          Platform.script.resolve('worker.dart').toFilePath(),
+        ],
+        workingDirectory: scratchSpace.path),
+    maxWorkers: 4,
+  );
   var response = await driver.doWork(WorkRequest(arguments: ['foo']));
   if (response.exitCode != EXIT_CODE_OK) {
     print('Worker request failed');
diff --git a/pkgs/bazel_worker/lib/src/async_message_grouper.dart b/pkgs/bazel_worker/lib/src/async_message_grouper.dart
index e1f0dea..8fc4778 100644
--- a/pkgs/bazel_worker/lib/src/async_message_grouper.dart
+++ b/pkgs/bazel_worker/lib/src/async_message_grouper.dart
@@ -86,13 +86,18 @@
             // Copy as much as possible from the input buffer. Limit is the
             // smaller of the remaining length to fill in the message and the
             // remaining length in the buffer.
-            var lengthToCopy = min(_message.length - _messagePos,
-                _inputBuffer.length - _inputBufferPos);
+            var lengthToCopy = min(
+              _message.length - _messagePos,
+              _inputBuffer.length - _inputBufferPos,
+            );
             _message.setRange(
-                _messagePos,
-                _messagePos + lengthToCopy,
-                _inputBuffer.sublist(
-                    _inputBufferPos, _inputBufferPos + lengthToCopy));
+              _messagePos,
+              _messagePos + lengthToCopy,
+              _inputBuffer.sublist(
+                _inputBufferPos,
+                _inputBufferPos + lengthToCopy,
+              ),
+            );
             _messagePos += lengthToCopy;
             _inputBufferPos += lengthToCopy;
 
diff --git a/pkgs/bazel_worker/lib/src/driver/driver.dart b/pkgs/bazel_worker/lib/src/driver/driver.dart
index 4a78020..06cf0fe 100644
--- a/pkgs/bazel_worker/lib/src/driver/driver.dart
+++ b/pkgs/bazel_worker/lib/src/driver/driver.dart
@@ -44,9 +44,12 @@
   /// Factory method that spawns a worker process.
   final SpawnWorker _spawnWorker;
 
-  BazelWorkerDriver(this._spawnWorker,
-      {int? maxIdleWorkers, int? maxWorkers, int? maxRetries})
-      : _maxIdleWorkers = maxIdleWorkers ?? 4,
+  BazelWorkerDriver(
+    this._spawnWorker, {
+    int? maxIdleWorkers,
+    int? maxWorkers,
+    int? maxRetries,
+  })  : _maxIdleWorkers = maxIdleWorkers ?? 4,
         _maxWorkers = maxWorkers ?? 4,
         _maxRetries = maxRetries ?? 4;
 
@@ -56,8 +59,10 @@
   /// [request] has been actually sent to the worker. This allows the caller
   /// to determine when actual work is being done versus just waiting for an
   /// available worker.
-  Future<WorkResponse> doWork(WorkRequest request,
-      {void Function(Future<WorkResponse?>)? trackWork}) {
+  Future<WorkResponse> doWork(
+    WorkRequest request, {
+    void Function(Future<WorkResponse?>)? trackWork,
+  }) {
     var attempt = _WorkAttempt(request, trackWork: trackWork);
     _workQueue.add(attempt);
     _runWorkQueue();
@@ -69,9 +74,11 @@
     for (var worker in _readyWorkers.toList()) {
       _killWorker(worker);
     }
-    await Future.wait(_spawningWorkers.map((worker) async {
-      _killWorker(await worker);
-    }));
+    await Future.wait(
+      _spawningWorkers.map((worker) async {
+        _killWorker(await worker);
+      }),
+    );
   }
 
   /// Runs as many items in [_workQueue] as possible given the number of
@@ -88,8 +95,10 @@
     if (_workQueue.isEmpty) return;
     if (_numWorkers == _maxWorkers && _idleWorkers.isEmpty) return;
     if (_numWorkers > _maxWorkers) {
-      throw StateError('Internal error, created to many workers. Please '
-          'file a bug at https://github.com/dart-lang/bazel_worker/issues/new');
+      throw StateError(
+        'Internal error, created to many workers. Please '
+        'file a bug at https://github.com/dart-lang/bazel_worker/issues/new',
+      );
     }
 
     // At this point we definitely want to run a task, we just need to decide
@@ -137,48 +146,51 @@
   void _runWorker(Process worker, _WorkAttempt attempt) {
     var rescheduled = false;
 
-    runZonedGuarded(() async {
-      var connection = _workerConnections[worker]!;
+    runZonedGuarded(
+      () async {
+        var connection = _workerConnections[worker]!;
 
-      connection.writeRequest(attempt.request);
-      var responseFuture = connection.readResponse();
-      if (attempt.trackWork != null) {
-        attempt.trackWork!(responseFuture);
-      }
-      var response = await responseFuture;
+        connection.writeRequest(attempt.request);
+        var responseFuture = connection.readResponse();
+        if (attempt.trackWork != null) {
+          attempt.trackWork!(responseFuture);
+        }
+        var response = await responseFuture;
 
-      // It is possible for us to complete with an error response due to an
-      // unhandled async error before we get here.
-      if (!attempt.responseCompleter.isCompleted) {
-        if (response.exitCode == EXIT_CODE_BROKEN_PIPE) {
+        // It is possible for us to complete with an error response due to an
+        // unhandled async error before we get here.
+        if (!attempt.responseCompleter.isCompleted) {
+          if (response.exitCode == EXIT_CODE_BROKEN_PIPE) {
+            rescheduled = _tryReschedule(attempt);
+            if (rescheduled) return;
+            stderr.writeln('Failed to run request ${attempt.request}');
+            response = WorkResponse(
+              exitCode: EXIT_CODE_ERROR,
+              output:
+                  'Invalid response from worker, this probably means it wrote '
+                  'invalid output or died.',
+            );
+          }
+          attempt.responseCompleter.complete(response);
+          _cleanUp(worker);
+        }
+      },
+      (e, s) {
+        // Note that we don't need to do additional cleanup here on failures. If
+        // the worker dies that is already handled in a generic fashion, we just
+        // need to make sure we complete with a valid response.
+        if (!attempt.responseCompleter.isCompleted) {
           rescheduled = _tryReschedule(attempt);
           if (rescheduled) return;
-          stderr.writeln('Failed to run request ${attempt.request}');
-          response = WorkResponse(
+          var response = WorkResponse(
             exitCode: EXIT_CODE_ERROR,
-            output:
-                'Invalid response from worker, this probably means it wrote '
-                'invalid output or died.',
+            output: 'Error running worker:\n$e\n$s',
           );
+          attempt.responseCompleter.complete(response);
+          _cleanUp(worker);
         }
-        attempt.responseCompleter.complete(response);
-        _cleanUp(worker);
-      }
-    }, (e, s) {
-      // Note that we don't need to do additional cleanup here on failures. If
-      // the worker dies that is already handled in a generic fashion, we just
-      // need to make sure we complete with a valid response.
-      if (!attempt.responseCompleter.isCompleted) {
-        rescheduled = _tryReschedule(attempt);
-        if (rescheduled) return;
-        var response = WorkResponse(
-          exitCode: EXIT_CODE_ERROR,
-          output: 'Error running worker:\n$e\n$s',
-        );
-        attempt.responseCompleter.complete(response);
-        _cleanUp(worker);
-      }
-    });
+      },
+    );
   }
 
   /// Performs post-work cleanup for [worker].
diff --git a/pkgs/bazel_worker/lib/src/driver/driver_connection.dart b/pkgs/bazel_worker/lib/src/driver/driver_connection.dart
index b419deb..80d5c98 100644
--- a/pkgs/bazel_worker/lib/src/driver/driver_connection.dart
+++ b/pkgs/bazel_worker/lib/src/driver/driver_connection.dart
@@ -34,13 +34,16 @@
 
   Future<void> get done => _messageGrouper.done;
 
-  StdDriverConnection(
-      {Stream<List<int>>? inputStream, StreamSink<List<int>>? outputStream})
-      : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin),
+  StdDriverConnection({
+    Stream<List<int>>? inputStream,
+    StreamSink<List<int>>? outputStream,
+  })  : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin),
         _outputStream = outputStream ?? stdout;
 
   factory StdDriverConnection.forWorker(Process worker) => StdDriverConnection(
-      inputStream: worker.stdout, outputStream: worker.stdin);
+        inputStream: worker.stdout,
+        outputStream: worker.stdin,
+      );
 
   /// Note: This will attempts to recover from invalid proto messages by parsing
   /// them as strings. This is a common error case for workers (they print a
diff --git a/pkgs/bazel_worker/lib/src/utils.dart b/pkgs/bazel_worker/lib/src/utils.dart
index 609b435..f67bbac 100644
--- a/pkgs/bazel_worker/lib/src/utils.dart
+++ b/pkgs/bazel_worker/lib/src/utils.dart
@@ -13,8 +13,9 @@
   var delimiterBuffer = CodedBufferWriter();
   delimiterBuffer.writeInt32NoTag(messageBuffer.lengthInBytes);
 
-  var result =
-      Uint8List(messageBuffer.lengthInBytes + delimiterBuffer.lengthInBytes);
+  var result = Uint8List(
+    messageBuffer.lengthInBytes + delimiterBuffer.lengthInBytes,
+  );
 
   delimiterBuffer.writeTo(result);
   messageBuffer.writeTo(result, delimiterBuffer.lengthInBytes);
diff --git a/pkgs/bazel_worker/lib/src/worker/async_worker_loop.dart b/pkgs/bazel_worker/lib/src/worker/async_worker_loop.dart
index 5182b55..a95d09a 100644
--- a/pkgs/bazel_worker/lib/src/worker/async_worker_loop.dart
+++ b/pkgs/bazel_worker/lib/src/worker/async_worker_loop.dart
@@ -32,20 +32,20 @@
         var request = await connection.readRequest();
         if (request == null) break;
         var printMessages = StringBuffer();
-        response = await runZoned(() => performRequest(request),
-            zoneSpecification:
-                ZoneSpecification(print: (self, parent, zone, message) {
-          printMessages.writeln();
-          printMessages.write(message);
-        }));
+        response = await runZoned(
+          () => performRequest(request),
+          zoneSpecification: ZoneSpecification(
+            print: (self, parent, zone, message) {
+              printMessages.writeln();
+              printMessages.write(message);
+            },
+          ),
+        );
         if (printMessages.isNotEmpty) {
           response.output = '${response.output}$printMessages';
         }
       } catch (e, s) {
-        response = WorkResponse(
-          exitCode: EXIT_CODE_ERROR,
-          output: '$e\n$s',
-        );
+        response = WorkResponse(exitCode: EXIT_CODE_ERROR, output: '$e\n$s');
       }
 
       connection.writeResponse(response);
diff --git a/pkgs/bazel_worker/lib/src/worker/sync_worker_loop.dart b/pkgs/bazel_worker/lib/src/worker/sync_worker_loop.dart
index a857105..51da684 100644
--- a/pkgs/bazel_worker/lib/src/worker/sync_worker_loop.dart
+++ b/pkgs/bazel_worker/lib/src/worker/sync_worker_loop.dart
@@ -30,19 +30,20 @@
         var request = connection.readRequest();
         if (request == null) break;
         var printMessages = StringBuffer();
-        response = runZoned(() => performRequest(request), zoneSpecification:
-            ZoneSpecification(print: (self, parent, zone, message) {
-          printMessages.writeln();
-          printMessages.write(message);
-        }));
+        response = runZoned(
+          () => performRequest(request),
+          zoneSpecification: ZoneSpecification(
+            print: (self, parent, zone, message) {
+              printMessages.writeln();
+              printMessages.write(message);
+            },
+          ),
+        );
         if (printMessages.isNotEmpty) {
           response.output = '${response.output}$printMessages';
         }
       } catch (e, s) {
-        response = WorkResponse(
-          exitCode: EXIT_CODE_ERROR,
-          output: '$e\n$s',
-        );
+        response = WorkResponse(exitCode: EXIT_CODE_ERROR, output: '$e\n$s');
       }
 
       connection.writeResponse(response);
diff --git a/pkgs/bazel_worker/lib/src/worker/worker_connection.dart b/pkgs/bazel_worker/lib/src/worker/worker_connection.dart
index b395316..fd5508e 100644
--- a/pkgs/bazel_worker/lib/src/worker/worker_connection.dart
+++ b/pkgs/bazel_worker/lib/src/worker/worker_connection.dart
@@ -29,13 +29,16 @@
   /// Creates a [StdAsyncWorkerConnection] with the specified [inputStream]
   /// and [outputStream], unless [sendPort] is specified, in which case
   /// creates a [SendPortAsyncWorkerConnection].
-  factory AsyncWorkerConnection(
-          {Stream<List<int>>? inputStream,
-          StreamSink<List<int>>? outputStream,
-          SendPort? sendPort}) =>
+  factory AsyncWorkerConnection({
+    Stream<List<int>>? inputStream,
+    StreamSink<List<int>>? outputStream,
+    SendPort? sendPort,
+  }) =>
       sendPort == null
           ? StdAsyncWorkerConnection(
-              inputStream: inputStream, outputStream: outputStream)
+              inputStream: inputStream,
+              outputStream: outputStream,
+            )
           : SendPortAsyncWorkerConnection(sendPort);
 
   @override
@@ -53,9 +56,10 @@
   final AsyncMessageGrouper _messageGrouper;
   final StreamSink<List<int>> _outputStream;
 
-  StdAsyncWorkerConnection(
-      {Stream<List<int>>? inputStream, StreamSink<List<int>>? outputStream})
-      : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin),
+  StdAsyncWorkerConnection({
+    Stream<List<int>>? inputStream,
+    StreamSink<List<int>>? outputStream,
+  })  : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin),
         _outputStream = outputStream ?? stdout;
 
   @override
diff --git a/pkgs/bazel_worker/lib/testing.dart b/pkgs/bazel_worker/lib/testing.dart
index 3ae4c1f..7aefabb 100644
--- a/pkgs/bazel_worker/lib/testing.dart
+++ b/pkgs/bazel_worker/lib/testing.dart
@@ -72,10 +72,18 @@
   }
 
   @override
-  StreamSubscription<Uint8List> listen(void Function(Uint8List bytes)? onData,
-      {Function? onError, void Function()? onDone, bool? cancelOnError}) {
-    return _controller.stream.listen(onData,
-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+  StreamSubscription<Uint8List> listen(
+    void Function(Uint8List bytes)? onData, {
+    Function? onError,
+    void Function()? onDone,
+    bool? cancelOnError,
+  }) {
+    return _controller.stream.listen(
+      onData,
+      onError: onError,
+      onDone: onDone,
+      cancelOnError: cancelOnError,
+    );
   }
 
   @override
@@ -165,8 +173,9 @@
   final List<WorkResponse> responses = <WorkResponse>[];
 
   TestAsyncWorkerConnection(
-      Stream<List<int>> inputStream, StreamSink<List<int>> outputStream)
-      : super(inputStream: inputStream, outputStream: outputStream);
+    Stream<List<int>> inputStream,
+    StreamSink<List<int>> outputStream,
+  ) : super(inputStream: inputStream, outputStream: outputStream);
 
   @override
   void writeResponse(WorkResponse response) {
diff --git a/pkgs/bazel_worker/test/driver_test.dart b/pkgs/bazel_worker/test/driver_test.dart
index c397830..c3db55c 100644
--- a/pkgs/bazel_worker/test/driver_test.dart
+++ b/pkgs/bazel_worker/test/driver_test.dart
@@ -23,27 +23,37 @@
       await _doRequests(count: 1);
     });
 
-    test('can run multiple batches of requests through multiple workers',
-        () async {
-      var maxWorkers = 4;
-      var maxIdleWorkers = 2;
-      driver = BazelWorkerDriver(MockWorker.spawn,
-          maxWorkers: maxWorkers, maxIdleWorkers: maxIdleWorkers);
-      for (var i = 0; i < 10; i++) {
-        await _doRequests(driver: driver);
-        expect(MockWorker.liveWorkers.length, maxIdleWorkers);
-        // No workers should be killed while there is ongoing work, but they
-        // should be cleaned up once there isn't any more work to do.
-        expect(MockWorker.deadWorkers.length,
-            (maxWorkers - maxIdleWorkers) * (i + 1));
-      }
-    });
+    test(
+      'can run multiple batches of requests through multiple workers',
+      () async {
+        var maxWorkers = 4;
+        var maxIdleWorkers = 2;
+        driver = BazelWorkerDriver(
+          MockWorker.spawn,
+          maxWorkers: maxWorkers,
+          maxIdleWorkers: maxIdleWorkers,
+        );
+        for (var i = 0; i < 10; i++) {
+          await _doRequests(driver: driver);
+          expect(MockWorker.liveWorkers.length, maxIdleWorkers);
+          // No workers should be killed while there is ongoing work, but they
+          // should be cleaned up once there isn't any more work to do.
+          expect(
+            MockWorker.deadWorkers.length,
+            (maxWorkers - maxIdleWorkers) * (i + 1),
+          );
+        }
+      },
+    );
 
     test('can run multiple requests through one worker', () async {
       var maxWorkers = 1;
       var maxIdleWorkers = 1;
-      driver = BazelWorkerDriver(MockWorker.spawn,
-          maxWorkers: maxWorkers, maxIdleWorkers: maxIdleWorkers);
+      driver = BazelWorkerDriver(
+        MockWorker.spawn,
+        maxWorkers: maxWorkers,
+        maxIdleWorkers: maxIdleWorkers,
+      );
       for (var i = 0; i < 10; i++) {
         await _doRequests(driver: driver);
         expect(MockWorker.liveWorkers.length, 1);
@@ -52,8 +62,11 @@
     });
 
     test('can run one request through multiple workers', () async {
-      driver =
-          BazelWorkerDriver(MockWorker.spawn, maxWorkers: 4, maxIdleWorkers: 4);
+      driver = BazelWorkerDriver(
+        MockWorker.spawn,
+        maxWorkers: 4,
+        maxIdleWorkers: 4,
+      );
       for (var i = 0; i < 10; i++) {
         await _doRequests(driver: driver, count: 1);
         expect(MockWorker.liveWorkers.length, 1);
@@ -63,8 +76,11 @@
 
     test('can run with maxIdleWorkers == 0', () async {
       var maxWorkers = 4;
-      driver = BazelWorkerDriver(MockWorker.spawn,
-          maxWorkers: maxWorkers, maxIdleWorkers: 0);
+      driver = BazelWorkerDriver(
+        MockWorker.spawn,
+        maxWorkers: maxWorkers,
+        maxIdleWorkers: 0,
+      );
       for (var i = 0; i < 10; i++) {
         await _doRequests(driver: driver);
         expect(MockWorker.liveWorkers.length, 0);
@@ -77,14 +93,15 @@
       driver = BazelWorkerDriver(MockWorker.spawn, maxWorkers: maxWorkers);
       var tracking = <Future>[];
       await _doRequests(
-          driver: driver,
-          count: 10,
-          trackWork: (Future response) {
-            // We should never be tracking more than `maxWorkers` jobs at a time.
-            expect(tracking.length, lessThan(maxWorkers));
-            tracking.add(response);
-            response.then((_) => tracking.remove(response));
-          });
+        driver: driver,
+        count: 10,
+        trackWork: (Future response) {
+          // We should never be tracking more than `maxWorkers` jobs at a time.
+          expect(tracking.length, lessThan(maxWorkers));
+          tracking.add(response);
+          response.then((_) => tracking.remove(response));
+        },
+      );
     });
 
     group('failing workers', () {
@@ -93,27 +110,39 @@
       void createDriver({int maxRetries = 2, int numBadWorkers = 2}) {
         var numSpawned = 0;
         driver = BazelWorkerDriver(
-            () async => MockWorker(workerLoopFactory: (MockWorker worker) {
-                  var connection = StdAsyncWorkerConnection(
-                      inputStream: worker._stdinController.stream,
-                      outputStream: worker._stdoutController.sink);
-                  if (numSpawned < numBadWorkers) {
-                    numSpawned++;
-                    return ThrowingMockWorkerLoop(
-                        worker, MockWorker.responseQueue, connection);
-                  } else {
-                    return MockWorkerLoop(MockWorker.responseQueue,
-                        connection: connection);
-                  }
-                }),
-            maxRetries: maxRetries);
+          () async => MockWorker(
+            workerLoopFactory: (MockWorker worker) {
+              var connection = StdAsyncWorkerConnection(
+                inputStream: worker._stdinController.stream,
+                outputStream: worker._stdoutController.sink,
+              );
+              if (numSpawned < numBadWorkers) {
+                numSpawned++;
+                return ThrowingMockWorkerLoop(
+                  worker,
+                  MockWorker.responseQueue,
+                  connection,
+                );
+              } else {
+                return MockWorkerLoop(
+                  MockWorker.responseQueue,
+                  connection: connection,
+                );
+              }
+            },
+          ),
+          maxRetries: maxRetries,
+        );
       }
 
       test('should retry up to maxRetries times', () async {
         createDriver();
         var expectedResponse = WorkResponse();
-        MockWorker.responseQueue.addAll(
-            [disconnectedResponse, disconnectedResponse, expectedResponse]);
+        MockWorker.responseQueue.addAll([
+          disconnectedResponse,
+          disconnectedResponse,
+          expectedResponse,
+        ]);
         var actualResponse = await driver!.doWork(WorkRequest());
         // The first 2 null responses are thrown away, and we should get the
         // third one.
@@ -125,23 +154,29 @@
 
       test('should fail if it exceeds maxRetries failures', () async {
         createDriver(maxRetries: 2, numBadWorkers: 3);
-        MockWorker.responseQueue.addAll(
-            [disconnectedResponse, disconnectedResponse, WorkResponse()]);
+        MockWorker.responseQueue.addAll([
+          disconnectedResponse,
+          disconnectedResponse,
+          WorkResponse(),
+        ]);
         var actualResponse = await driver!.doWork(WorkRequest());
         // Should actually get a bad response.
         expect(actualResponse.exitCode, 15);
         expect(
-            actualResponse.output,
-            'Invalid response from worker, this probably means it wrote '
-            'invalid output or died.');
+          actualResponse.output,
+          'Invalid response from worker, this probably means it wrote '
+          'invalid output or died.',
+        );
 
         expect(MockWorker.deadWorkers.length, 3);
       });
     });
 
     test('handles spawnWorker failures', () async {
-      driver = BazelWorkerDriver(() async => throw StateError('oh no!'),
-          maxRetries: 0);
+      driver = BazelWorkerDriver(
+        () async => throw StateError('oh no!'),
+        maxRetries: 0,
+      );
       expect(driver!.doWork(WorkRequest()), throwsA(isA<StateError>()));
     });
 
@@ -156,10 +191,11 @@
 
 /// Runs [count] of fake work requests through [driver], and asserts that they
 /// all completed.
-Future _doRequests(
-    {BazelWorkerDriver? driver,
-    int count = 100,
-    void Function(Future<WorkResponse?>)? trackWork}) async {
+Future _doRequests({
+  BazelWorkerDriver? driver,
+  int count = 100,
+  void Function(Future<WorkResponse?>)? trackWork,
+}) async {
   // If we create a driver, we need to make sure and terminate it.
   var terminateDriver = driver == null;
   driver ??= BazelWorkerDriver(MockWorker.spawn);
@@ -167,7 +203,8 @@
   var responses = List.generate(count, (_) => WorkResponse());
   MockWorker.responseQueue.addAll(responses);
   var actualResponses = await Future.wait(
-      requests.map((request) => driver!.doWork(request, trackWork: trackWork)));
+    requests.map((request) => driver!.doWork(request, trackWork: trackWork)),
+  );
   expect(actualResponses, unorderedEquals(responses));
   if (terminateDriver) await driver.terminateWorkers();
 }
@@ -191,9 +228,11 @@
 class ThrowingMockWorkerLoop extends MockWorkerLoop {
   final MockWorker _mockWorker;
 
-  ThrowingMockWorkerLoop(this._mockWorker, Queue<WorkResponse> responseQueue,
-      AsyncWorkerConnection connection)
-      : super(responseQueue, connection: connection);
+  ThrowingMockWorkerLoop(
+    this._mockWorker,
+    Queue<WorkResponse> responseQueue,
+    AsyncWorkerConnection connection,
+  ) : super(responseQueue, connection: connection);
 
   /// Run the worker loop. The returned [Future] doesn't complete until
   /// [connection#readRequest] returns `null`.
@@ -234,10 +273,13 @@
     liveWorkers.add(this);
     var workerLoop = workerLoopFactory != null
         ? workerLoopFactory(this)
-        : MockWorkerLoop(responseQueue,
+        : MockWorkerLoop(
+            responseQueue,
             connection: StdAsyncWorkerConnection(
-                inputStream: _stdinController.stream,
-                outputStream: _stdoutController.sink));
+              inputStream: _stdinController.stream,
+              outputStream: _stdoutController.sink,
+            ),
+          );
     workerLoop.run();
   }
 
@@ -260,8 +302,10 @@
   int get pid => throw UnsupportedError('Not needed.');
 
   @override
-  bool kill(
-      [ProcessSignal processSignal = ProcessSignal.sigterm, int exitCode = 0]) {
+  bool kill([
+    ProcessSignal processSignal = ProcessSignal.sigterm,
+    int exitCode = 0,
+  ]) {
     if (_killed) return false;
     () async {
       await _stdoutController.close();
diff --git a/pkgs/bazel_worker/test/message_grouper_test.dart b/pkgs/bazel_worker/test/message_grouper_test.dart
index 475190e..fd99911 100644
--- a/pkgs/bazel_worker/test/message_grouper_test.dart
+++ b/pkgs/bazel_worker/test/message_grouper_test.dart
@@ -18,8 +18,10 @@
   });
 }
 
-void runTests(TestStdin Function() stdinFactory,
-    MessageGrouper Function(Stdin) messageGrouperFactory) {
+void runTests(
+  TestStdin Function() stdinFactory,
+  MessageGrouper Function(Stdin) messageGrouperFactory,
+) {
   late MessageGrouper messageGrouper;
 
   late TestStdin stdinStream;
@@ -52,16 +54,12 @@
   });
 
   test('Short message', () async {
-    await check([
-      5,
-      10,
-      20,
-      30,
-      40,
-      50
-    ], [
-      [10, 20, 30, 40, 50]
-    ]);
+    await check(
+      [5, 10, 20, 30, 40, 50],
+      [
+        [10, 20, 30, 40, 50],
+      ],
+    );
   });
 
   test('Message with 2-byte length', () async {
@@ -79,57 +77,44 @@
   });
 
   test('Multiple messages', () async {
-    await check([
-      2,
-      10,
-      20,
-      2,
-      30,
-      40
-    ], [
-      [10, 20],
-      [30, 40]
-    ]);
+    await check(
+      [2, 10, 20, 2, 30, 40],
+      [
+        [10, 20],
+        [30, 40],
+      ],
+    );
   });
 
   test('Empty message at start', () async {
-    await check([
-      0,
-      2,
-      10,
-      20
-    ], [
-      [],
-      [10, 20]
-    ]);
+    await check(
+      [0, 2, 10, 20],
+      [
+        [],
+        [10, 20],
+      ],
+    );
   });
 
   test('Empty message at end', () async {
-    await check([
-      2,
-      10,
-      20,
-      0
-    ], [
-      [10, 20],
-      []
-    ]);
+    await check(
+      [2, 10, 20, 0],
+      [
+        [10, 20],
+        [],
+      ],
+    );
   });
 
   test('Empty message in the middle', () async {
-    await check([
-      2,
-      10,
-      20,
-      0,
-      2,
-      30,
-      40
-    ], [
-      [10, 20],
-      [],
-      [30, 40]
-    ]);
+    await check(
+      [2, 10, 20, 0, 2, 30, 40],
+      [
+        [10, 20],
+        [],
+        [30, 40],
+      ],
+    );
   });
 
   test('Handles the case when stdin gives an error instead of EOF', () async {
diff --git a/pkgs/bazel_worker/test/worker_loop_test.dart b/pkgs/bazel_worker/test/worker_loop_test.dart
index 50d2151..24068b1 100644
--- a/pkgs/bazel_worker/test/worker_loop_test.dart
+++ b/pkgs/bazel_worker/test/worker_loop_test.dart
@@ -11,36 +11,45 @@
 
 void main() {
   group('SyncWorkerLoop', () {
-    runTests(TestStdinSync.new, TestSyncWorkerConnection.new,
-        TestSyncWorkerLoop.new);
+    runTests(
+      TestStdinSync.new,
+      TestSyncWorkerConnection.new,
+      TestSyncWorkerLoop.new,
+    );
   });
 
   group('AsyncWorkerLoop', () {
-    runTests(TestStdinAsync.new, TestAsyncWorkerConnection.new,
-        TestAsyncWorkerLoop.new);
+    runTests(
+      TestStdinAsync.new,
+      TestAsyncWorkerConnection.new,
+      TestAsyncWorkerLoop.new,
+    );
   });
 
   group('SyncWorkerLoopWithPrint', () {
     runTests(
-        TestStdinSync.new,
-        TestSyncWorkerConnection.new,
-        (TestSyncWorkerConnection connection) =>
-            TestSyncWorkerLoop(connection, printMessage: 'Goodbye!'));
+      TestStdinSync.new,
+      TestSyncWorkerConnection.new,
+      (TestSyncWorkerConnection connection) =>
+          TestSyncWorkerLoop(connection, printMessage: 'Goodbye!'),
+    );
   });
 
   group('AsyncWorkerLoopWithPrint', () {
     runTests(
-        TestStdinAsync.new,
-        TestAsyncWorkerConnection.new,
-        (TestAsyncWorkerConnection connection) =>
-            TestAsyncWorkerLoop(connection, printMessage: 'Goodbye!'));
+      TestStdinAsync.new,
+      TestAsyncWorkerConnection.new,
+      (TestAsyncWorkerConnection connection) =>
+          TestAsyncWorkerLoop(connection, printMessage: 'Goodbye!'),
+    );
   });
 }
 
 void runTests<T extends TestWorkerConnection>(
-    TestStdin Function() stdinFactory,
-    T Function(Stdin, Stdout) workerConnectionFactory,
-    TestWorkerLoop Function(T) workerLoopFactory) {
+  TestStdin Function() stdinFactory,
+  T Function(Stdin, Stdout) workerConnectionFactory,
+  TestWorkerLoop Function(T) workerLoopFactory,
+) {
   late TestStdin stdinStream;
   late TestStdoutStream stdoutStream;
   late T connection;
@@ -63,19 +72,29 @@
 
     // Make sure `print` never gets called in the parent zone.
     var printMessages = <String>[];
-    await runZoned(() => workerLoop.run(), zoneSpecification:
-        ZoneSpecification(print: (self, parent, zone, message) {
-      printMessages.add(message);
-    }));
-    expect(printMessages, isEmpty,
-        reason: 'The worker loop should hide all print calls from the parent '
-            'zone.');
+    await runZoned(
+      () => workerLoop.run(),
+      zoneSpecification: ZoneSpecification(
+        print: (self, parent, zone, message) {
+          printMessages.add(message);
+        },
+      ),
+    );
+    expect(
+      printMessages,
+      isEmpty,
+      reason: 'The worker loop should hide all print calls from the parent '
+          'zone.',
+    );
 
     expect(connection.responses, hasLength(1));
     expect(connection.responses[0], response);
     if (workerLoop.printMessage != null) {
-      expect(response.output, endsWith(workerLoop.printMessage!),
-          reason: 'Print messages should get appended to the response output.');
+      expect(
+        response.output,
+        endsWith(workerLoop.printMessage!),
+        reason: 'Print messages should get appended to the response output.',
+      );
     }
 
     // Check that a serialized version was written to std out.
diff --git a/pkgs/clock/analysis_options.yaml b/pkgs/clock/analysis_options.yaml
index 9ee7c2b..db6072d 100644
--- a/pkgs/clock/analysis_options.yaml
+++ b/pkgs/clock/analysis_options.yaml
@@ -11,4 +11,3 @@
   rules:
     - avoid_private_typedef_functions
     - avoid_redundant_argument_values
-    - use_super_parameters
diff --git a/pkgs/coverage/analysis_options.yaml b/pkgs/coverage/analysis_options.yaml
index 82ce5e0..bb1afe0 100644
--- a/pkgs/coverage/analysis_options.yaml
+++ b/pkgs/coverage/analysis_options.yaml
@@ -9,14 +9,9 @@
 
 linter:
   rules:
-    - always_declare_return_types
     - avoid_slow_async_io
     - cancel_subscriptions
-    - comment_references
     - literal_only_boolean_expressions
     - prefer_final_locals
     - sort_constructors_first
     - sort_unnamed_constructors_first
-    - test_types_in_equals
-    - throw_in_finally
-    - type_annotate_public_apis
diff --git a/pkgs/file/CHANGELOG.md b/pkgs/file/CHANGELOG.md
index 50c96c4..3a3969c 100644
--- a/pkgs/file/CHANGELOG.md
+++ b/pkgs/file/CHANGELOG.md
@@ -1,3 +1,5 @@
+## 7.0.2-wip
+
 ## 7.0.1
 
 * Update the pubspec repository field to reflect the new package repository.
diff --git a/pkgs/file/analysis_options.yaml b/pkgs/file/analysis_options.yaml
index 8fbd2e4..d978f81 100644
--- a/pkgs/file/analysis_options.yaml
+++ b/pkgs/file/analysis_options.yaml
@@ -1,6 +1 @@
-include: package:lints/recommended.yaml
-
-analyzer:
-  errors:
-    # Allow having TODOs in the code
-    todo: ignore
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/file/example/main.dart b/pkgs/file/example/main.dart
index 7ca0bc7..b03b363 100644
--- a/pkgs/file/example/main.dart
+++ b/pkgs/file/example/main.dart
@@ -7,8 +7,8 @@
 
 Future<void> main() async {
   final FileSystem fs = MemoryFileSystem();
-  final Directory tmp = await fs.systemTempDirectory.createTemp('example_');
-  final File outputFile = tmp.childFile('output');
+  final tmp = await fs.systemTempDirectory.createTemp('example_');
+  final outputFile = tmp.childFile('output');
   await outputFile.writeAsString('Hello world!');
   print(outputFile.readAsStringSync());
 }
diff --git a/pkgs/file/lib/chroot.dart b/pkgs/file/lib/chroot.dart
index 56d2bd5..6992ad0 100644
--- a/pkgs/file/lib/chroot.dart
+++ b/pkgs/file/lib/chroot.dart
@@ -3,4 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// A file system that provides a view into _another_ `FileSystem` via a path.
+library;
+
 export 'src/backends/chroot.dart';
diff --git a/pkgs/file/lib/file.dart b/pkgs/file/lib/file.dart
index cdde9fe..c2e97b2 100644
--- a/pkgs/file/lib/file.dart
+++ b/pkgs/file/lib/file.dart
@@ -4,5 +4,7 @@
 
 /// Core interfaces containing the abstract `FileSystem` interface definition
 /// and all associated types used by `FileSystem`.
+library;
+
 export 'src/forwarding.dart';
 export 'src/interface.dart';
diff --git a/pkgs/file/lib/local.dart b/pkgs/file/lib/local.dart
index 74f506e..5b1e3cd 100644
--- a/pkgs/file/lib/local.dart
+++ b/pkgs/file/lib/local.dart
@@ -4,4 +4,6 @@
 
 /// A local file system implementation. This relies on the use of `dart:io`
 /// and is thus not suitable for use in the browser.
+library;
+
 export 'src/backends/local.dart';
diff --git a/pkgs/file/lib/memory.dart b/pkgs/file/lib/memory.dart
index c5705ef..690b65f 100644
--- a/pkgs/file/lib/memory.dart
+++ b/pkgs/file/lib/memory.dart
@@ -4,5 +4,7 @@
 
 /// An implementation of `FileSystem` that exists entirely in memory with an
 /// internal representation loosely based on the Filesystem Hierarchy Standard.
+library;
+
 export 'src/backends/memory.dart';
 export 'src/backends/memory/operations.dart';
diff --git a/pkgs/file/lib/src/backends/chroot.dart b/pkgs/file/lib/src/backends/chroot.dart
index 6082e80..402dbec 100644
--- a/pkgs/file/lib/src/backends/chroot.dart
+++ b/pkgs/file/lib/src/backends/chroot.dart
@@ -2,16 +2,16 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-library file.src.backends.chroot;
-
 import 'dart:convert';
 import 'dart:typed_data';
 
-import 'package:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
 import 'package:path/path.dart' as p;
 
+import '../common.dart' as common;
+import '../forwarding.dart';
+import '../interface.dart';
+import '../io.dart' as io;
+
 part 'chroot/chroot_directory.dart';
 part 'chroot/chroot_file.dart';
 part 'chroot/chroot_file_system.dart';
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_directory.dart b/pkgs/file/lib/src/backends/chroot/chroot_directory.dart
index 8fec7b1..e094193 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_directory.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_directory.dart
@@ -2,18 +2,18 @@
 // 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.
 
-part of file.src.backends.chroot;
+part of '../chroot.dart';
 
 class _ChrootDirectory extends _ChrootFileSystemEntity<Directory, io.Directory>
     with ForwardingDirectory<Directory>, common.DirectoryAddOnsMixin {
-  _ChrootDirectory(ChrootFileSystem fs, String path) : super(fs, path);
+  _ChrootDirectory(super.fs, super.path);
 
   factory _ChrootDirectory.wrapped(
     ChrootFileSystem fs,
     Directory delegate, {
     bool relative = false,
   }) {
-    String localPath = fs._local(delegate.path, relative: relative);
+    var localPath = fs._local(delegate.path, relative: relative);
     return _ChrootDirectory(fs, localPath);
   }
 
@@ -32,7 +32,7 @@
       if (await fileSystem.type(path) != expectedType) {
         throw common.notADirectory(path);
       }
-      FileSystemEntityType type = await fileSystem.type(newPath);
+      var type = await fileSystem.type(newPath);
       if (type != FileSystemEntityType.notFound) {
         if (type != expectedType) {
           throw common.notADirectory(newPath);
@@ -44,7 +44,7 @@
           throw common.directoryNotEmpty(newPath);
         }
       }
-      String target = await fileSystem.link(path).target();
+      var target = await fileSystem.link(path).target();
       await fileSystem.link(path).delete();
       await fileSystem.link(newPath).create(target);
       return fileSystem.directory(newPath);
@@ -60,7 +60,7 @@
       if (fileSystem.typeSync(path) != expectedType) {
         throw common.notADirectory(path);
       }
-      FileSystemEntityType type = fileSystem.typeSync(newPath);
+      var type = fileSystem.typeSync(newPath);
       if (type != FileSystemEntityType.notFound) {
         if (type != expectedType) {
           throw common.notADirectory(newPath);
@@ -72,7 +72,7 @@
           throw common.directoryNotEmpty(newPath);
         }
       }
-      String target = fileSystem.link(path).targetSync();
+      var target = fileSystem.link(path).targetSync();
       fileSystem.link(path).deleteSync();
       fileSystem.link(newPath).createSync(target);
       return fileSystem.directory(newPath);
@@ -97,17 +97,15 @@
   @override
   Future<Directory> create({bool recursive = false}) async {
     if (_isLink) {
-      switch (await fileSystem.type(path)) {
-        case FileSystemEntityType.notFound:
-          throw common.noSuchFileOrDirectory(path);
-        case FileSystemEntityType.file:
-          throw common.fileExists(path);
-        case FileSystemEntityType.directory:
+      return switch (await fileSystem.type(path)) {
+        FileSystemEntityType.notFound =>
+          throw common.noSuchFileOrDirectory(path),
+        FileSystemEntityType.file => throw common.fileExists(path),
+        FileSystemEntityType.directory =>
           // Nothing to do.
-          return this;
-        default:
-          throw AssertionError();
-      }
+          this,
+        _ => throw AssertionError()
+      };
     } else {
       return wrap(await delegate.create(recursive: recursive));
     }
@@ -137,8 +135,8 @@
     bool recursive = false,
     bool followLinks = true,
   }) {
-    Directory delegate = this.delegate as Directory;
-    String dirname = delegate.path;
+    var delegate = this.delegate as Directory;
+    var dirname = delegate.path;
     return delegate
         .list(recursive: recursive, followLinks: followLinks)
         .map((io.FileSystemEntity entity) => _denormalize(entity, dirname));
@@ -149,8 +147,8 @@
     bool recursive = false,
     bool followLinks = true,
   }) {
-    Directory delegate = this.delegate as Directory;
-    String dirname = delegate.path;
+    var delegate = this.delegate as Directory;
+    var dirname = delegate.path;
     return delegate
         .listSync(recursive: recursive, followLinks: followLinks)
         .map((io.FileSystemEntity entity) => _denormalize(entity, dirname))
@@ -158,9 +156,9 @@
   }
 
   FileSystemEntity _denormalize(io.FileSystemEntity entity, String dirname) {
-    p.Context ctx = fileSystem.path;
-    String relativePart = ctx.relative(entity.path, from: dirname);
-    String entityPath = ctx.join(path, relativePart);
+    var ctx = fileSystem.path;
+    var relativePart = ctx.relative(entity.path, from: dirname);
+    var entityPath = ctx.join(path, relativePart);
     if (entity is io.File) {
       return _ChrootFile(fileSystem, entityPath);
     } else if (entity is io.Directory) {
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_file.dart b/pkgs/file/lib/src/backends/chroot/chroot_file.dart
index 4b67bc1..d6c29fc 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_file.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_file.dart
@@ -2,20 +2,20 @@
 // 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.
 
-part of file.src.backends.chroot;
+part of '../chroot.dart';
 
 typedef _SetupCallback = dynamic Function();
 
 class _ChrootFile extends _ChrootFileSystemEntity<File, io.File>
     with ForwardingFile {
-  _ChrootFile(ChrootFileSystem fs, String path) : super(fs, path);
+  _ChrootFile(super.fs, super.path);
 
   factory _ChrootFile.wrapped(
     ChrootFileSystem fs,
     io.File delegate, {
     bool relative = false,
   }) {
-    String localPath = fs._local(delegate.path, relative: relative);
+    var localPath = fs._local(delegate.path, relative: relative);
     return _ChrootFile(fs, localPath);
   }
 
@@ -126,7 +126,7 @@
 
   @override
   Future<File> create({bool recursive = false, bool exclusive = false}) async {
-    String path = fileSystem._resolve(
+    var path = fileSystem._resolve(
       this.path,
       followLinks: false,
       notFound: recursive ? _NotFoundBehavior.mkdir : _NotFoundBehavior.allow,
@@ -158,7 +158,7 @@
 
   @override
   void createSync({bool recursive = false, bool exclusive = false}) {
-    String path = fileSystem._resolve(
+    var path = fileSystem._resolve(
       this.path,
       followLinks: false,
       notFound: recursive ? _NotFoundBehavior.mkdir : _NotFoundBehavior.allow,
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart b/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart
index 6889c98..503821f 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart
@@ -2,7 +2,7 @@
 // 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.
 
-part of file.src.backends.chroot;
+part of '../chroot.dart';
 
 const String _thisDir = '.';
 const String _parentDir = '..';
@@ -107,7 +107,7 @@
     }
 
     value = _resolve(value, notFound: _NotFoundBehavior.throwError);
-    String realPath = _real(value, resolve: false);
+    var realPath = _real(value, resolve: false);
     switch (delegate.typeSync(realPath, followLinks: false)) {
       case FileSystemEntityType.directory:
         break;
@@ -117,7 +117,7 @@
         throw common.notADirectory(path as String);
     }
     assert(() {
-      p.Context ctx = delegate.path;
+      var ctx = delegate.path;
       return ctx.isAbsolute(value) && value == ctx.canonicalize(value);
     }());
     _cwd = value;
@@ -201,7 +201,7 @@
       throw _ChrootJailException();
     }
     // TODO(tvolkert): See if _context.relative() works here
-    String result = realPath.substring(root.length);
+    var result = realPath.substring(root.length);
     if (result.isEmpty) {
       result = _localRoot;
     }
@@ -263,8 +263,8 @@
       throw common.noSuchFileOrDirectory(path);
     }
 
-    p.Context ctx = this.path;
-    String root = _localRoot;
+    var ctx = this.path;
+    var root = _localRoot;
     List<String> parts, ledger;
     if (ctx.isAbsolute(path)) {
       parts = ctx.split(path).sublist(1);
@@ -277,9 +277,9 @@
     }
 
     String getCurrentPath() => root + ctx.joinAll(ledger);
-    Set<String> breadcrumbs = <String>{};
+    var breadcrumbs = <String>{};
     while (parts.isNotEmpty) {
-      String segment = parts.removeAt(0);
+      var segment = parts.removeAt(0);
       if (segment == _thisDir) {
         continue;
       } else if (segment == _parentDir) {
@@ -290,8 +290,8 @@
       }
 
       ledger.add(segment);
-      String currentPath = getCurrentPath();
-      String realPath = _real(currentPath, resolve: false);
+      var currentPath = getCurrentPath();
+      var realPath = _real(currentPath, resolve: false);
 
       switch (delegate.typeSync(realPath, followLinks: false)) {
         case FileSystemEntityType.directory:
@@ -333,7 +333,7 @@
           if (!breadcrumbs.add(currentPath)) {
             throw common.tooManyLevelsOfSymbolicLinks(path);
           }
-          String target = delegate.link(realPath).targetSync();
+          var target = delegate.link(realPath).targetSync();
           if (ctx.isAbsolute(target)) {
             ledger.clear();
             parts.insertAll(0, ctx.split(target).sublist(1));
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart b/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart
index 8e859ac..18e37cd 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart
@@ -2,7 +2,7 @@
 // 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.
 
-part of file.src.backends.chroot;
+part of '../chroot.dart';
 
 abstract class _ChrootFileSystemEntity<T extends FileSystemEntity,
     D extends io.FileSystemEntity> extends ForwardingFileSystemEntity<T, D> {
@@ -103,7 +103,7 @@
 
   @override
   Future<T> delete({bool recursive = false}) async {
-    String path = fileSystem._resolve(this.path,
+    var path = fileSystem._resolve(this.path,
         followLinks: false, notFound: _NotFoundBehavior.throwError);
 
     String real(String path) => fileSystem._real(path, resolve: false);
@@ -114,7 +114,7 @@
       if (expectedType == FileSystemEntityType.link) {
         await fileSystem.delegate.link(real(path)).delete();
       } else {
-        String resolvedPath = fileSystem._resolve(p.basename(path),
+        var resolvedPath = fileSystem._resolve(p.basename(path),
             from: p.dirname(path), notFound: _NotFoundBehavior.allowAtTail);
         if (!recursive && await type(resolvedPath) != expectedType) {
           throw expectedType == FileSystemEntityType.file
@@ -132,7 +132,7 @@
 
   @override
   void deleteSync({bool recursive = false}) {
-    String path = fileSystem._resolve(this.path,
+    var path = fileSystem._resolve(this.path,
         followLinks: false, notFound: _NotFoundBehavior.throwError);
 
     String real(String path) => fileSystem._real(path, resolve: false);
@@ -143,7 +143,7 @@
       if (expectedType == FileSystemEntityType.link) {
         fileSystem.delegate.link(real(path)).deleteSync();
       } else {
-        String resolvedPath = fileSystem._resolve(p.basename(path),
+        var resolvedPath = fileSystem._resolve(p.basename(path),
             from: p.dirname(path), notFound: _NotFoundBehavior.allowAtTail);
         if (!recursive && type(resolvedPath) != expectedType) {
           throw expectedType == FileSystemEntityType.file
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_link.dart b/pkgs/file/lib/src/backends/chroot/chroot_link.dart
index acbeda6..1620df9 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_link.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_link.dart
@@ -2,18 +2,18 @@
 // 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.
 
-part of file.src.backends.chroot;
+part of '../chroot.dart';
 
 class _ChrootLink extends _ChrootFileSystemEntity<Link, io.Link>
     with ForwardingLink {
-  _ChrootLink(ChrootFileSystem fs, String path) : super(fs, path);
+  _ChrootLink(super.fs, super.path);
 
   factory _ChrootLink.wrapped(
     ChrootFileSystem fs,
     io.Link delegate, {
     bool relative = false,
   }) {
-    String localPath = fs._local(delegate.path, relative: relative);
+    var localPath = fs._local(delegate.path, relative: relative);
     return _ChrootLink(fs, localPath);
   }
 
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart b/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart
index 4105ac8..10bbd70 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart
@@ -2,7 +2,7 @@
 // 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.
 
-part of file.src.backends.chroot;
+part of '../chroot.dart';
 
 class _ChrootRandomAccessFile with ForwardingRandomAccessFile {
   _ChrootRandomAccessFile(this.path, this.delegate);
diff --git a/pkgs/file/lib/src/backends/local/local_directory.dart b/pkgs/file/lib/src/backends/local/local_directory.dart
index e23e68f..3e1db61 100644
--- a/pkgs/file/lib/src/backends/local/local_directory.dart
+++ b/pkgs/file/lib/src/backends/local/local_directory.dart
@@ -2,10 +2,10 @@
 // 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
-
+import '../../common.dart' as common;
+import '../../forwarding.dart';
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'local_file_system_entity.dart';
 
 /// [Directory] implementation that forwards all calls to `dart:io`.
@@ -13,7 +13,7 @@
     with ForwardingDirectory<LocalDirectory>, common.DirectoryAddOnsMixin {
   /// Instantiates a new [LocalDirectory] tied to the specified file system
   /// and delegating to the specified [delegate].
-  LocalDirectory(FileSystem fs, io.Directory delegate) : super(fs, delegate);
+  LocalDirectory(super.fs, super.delegate);
 
   @override
   String toString() => "LocalDirectory: '$path'";
diff --git a/pkgs/file/lib/src/backends/local/local_file.dart b/pkgs/file/lib/src/backends/local/local_file.dart
index 36293ba..a4bc106 100644
--- a/pkgs/file/lib/src/backends/local/local_file.dart
+++ b/pkgs/file/lib/src/backends/local/local_file.dart
@@ -2,9 +2,9 @@
 // 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:file/file.dart';
-import 'package:file/src/io.dart' as io;
-
+import '../../forwarding.dart';
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'local_file_system_entity.dart';
 
 /// [File] implementation that forwards all calls to `dart:io`.
@@ -12,7 +12,7 @@
     with ForwardingFile {
   /// Instantiates a new [LocalFile] tied to the specified file system
   /// and delegating to the specified [delegate].
-  LocalFile(FileSystem fs, io.File delegate) : super(fs, delegate);
+  LocalFile(super.fs, super.delegate);
 
   @override
   String toString() => "LocalFile: '$path'";
diff --git a/pkgs/file/lib/src/backends/local/local_file_system.dart b/pkgs/file/lib/src/backends/local/local_file_system.dart
index 635998e..7541c37 100644
--- a/pkgs/file/lib/src/backends/local/local_file_system.dart
+++ b/pkgs/file/lib/src/backends/local/local_file_system.dart
@@ -2,10 +2,10 @@
 // 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
 import 'package:path/path.dart' as p;
 
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'local_directory.dart';
 import 'local_file.dart';
 import 'local_link.dart';
diff --git a/pkgs/file/lib/src/backends/local/local_file_system_entity.dart b/pkgs/file/lib/src/backends/local/local_file_system_entity.dart
index ca4617b..d0da559 100644
--- a/pkgs/file/lib/src/backends/local/local_file_system_entity.dart
+++ b/pkgs/file/lib/src/backends/local/local_file_system_entity.dart
@@ -2,9 +2,9 @@
 // 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:file/file.dart';
-import 'package:file/src/io.dart' as io;
-
+import '../../forwarding.dart';
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'local_directory.dart';
 import 'local_file.dart';
 import 'local_link.dart';
diff --git a/pkgs/file/lib/src/backends/local/local_link.dart b/pkgs/file/lib/src/backends/local/local_link.dart
index fc67d5e..2ce4791 100644
--- a/pkgs/file/lib/src/backends/local/local_link.dart
+++ b/pkgs/file/lib/src/backends/local/local_link.dart
@@ -2,9 +2,9 @@
 // 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:file/file.dart';
-import 'package:file/src/io.dart' as io;
-
+import '../../forwarding.dart';
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'local_file_system_entity.dart';
 
 /// [Link] implementation that forwards all calls to `dart:io`.
@@ -12,7 +12,7 @@
     with ForwardingLink {
   /// Instantiates a new [LocalLink] tied to the specified file system
   /// and delegating to the specified [delegate].
-  LocalLink(FileSystem fs, io.Link delegate) : super(fs, delegate);
+  LocalLink(super.fs, super.delegate);
 
   @override
   String toString() => "LocalLink: '$path'";
diff --git a/pkgs/file/lib/src/backends/memory/clock.dart b/pkgs/file/lib/src/backends/memory/clock.dart
index 98d5434..57c1b72 100644
--- a/pkgs/file/lib/src/backends/memory/clock.dart
+++ b/pkgs/file/lib/src/backends/memory/clock.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// ignore_for_file: comment_references
+
 /// Interface describing clocks used by the [MemoryFileSystem].
 ///
 /// The [MemoryFileSystem] uses a clock to determine the modification times of
diff --git a/pkgs/file/lib/src/backends/memory/common.dart b/pkgs/file/lib/src/backends/memory/common.dart
index 80e3c38..eb4ca43 100644
--- a/pkgs/file/lib/src/backends/memory/common.dart
+++ b/pkgs/file/lib/src/backends/memory/common.dart
@@ -2,7 +2,7 @@
 // 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:file/src/common.dart' as common;
+import '../../common.dart' as common;
 
 /// Generates a path to use in error messages.
 typedef PathGenerator = dynamic Function();
diff --git a/pkgs/file/lib/src/backends/memory/memory_directory.dart b/pkgs/file/lib/src/backends/memory/memory_directory.dart
index 95fe542..e73b967 100644
--- a/pkgs/file/lib/src/backends/memory/memory_directory.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_directory.dart
@@ -2,11 +2,11 @@
 // 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
 import 'package:meta/meta.dart';
 
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'common.dart';
 import 'memory_file.dart';
 import 'memory_file_system_entity.dart';
@@ -25,8 +25,7 @@
     with common.DirectoryAddOnsMixin
     implements Directory {
   /// Instantiates a new [MemoryDirectory].
-  MemoryDirectory(NodeBasedFileSystem fileSystem, String path)
-      : super(fileSystem, path);
+  MemoryDirectory(super.fileSystem, super.path);
 
   @override
   io.FileSystemEntityType get expectedType => io.FileSystemEntityType.directory;
@@ -52,7 +51,7 @@
   @override
   void createSync({bool recursive = false}) {
     fileSystem.opHandle(path, FileSystemOp.create);
-    Node? node = internalCreateSync(
+    var node = internalCreateSync(
       followTailLink: true,
       visitLinks: true,
       createChild: (DirectoryNode parent, bool isFinalSegment) {
@@ -75,19 +74,19 @@
   @override
   Directory createTempSync([String? prefix]) {
     prefix = '${prefix ?? ''}rand';
-    String fullPath = fileSystem.path.join(path, prefix);
-    String dirname = fileSystem.path.dirname(fullPath);
-    String basename = fileSystem.path.basename(fullPath);
-    DirectoryNode? node = fileSystem.findNode(dirname) as DirectoryNode?;
+    var fullPath = fileSystem.path.join(path, prefix);
+    var dirname = fileSystem.path.dirname(fullPath);
+    var basename = fileSystem.path.basename(fullPath);
+    var node = fileSystem.findNode(dirname) as DirectoryNode?;
     checkExists(node, () => dirname);
     utils.checkIsDir(node!, () => dirname);
-    int tempCounter = _systemTempCounter[fileSystem] ?? 0;
+    var tempCounter = _systemTempCounter[fileSystem] ?? 0;
     String name() => '$basename$tempCounter';
     while (node.children.containsKey(name())) {
       tempCounter++;
     }
     _systemTempCounter[fileSystem] = tempCounter;
-    DirectoryNode tempDir = DirectoryNode(node);
+    var tempDir = DirectoryNode(node);
     node.children[name()] = tempDir;
     return MemoryDirectory(fileSystem, fileSystem.path.join(dirname, name()))
       ..createSync();
@@ -128,9 +127,9 @@
     bool recursive = false,
     bool followLinks = true,
   }) {
-    DirectoryNode node = backing as DirectoryNode;
-    List<FileSystemEntity> listing = <FileSystemEntity>[];
-    List<_PendingListTask> tasks = <_PendingListTask>[
+    var node = backing as DirectoryNode;
+    var listing = <FileSystemEntity>[];
+    var tasks = <_PendingListTask>[
       _PendingListTask(
         node,
         path.endsWith(fileSystem.path.separator)
@@ -140,14 +139,14 @@
       ),
     ];
     while (tasks.isNotEmpty) {
-      _PendingListTask task = tasks.removeLast();
+      var task = tasks.removeLast();
       task.dir.children.forEach((String name, Node child) {
-        Set<LinkNode> breadcrumbs = Set<LinkNode>.from(task.breadcrumbs);
-        String childPath = fileSystem.path.join(task.path, name);
+        var breadcrumbs = Set<LinkNode>.from(task.breadcrumbs);
+        var childPath = fileSystem.path.join(task.path, name);
         while (followLinks &&
             utils.isLink(child) &&
             breadcrumbs.add(child as LinkNode)) {
-          Node? referent = child.referentOrNull;
+          var referent = child.referentOrNull;
           if (referent != null) {
             child = referent;
           }
diff --git a/pkgs/file/lib/src/backends/memory/memory_file.dart b/pkgs/file/lib/src/backends/memory/memory_file.dart
index ba4faab..1a8f5f9 100644
--- a/pkgs/file/lib/src/backends/memory/memory_file.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_file.dart
@@ -7,26 +7,25 @@
 import 'dart:math' as math show min;
 import 'dart:typed_data';
 
-import 'package:file/file.dart';
-import 'package:file/src/backends/memory/operations.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
 import 'package:meta/meta.dart';
 
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'common.dart';
 import 'memory_file_system_entity.dart';
 import 'memory_random_access_file.dart';
 import 'node.dart';
+import 'operations.dart';
 import 'utils.dart' as utils;
 
 /// Internal implementation of [File].
 class MemoryFile extends MemoryFileSystemEntity implements File {
   /// Instantiates a new [MemoryFile].
-  const MemoryFile(NodeBasedFileSystem fileSystem, String path)
-      : super(fileSystem, path);
+  const MemoryFile(super.fileSystem, super.path);
 
   FileNode get _resolvedBackingOrCreate {
-    Node? node = backingOrNull;
+    var node = backingOrNull;
     if (node == null) {
       node = _doCreate();
     } else {
@@ -61,7 +60,7 @@
   }
 
   Node? _doCreate({bool recursive = false}) {
-    Node? node = internalCreateSync(
+    var node = internalCreateSync(
       followTailLink: true,
       createChild: (DirectoryNode parent, bool isFinalSegment) {
         if (isFinalSegment) {
@@ -88,7 +87,7 @@
         newPath,
         followTailLink: true,
         checkType: (Node node) {
-          FileSystemEntityType actualType = node.stat.type;
+          var actualType = node.stat.type;
           if (actualType != expectedType) {
             throw actualType == FileSystemEntityType.notFound
                 ? common.noSuchFileOrDirectory(path)
@@ -103,7 +102,7 @@
   @override
   File copySync(String newPath) {
     fileSystem.opHandle(path, FileSystemOp.copy);
-    FileNode sourceNode = resolvedBacking as FileNode;
+    var sourceNode = resolvedBacking as FileNode;
     fileSystem.findNode(
       newPath,
       segmentVisitor: (
@@ -116,7 +115,7 @@
         if (currentSegment == finalSegment) {
           if (child != null) {
             if (utils.isLink(child)) {
-              List<String> ledger = <String>[];
+              var ledger = <String>[];
               child = utils.resolveLinks(child as LinkNode, () => newPath,
                   ledger: ledger);
               checkExists(child, () => newPath);
@@ -127,7 +126,7 @@
             utils.checkType(expectedType, child.type, () => newPath);
             parent.children.remove(childName);
           }
-          FileNode newNode = FileNode(parent);
+          var newNode = FileNode(parent);
           newNode.copyFrom(sourceNode);
           parent.children[childName] = newNode;
         }
@@ -158,7 +157,7 @@
 
   @override
   void setLastAccessedSync(DateTime time) {
-    FileNode node = resolvedBacking as FileNode;
+    var node = resolvedBacking as FileNode;
     node.accessed = time.millisecondsSinceEpoch;
   }
 
@@ -174,7 +173,7 @@
 
   @override
   void setLastModifiedSync(DateTime time) {
-    FileNode node = resolvedBacking as FileNode;
+    var node = resolvedBacking as FileNode;
     node.modified = time.millisecondsSinceEpoch;
   }
 
@@ -199,8 +198,8 @@
   Stream<List<int>> openRead([int? start, int? end]) {
     fileSystem.opHandle(path, FileSystemOp.open);
     try {
-      FileNode node = resolvedBacking as FileNode;
-      Uint8List content = node.content;
+      var node = resolvedBacking as FileNode;
+      var content = node.content;
       if (start != null) {
         content = end == null
             ? content.sublist(start)
@@ -253,13 +252,13 @@
 
   @override
   List<String> readAsLinesSync({Encoding encoding = utf8}) {
-    String str = readAsStringSync(encoding: encoding);
+    var str = readAsStringSync(encoding: encoding);
 
     if (str.isEmpty) {
       return <String>[];
     }
 
-    final List<String> lines = str.split('\n');
+    final lines = str.split('\n');
     if (str.endsWith('\n')) {
       // A final newline should not create an additional line.
       lines.removeLast();
@@ -287,7 +286,7 @@
     if (!utils.isWriteMode(mode)) {
       throw common.badFileDescriptor(path);
     }
-    FileNode node = _resolvedBackingOrCreate;
+    var node = _resolvedBackingOrCreate;
     _truncateIfNecessary(node, mode);
     fileSystem.opHandle(path, FileSystemOp.write);
     node.write(bytes);
@@ -349,7 +348,7 @@
       deferredException = e;
     }
 
-    Future<FileNode> future = Future<FileNode>.microtask(() {
+    var future = Future<FileNode>.microtask(() {
       if (deferredException != null) {
         throw deferredException;
       }
@@ -387,7 +386,7 @@
 
   @override
   void writeAll(Iterable<dynamic> objects, [String separator = '']) {
-    bool firstIter = true;
+    var firstIter = true;
     for (dynamic obj in objects) {
       if (!firstIter) {
         write(separator);
@@ -418,7 +417,7 @@
     _streamCompleter = Completer<void>();
 
     stream.listen(
-      (List<int> data) => _addData(data),
+      _addData,
       cancelOnError: true,
       onError: (Object error, StackTrace stackTrace) {
         _streamCompleter!.completeError(error, stackTrace);
@@ -445,8 +444,7 @@
       _isClosed = true;
       _pendingWrites.then(
         (_) => _completer.complete(),
-        onError: (Object error, StackTrace stackTrace) =>
-            _completer.completeError(error, stackTrace),
+        onError: _completer.completeError,
       );
     }
     return _completer.future;
diff --git a/pkgs/file/lib/src/backends/memory/memory_file_stat.dart b/pkgs/file/lib/src/backends/memory/memory_file_stat.dart
index 94f86d1..ce6beda 100644
--- a/pkgs/file/lib/src/backends/memory/memory_file_stat.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_file_stat.dart
@@ -2,7 +2,7 @@
 // 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:file/src/io.dart' as io;
+import '../../io.dart' as io;
 
 /// Internal implementation of [io.FileStat].
 class MemoryFileStat implements io.FileStat {
@@ -47,8 +47,8 @@
 
   @override
   String modeString() {
-    int permissions = mode & 0xFFF;
-    List<String> codes = const <String>[
+    var permissions = mode & 0xFFF;
+    var codes = const <String>[
       '---',
       '--x',
       '-w-',
@@ -58,7 +58,7 @@
       'rw-',
       'rwx',
     ];
-    List<String> result = <String>[];
+    var result = <String>[];
     result
       ..add(codes[(permissions >> 6) & 0x7])
       ..add(codes[(permissions >> 3) & 0x7])
diff --git a/pkgs/file/lib/src/backends/memory/memory_file_system.dart b/pkgs/file/lib/src/backends/memory/memory_file_system.dart
index f3cdaee..dd359f0 100644
--- a/pkgs/file/lib/src/backends/memory/memory_file_system.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_file_system.dart
@@ -2,11 +2,10 @@
 // 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:file/file.dart';
-import 'package:file/src/backends/memory/operations.dart';
-import 'package:file/src/io.dart' as io;
 import 'package:path/path.dart' as p;
 
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'clock.dart';
 import 'common.dart';
 import 'memory_directory.dart';
@@ -14,6 +13,7 @@
 import 'memory_file_stat.dart';
 import 'memory_link.dart';
 import 'node.dart';
+import 'operations.dart';
 import 'style.dart';
 import 'utils.dart' as utils;
 
@@ -91,7 +91,7 @@
   p.Context _context;
 
   @override
-  final Function(String context, FileSystemOp operation) opHandle;
+  final void Function(String context, FileSystemOp operation) opHandle;
 
   @override
   final Clock clock;
@@ -141,7 +141,7 @@
     }
 
     value = directory(value).resolveSymbolicLinksSync();
-    Node? node = findNode(value);
+    var node = findNode(value);
     checkExists(node, () => value);
     utils.checkIsDir(node!, () => value);
     assert(_context.isAbsolute(value));
@@ -166,9 +166,9 @@
 
   @override
   bool identicalSync(String path1, String path2) {
-    Node? node1 = findNode(path1);
+    var node1 = findNode(path1);
     checkExists(node1, () => path1);
-    Node? node2 = findNode(path2);
+    var node2 = findNode(path2);
     checkExists(node2, () => path2);
     return node1 != null && node1 == node2;
   }
@@ -220,14 +220,13 @@
       reference ??= _current;
     }
 
-    List<String> parts = path.split(style.separator)
-      ..removeWhere(utils.isEmpty);
-    DirectoryNode? directory = reference?.directory;
+    var parts = path.split(style.separator)..removeWhere(utils.isEmpty);
+    var directory = reference?.directory;
     Node? child = directory;
 
-    int finalSegment = parts.length - 1;
-    for (int i = 0; i <= finalSegment; i++) {
-      String basename = parts[i];
+    var finalSegment = parts.length - 1;
+    for (var i = 0; i <= finalSegment; i++) {
+      var basename = parts[i];
       assert(basename.isNotEmpty);
 
       switch (basename) {
diff --git a/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart b/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart
index ad987d7..1990abc 100644
--- a/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart
@@ -2,11 +2,11 @@
 // 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
 import 'package:meta/meta.dart';
 
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'common.dart';
 import 'memory_directory.dart';
 import 'node.dart';
@@ -60,7 +60,7 @@
   /// The type of the node is not guaranteed to match [expectedType].
   @protected
   Node get backing {
-    Node? node = fileSystem.findNode(path);
+    var node = fileSystem.findNode(path);
     checkExists(node, () => path);
     return node!;
   }
@@ -71,7 +71,7 @@
   /// doesn't match, this will throw a [io.FileSystemException].
   @protected
   Node get resolvedBacking {
-    Node node = backing;
+    var node = backing;
     node = utils.isLink(node)
         ? utils.resolveLinks(node as LinkNode, () => path)
         : node;
@@ -107,14 +107,14 @@
     if (path.isEmpty) {
       throw common.noSuchFileOrDirectory(path);
     }
-    List<String> ledger = <String>[];
+    var ledger = <String>[];
     if (isAbsolute) {
       ledger.add(fileSystem.style.drive);
     }
-    Node? node = fileSystem.findNode(path,
+    var node = fileSystem.findNode(path,
         pathWithSymlinks: ledger, followTailLink: true);
     checkExists(node, () => path);
-    String resolved = ledger.join(fileSystem.path.separator);
+    var resolved = ledger.join(fileSystem.path.separator);
     if (resolved == fileSystem.style.drive) {
       resolved = fileSystem.style.root;
     } else if (!fileSystem.path.isAbsolute(resolved)) {
@@ -151,7 +151,7 @@
 
   @override
   FileSystemEntity get absolute {
-    String absolutePath = path;
+    var absolutePath = path;
     if (!fileSystem.path.isAbsolute(absolutePath)) {
       absolutePath = fileSystem.path.join(fileSystem.cwd, absolutePath);
     }
@@ -242,7 +242,7 @@
     bool followTailLink = false,
     utils.TypeChecker? checkType,
   }) {
-    Node node = backing;
+    var node = backing;
     (checkType ?? defaultCheckType)(node);
     fileSystem.findNode(
       newPath,
@@ -256,7 +256,7 @@
         if (currentSegment == finalSegment) {
           if (child != null) {
             if (followTailLink) {
-              FileSystemEntityType childType = child.stat.type;
+              var childType = child.stat.type;
               if (childType != FileSystemEntityType.notFound) {
                 utils.checkType(expectedType, child.stat.type, () => newPath);
               }
@@ -289,7 +289,7 @@
     utils.TypeChecker? checkType,
   }) {
     fileSystem.opHandle(path, FileSystemOp.delete);
-    Node node = backing;
+    var node = backing;
     if (!recursive) {
       if (node is DirectoryNode && node.children.isNotEmpty) {
         throw common.directoryNotEmpty(path);
diff --git a/pkgs/file/lib/src/backends/memory/memory_link.dart b/pkgs/file/lib/src/backends/memory/memory_link.dart
index 7d5afb4..a599fe8 100644
--- a/pkgs/file/lib/src/backends/memory/memory_link.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_link.dart
@@ -2,11 +2,11 @@
 // 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
 import 'package:meta/meta.dart';
 
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'memory_file_system_entity.dart';
 import 'node.dart';
 import 'operations.dart';
@@ -15,8 +15,7 @@
 /// Internal implementation of [Link].
 class MemoryLink extends MemoryFileSystemEntity implements Link {
   /// Instantiates a new [MemoryLink].
-  const MemoryLink(NodeBasedFileSystem fileSystem, String path)
-      : super(fileSystem, path);
+  const MemoryLink(super.fileSystem, super.path);
 
   @override
   io.FileSystemEntityType get expectedType => io.FileSystemEntityType.link;
@@ -50,7 +49,7 @@
 
   @override
   void createSync(String target, {bool recursive = false}) {
-    bool preexisting = true;
+    var preexisting = true;
     fileSystem.opHandle(path, FileSystemOp.create);
     internalCreateSync(
         createChild: (DirectoryNode parent, bool isFinalSegment) {
@@ -76,7 +75,7 @@
 
   @override
   void updateSync(String target) {
-    Node node = backing;
+    var node = backing;
     utils.checkType(expectedType, node.type, () => path);
     (node as LinkNode).target = target;
   }
@@ -93,7 +92,7 @@
 
   @override
   String targetSync() {
-    Node node = backing;
+    var node = backing;
     if (node.type != expectedType) {
       // Note: this may change; https://github.com/dart-lang/sdk/issues/28204
       throw common.noSuchFileOrDirectory(path);
diff --git a/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart b/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart
index d4fe73d..190f0a1 100644
--- a/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart
@@ -6,10 +6,11 @@
 import 'dart:math' as math show min;
 import 'dart:typed_data';
 
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
-
+import '../../common.dart' as common;
+import '../../io.dart' as io;
+import '../memory.dart' show MemoryFileSystem;
 import 'memory_file.dart';
+import 'memory_file_system.dart' show MemoryFileSystem;
 import 'node.dart';
 import 'utils.dart' as utils;
 
@@ -106,8 +107,8 @@
   /// Wraps a synchronous function to make it appear asynchronous.
   ///
   /// [_asyncOperationPending], [_checkAsync], and [_asyncWrapper] are used to
-  /// mimic [RandomAccessFile]'s enforcement that only one asynchronous
-  /// operation is pending for a [RandomAccessFile] instance.  Since
+  /// mimic [io.RandomAccessFile]'s enforcement that only one asynchronous
+  /// operation is pending for a [io.RandomAccessFile] instance.  Since
   /// [MemoryFileSystem]-based classes are likely to be used in tests, fidelity
   /// is important to catch errors that might occur in production.
   ///
@@ -211,7 +212,7 @@
     _checkReadable('read');
     // TODO(jamesderlin): Check for integer overflow.
     final int end = math.min(_position + bytes, lengthSync());
-    final Uint8List copy = _node.content.sublist(_position, end);
+    final copy = _node.content.sublist(_position, end);
     _position = end;
     return copy;
   }
@@ -243,7 +244,7 @@
 
     end = RangeError.checkValidRange(start, end, buffer.length);
 
-    final int length = lengthSync();
+    final length = lengthSync();
     int i;
     for (i = start; i < end && _position < length; i += 1, _position += 1) {
       buffer[i] = _node.content[_position];
@@ -288,7 +289,7 @@
           'truncate failed', path, common.invalidArgument(path).osError);
     }
 
-    final int oldLength = lengthSync();
+    final oldLength = lengthSync();
     if (length < oldLength) {
       _node.truncate(length);
 
@@ -329,7 +330,7 @@
     // [Uint8List] will truncate values to 8-bits automatically, so we don't
     // need to check [value].
 
-    int length = lengthSync();
+    var length = lengthSync();
     if (_position >= length) {
       // If [_position] is out of bounds, [RandomAccessFile] zero-fills the
       // file.
@@ -363,8 +364,8 @@
 
     end = RangeError.checkValidRange(start, end, buffer.length);
 
-    final int writeByteCount = end - start;
-    final int endPosition = _position + writeByteCount;
+    final writeByteCount = end - start;
+    final endPosition = _position + writeByteCount;
 
     if (endPosition > lengthSync()) {
       truncateSync(endPosition);
diff --git a/pkgs/file/lib/src/backends/memory/node.dart b/pkgs/file/lib/src/backends/memory/node.dart
index ae4d3f7..eea72b5 100644
--- a/pkgs/file/lib/src/backends/memory/node.dart
+++ b/pkgs/file/lib/src/backends/memory/node.dart
@@ -4,13 +4,12 @@
 
 import 'dart:typed_data';
 
-import 'package:file/file.dart';
-import 'package:file/src/backends/memory/operations.dart';
-import 'package:file/src/io.dart' as io;
-
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'clock.dart';
 import 'common.dart';
 import 'memory_file_stat.dart';
+import 'operations.dart';
 import 'style.dart';
 
 /// Visitor callback for use with [NodeBasedFileSystem.findNode].
@@ -115,7 +114,7 @@
 
   /// Reparents this node to live in the specified directory.
   set parent(DirectoryNode parent) {
-    DirectoryNode ancestor = parent;
+    var ancestor = parent;
     while (!ancestor.isRoot) {
       if (ancestor == this) {
         throw const io.FileSystemException(
@@ -149,8 +148,8 @@
 /// you call [stat] on them).
 abstract class RealNode extends Node {
   /// Constructs a new [RealNode] as a child of the specified [parent].
-  RealNode(DirectoryNode? parent) : super(parent) {
-    int now = clock.now.millisecondsSinceEpoch;
+  RealNode(super.parent) {
+    var now = clock.now.millisecondsSinceEpoch;
     changed = now;
     modified = now;
     accessed = now;
@@ -195,7 +194,7 @@
 /// Class that represents the backing for an in-memory directory.
 class DirectoryNode extends RealNode {
   /// Constructs a new [DirectoryNode] as a child of the specified [parent].
-  DirectoryNode(DirectoryNode? parent) : super(parent);
+  DirectoryNode(super.parent);
 
   /// Child nodes, indexed by their basename.
   final Map<String, Node> children = <String, Node>{};
@@ -237,7 +236,7 @@
 /// Class that represents the backing for an in-memory regular file.
 class FileNode extends RealNode {
   /// Constructs a new [FileNode] as a child of the specified [parent].
-  FileNode(DirectoryNode parent) : super(parent);
+  FileNode(DirectoryNode super.parent);
 
   /// File contents in bytes.
   Uint8List get content => _content;
@@ -251,7 +250,7 @@
 
   /// Appends the specified bytes to the end of this node's [content].
   void write(List<int> bytes) {
-    Uint8List existing = _content;
+    var existing = _content;
     _content = Uint8List(existing.length + bytes.length);
     _content.setRange(0, existing.length, existing);
     _content.setRange(existing.length, _content.length, bytes);
@@ -286,9 +285,7 @@
 class LinkNode extends Node {
   /// Constructs a new [LinkNode] as a child of the specified [parent] and
   /// linking to the specified [target] path.
-  LinkNode(DirectoryNode parent, this.target)
-      : assert(target.isNotEmpty),
-        super(parent);
+  LinkNode(DirectoryNode super.parent, this.target) : assert(target.isNotEmpty);
 
   /// The path to which this link points.
   String target;
@@ -309,7 +306,7 @@
     Node? Function(DirectoryNode parent, String childName, Node? child)?
         tailVisitor,
   }) {
-    Node? referent = fs.findNode(
+    var referent = fs.findNode(
       target,
       reference: this,
       segmentVisitor: (
@@ -349,7 +346,7 @@
     }
     _reentrant = true;
     try {
-      Node? node = referentOrNull;
+      var node = referentOrNull;
       return node == null ? MemoryFileStat.notFound : node.stat;
     } finally {
       _reentrant = false;
diff --git a/pkgs/file/lib/src/backends/memory/operations.dart b/pkgs/file/lib/src/backends/memory/operations.dart
index 9fc7462..57d118b 100644
--- a/pkgs/file/lib/src/backends/memory/operations.dart
+++ b/pkgs/file/lib/src/backends/memory/operations.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// ignore_for_file: comment_references
+
 /// A file system operation used by the [MemoryFileSytem] to allow
 /// tests to insert errors for certain operations.
 ///
@@ -64,23 +66,15 @@
 
   @override
   String toString() {
-    switch (_value) {
-      case 0:
-        return 'FileSystemOp.read';
-      case 1:
-        return 'FileSystemOp.write';
-      case 2:
-        return 'FileSystemOp.delete';
-      case 3:
-        return 'FileSystemOp.create';
-      case 4:
-        return 'FileSystemOp.open';
-      case 5:
-        return 'FileSystemOp.copy';
-      case 6:
-        return 'FileSystemOp.exists';
-      default:
-        throw StateError('Invalid FileSytemOp type: $this');
-    }
+    return switch (_value) {
+      0 => 'FileSystemOp.read',
+      1 => 'FileSystemOp.write',
+      2 => 'FileSystemOp.delete',
+      3 => 'FileSystemOp.create',
+      4 => 'FileSystemOp.open',
+      5 => 'FileSystemOp.copy',
+      6 => 'FileSystemOp.exists',
+      _ => throw StateError('Invalid FileSytemOp type: $this')
+    };
   }
 }
diff --git a/pkgs/file/lib/src/backends/memory/style.dart b/pkgs/file/lib/src/backends/memory/style.dart
index 701c9d0..f4bd33f 100644
--- a/pkgs/file/lib/src/backends/memory/style.dart
+++ b/pkgs/file/lib/src/backends/memory/style.dart
@@ -2,9 +2,10 @@
 // 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:file/file.dart';
 import 'package:path/path.dart' as p;
 
+import '../../interface.dart';
+
 /// Class that represents the path style that a memory file system should
 /// adopt.
 ///
diff --git a/pkgs/file/lib/src/backends/memory/utils.dart b/pkgs/file/lib/src/backends/memory/utils.dart
index eec9980..aa24cfb 100644
--- a/pkgs/file/lib/src/backends/memory/utils.dart
+++ b/pkgs/file/lib/src/backends/memory/utils.dart
@@ -2,20 +2,19 @@
 // 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
-
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
 import 'common.dart';
 import 'node.dart';
 
-/// Checks if `node.type` returns [io.FileSystemEntityType.FILE].
+/// Checks if `node.type` returns [io.FileSystemEntityType.file].
 bool isFile(Node? node) => node?.type == io.FileSystemEntityType.file;
 
-/// Checks if `node.type` returns [io.FileSystemEntityType.DIRECTORY].
+/// Checks if `node.type` returns [io.FileSystemEntityType.directory].
 bool isDirectory(Node? node) => node?.type == io.FileSystemEntityType.directory;
 
-/// Checks if `node.type` returns [io.FileSystemEntityType.LINK].
+/// Checks if `node.type` returns [io.FileSystemEntityType.link].
 bool isLink(Node? node) => node?.type == io.FileSystemEntityType.link;
 
 /// Validator function that is expected to throw a [FileSystemException] if
@@ -86,7 +85,7 @@
       tailVisitor,
 }) {
   // Record a breadcrumb trail to guard against symlink loops.
-  Set<LinkNode> breadcrumbs = <LinkNode>{};
+  var breadcrumbs = <LinkNode>{};
 
   Node node = link;
   while (isLink(node)) {
diff --git a/pkgs/file/lib/src/forwarding/forwarding_directory.dart b/pkgs/file/lib/src/forwarding/forwarding_directory.dart
index dba0c8e..ad1c548 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_directory.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_directory.dart
@@ -2,8 +2,9 @@
 // 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
+import '../forwarding.dart';
+import '../interface.dart';
+import '../io.dart' as io;
 
 /// A directory that forwards all methods and properties to a delegate.
 mixin ForwardingDirectory<T extends Directory>
diff --git a/pkgs/file/lib/src/forwarding/forwarding_file.dart b/pkgs/file/lib/src/forwarding/forwarding_file.dart
index 49c211d..d6cfe3b 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_file.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_file.dart
@@ -5,8 +5,9 @@
 import 'dart:convert';
 import 'dart:typed_data';
 
-import 'package:file/src/io.dart' as io;
-import 'package:file/file.dart';
+import '../forwarding.dart';
+import '../interface.dart';
+import '../io.dart' as io;
 
 /// A file that forwards all methods and properties to a delegate.
 mixin ForwardingFile
diff --git a/pkgs/file/lib/src/forwarding/forwarding_file_system.dart b/pkgs/file/lib/src/forwarding/forwarding_file_system.dart
index d864db9..885fdb6 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_file_system.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_file_system.dart
@@ -2,11 +2,12 @@
 // 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
 import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 
+import '../interface.dart';
+import '../io.dart' as io;
+
 /// A file system that forwards all methods and properties to a delegate.
 abstract class ForwardingFileSystem extends FileSystem {
   /// Creates a new [ForwardingFileSystem] that forwards all methods and
diff --git a/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart b/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart
index 3c41b39..1c0628e 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart
@@ -2,10 +2,11 @@
 // 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
 import 'package:meta/meta.dart';
 
+import '../interface.dart';
+import '../io.dart' as io;
+
 /// A file system entity that forwards all methods and properties to a delegate.
 abstract class ForwardingFileSystemEntity<T extends FileSystemEntity,
     D extends io.FileSystemEntity> implements FileSystemEntity {
diff --git a/pkgs/file/lib/src/forwarding/forwarding_link.dart b/pkgs/file/lib/src/forwarding/forwarding_link.dart
index 7a60ecb..915e710 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_link.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_link.dart
@@ -2,8 +2,9 @@
 // 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
+import '../forwarding.dart';
+import '../interface.dart';
+import '../io.dart' as io;
 
 /// A link that forwards all methods and properties to a delegate.
 mixin ForwardingLink
diff --git a/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart b/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart
index 9dd4079..3847b91 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart
@@ -5,11 +5,12 @@
 import 'dart:convert';
 import 'dart:typed_data';
 
-import 'package:file/src/io.dart' as io;
 import 'package:meta/meta.dart';
 
-/// A [RandomAccessFile] implementation that forwards all methods and properties
-/// to a delegate.
+import '../io.dart' as io;
+
+/// A [io.RandomAccessFile] implementation that forwards all methods and
+/// properties to a delegate.
 mixin ForwardingRandomAccessFile implements io.RandomAccessFile {
   /// The entity to which this entity will forward all methods and properties.
   @protected
diff --git a/pkgs/file/lib/src/interface.dart b/pkgs/file/lib/src/interface.dart
index 4662e35..d9b7ed5 100644
--- a/pkgs/file/lib/src/interface.dart
+++ b/pkgs/file/lib/src/interface.dart
@@ -2,8 +2,6 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-library file.src.interface;
-
 export 'interface/directory.dart';
 export 'interface/error_codes.dart';
 export 'interface/file.dart';
diff --git a/pkgs/file/lib/src/interface/error_codes.dart b/pkgs/file/lib/src/interface/error_codes.dart
index 8943538..4836b56 100644
--- a/pkgs/file/lib/src/interface/error_codes.dart
+++ b/pkgs/file/lib/src/interface/error_codes.dart
@@ -168,7 +168,7 @@
   static int get EXDEV => _platform((_Codes codes) => codes.exdev);
 
   static int _platform(int Function(_Codes codes) getCode) {
-    _Codes codes = (_platforms[operatingSystem] ?? _platforms['linux'])!;
+    var codes = (_platforms[operatingSystem] ?? _platforms['linux'])!;
     return getCode(codes);
   }
 }
diff --git a/pkgs/file/lib/src/interface/file_system.dart b/pkgs/file/lib/src/interface/file_system.dart
index ecc01a8..2d4e4aa 100644
--- a/pkgs/file/lib/src/interface/file_system.dart
+++ b/pkgs/file/lib/src/interface/file_system.dart
@@ -6,7 +6,6 @@
 import 'package:path/path.dart' as p;
 
 import '../io.dart' as io;
-
 import 'directory.dart';
 import 'file.dart';
 import 'file_system_entity.dart';
@@ -99,9 +98,9 @@
   bool get isWatchSupported;
 
   /// Finds the type of file system object that a [path] points to. Returns
-  /// a Future<FileSystemEntityType> that completes with the result.
+  /// a `Future<FileSystemEntityType>` that completes with the result.
   ///
-  /// [io.FileSystemEntityType.LINK] will only be returned if [followLinks] is
+  /// [io.FileSystemEntityType.link] will only be returned if [followLinks] is
   /// `false`, and [path] points to a link
   ///
   /// If the [path] does not point to a file system object or an error occurs
@@ -111,37 +110,38 @@
   /// Syncronously finds the type of file system object that a [path] points
   /// to. Returns a [io.FileSystemEntityType].
   ///
-  /// [io.FileSystemEntityType.LINK] will only be returned if [followLinks] is
+  /// [io.FileSystemEntityType.link] will only be returned if [followLinks] is
   /// `false`, and [path] points to a link
   ///
   /// If the [path] does not point to a file system object or an error occurs
   /// then [io.FileSystemEntityType.notFound] is returned.
   io.FileSystemEntityType typeSync(String path, {bool followLinks = true});
 
-  /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.FILE].
+  /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.file].
   Future<bool> isFile(String path) async =>
       await type(path) == io.FileSystemEntityType.file;
 
   /// Synchronously checks if [`type(path)`](type) returns
-  /// [io.FileSystemEntityType.FILE].
+  /// [io.FileSystemEntityType.file].
   bool isFileSync(String path) =>
       typeSync(path) == io.FileSystemEntityType.file;
 
-  /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.DIRECTORY].
+  /// Checks if [`type(path)`](type) returns
+  /// [io.FileSystemEntityType.directory].
   Future<bool> isDirectory(String path) async =>
       await type(path) == io.FileSystemEntityType.directory;
 
   /// Synchronously checks if [`type(path)`](type) returns
-  /// [io.FileSystemEntityType.DIRECTORY].
+  /// [io.FileSystemEntityType.directory].
   bool isDirectorySync(String path) =>
       typeSync(path) == io.FileSystemEntityType.directory;
 
-  /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.LINK].
+  /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.link].
   Future<bool> isLink(String path) async =>
       await type(path, followLinks: false) == io.FileSystemEntityType.link;
 
   /// Synchronously checks if [`type(path)`](type) returns
-  /// [io.FileSystemEntityType.LINK].
+  /// [io.FileSystemEntityType.link].
   bool isLinkSync(String path) =>
       typeSync(path, followLinks: false) == io.FileSystemEntityType.link;
 
diff --git a/pkgs/file/lib/src/io.dart b/pkgs/file/lib/src/io.dart
index 9d57e78..28c1d6d 100644
--- a/pkgs/file/lib/src/io.dart
+++ b/pkgs/file/lib/src/io.dart
@@ -8,6 +8,8 @@
 /// the `file` package. The `file` package re-exports these interfaces (or in
 /// some cases, implementations of these interfaces by the same name), so this
 /// file need not be exposes publicly and exists for internal use only.
+library;
+
 export 'dart:io'
     show
         Directory,
diff --git a/pkgs/file/pubspec.yaml b/pkgs/file/pubspec.yaml
index 5de5d37..0ad65b0 100644
--- a/pkgs/file/pubspec.yaml
+++ b/pkgs/file/pubspec.yaml
@@ -1,5 +1,5 @@
 name: file
-version: 7.0.1
+version: 7.0.2-wip
 description: A pluggable, mockable file system abstraction for Dart.
 repository: https://github.com/dart-lang/tools/tree/main/pkgs/file
 issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Afile
@@ -12,6 +12,10 @@
   path: ^1.8.3
 
 dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
   file_testing: ^3.0.0
-  lints: ^2.0.1
   test: ^1.23.1
+
+dependency_overrides:
+  file_testing:
+    path: ../file_testing
diff --git a/pkgs/file/test/chroot_test.dart b/pkgs/file/test/chroot_test.dart
index 6c34ff2..cf23f47 100644
--- a/pkgs/file/test/chroot_test.dart
+++ b/pkgs/file/test/chroot_test.dart
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 
 @TestOn('vm')
+library;
+
 import 'dart:io' as io;
 
 import 'package:file/chroot.dart';
@@ -17,14 +19,15 @@
 void main() {
   group('ChrootFileSystem', () {
     ChrootFileSystem createMemoryBackedChrootFileSystem() {
-      MemoryFileSystem fs = MemoryFileSystem();
+      var fs = MemoryFileSystem();
       fs.directory('/tmp').createSync();
       return ChrootFileSystem(fs, '/tmp');
     }
 
     // TODO(jamesderlin): Make ChrootFile.openSync return a delegating
     // RandomAccessFile that uses the chroot'd path.
-    List<String> skipCommon = <String>[
+    var skipCommon = <String>[
+      // ignore: lines_longer_than_80_chars
       'File > open > .* > RandomAccessFile > read > openReadHandleDoesNotChange',
       'File > open > .* > RandomAccessFile > openWriteHandleDoesNotChange',
     ];
@@ -137,6 +140,7 @@
           test('referencesRootEntityForJailbreakPath', () {
             mem.file('/foo').createSync();
             dynamic f = fs.file('../foo');
+            // ignore: avoid_dynamic_calls
             expect(f.delegate.path, '/tmp/foo');
           });
         });
@@ -151,7 +155,7 @@
 
         group('copy', () {
           test('copiesToRootDirectoryIfDestinationIsJailbreakPath', () {
-            File f = fs.file('/foo')..createSync();
+            var f = fs.file('/foo')..createSync();
             f.copySync('../bar');
             expect(mem.file('/bar'), isNot(exists));
             expect(mem.file('/tmp/bar'), exists);
diff --git a/pkgs/file/test/common_tests.dart b/pkgs/file/test/common_tests.dart
index 6028c77..491d4f9 100644
--- a/pkgs/file/test/common_tests.dart
+++ b/pkgs/file/test/common_tests.dart
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 
 @TestOn('vm')
+library;
+
 import 'dart:async';
 import 'dart:convert';
 import 'dart:io' as io;
@@ -10,8 +12,8 @@
 import 'package:file/file.dart';
 import 'package:file_testing/file_testing.dart';
 import 'package:path/path.dart' as p;
-import 'package:test/test.dart';
 import 'package:test/test.dart' as testpkg show group, setUp, tearDown, test;
+import 'package:test/test.dart';
 
 import 'utils.dart';
 
@@ -54,7 +56,7 @@
   List<String> skip = const <String>[],
   FileSystemGenerator? replay,
 }) {
-  RootPathGenerator? rootfn = root;
+  var rootfn = root;
 
   group('common', () {
     late FileSystemGenerator createFs;
@@ -62,7 +64,7 @@
     late List<SetUpTearDown> tearDowns;
     late FileSystem fs;
     late String root;
-    List<String> stack = <String>[];
+    var stack = <String>[];
 
     void skipIfNecessary(String description, void Function() callback) {
       stack.add(description);
@@ -105,7 +107,7 @@
               testpkg.setUp(() async {
                 await Future.forEach(setUps, (SetUpTearDown setUp) => setUp());
                 await body();
-                for (SetUpTearDown tearDown in tearDowns) {
+                for (var tearDown in tearDowns) {
                   await tearDown();
                 }
                 createFs = replay;
@@ -115,7 +117,7 @@
               testpkg.test(description, body, skip: skip);
 
               testpkg.tearDown(() async {
-                for (SetUpTearDown tearDown in tearDowns) {
+                for (var tearDown in tearDowns) {
                   await tearDown();
                 }
               });
@@ -126,13 +128,13 @@
     /// Returns [path] prefixed by the [root] namespace.
     /// This is only intended for absolute paths.
     String ns(String path) {
-      p.Context posix = p.Context(style: p.Style.posix);
-      List<String> parts = posix.split(path);
+      var posix = p.Context(style: p.Style.posix);
+      var parts = posix.split(path);
       parts[0] = root;
       path = fs.path.joinAll(parts);
-      String rootPrefix = fs.path.rootPrefix(path);
+      var rootPrefix = fs.path.rootPrefix(path);
       assert(rootPrefix.isNotEmpty);
-      String result = root == rootPrefix
+      var result = root == rootPrefix
           ? path
           : (path == rootPrefix
               ? root
@@ -160,7 +162,7 @@
 
         test('succeedsWithUriArgument', () {
           fs.directory(ns('/foo')).createSync();
-          Uri uri = fs.path.toUri(ns('/foo'));
+          var uri = fs.path.toUri(ns('/foo'));
           expect(fs.directory(uri), exists);
         });
 
@@ -173,11 +175,11 @@
         });
 
         // Fails due to
-        // https://github.com/google/file.dart/issues/112
+        // https://github.com/dart-lang/tools/issues/632
         test('considersBothSlashesEquivalent', () {
           fs.directory(r'foo\bar_dir').createSync(recursive: true);
           expect(fs.directory(r'foo/bar_dir'), exists);
-        }, skip: 'Fails due to https://github.com/google/file.dart/issues/112');
+        }, skip: 'Fails due to https://github.com/dart-lang/tools/issues/632');
       });
 
       group('file', () {
@@ -191,7 +193,7 @@
 
         test('succeedsWithUriArgument', () {
           fs.file(ns('/foo')).createSync();
-          Uri uri = fs.path.toUri(ns('/foo'));
+          var uri = fs.path.toUri(ns('/foo'));
           expect(fs.file(uri), exists);
         });
 
@@ -204,11 +206,11 @@
         });
 
         // Fails due to
-        // https://github.com/google/file.dart/issues/112
+        // https://github.com/dart-lang/tools/issues/632
         test('considersBothSlashesEquivalent', () {
           fs.file(r'foo\bar_file').createSync(recursive: true);
           expect(fs.file(r'foo/bar_file'), exists);
-        }, skip: 'Fails due to https://github.com/google/file.dart/issues/112');
+        }, skip: 'Fails due to https://github.com/dart-lang/tools/issues/632');
       });
 
       group('link', () {
@@ -223,7 +225,7 @@
         test('succeedsWithUriArgument', () {
           fs.file(ns('/foo')).createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          Uri uri = fs.path.toUri(ns('/bar'));
+          var uri = fs.path.toUri(ns('/bar'));
           expect(fs.link(uri), exists);
         });
 
@@ -248,7 +250,7 @@
 
       group('systemTempDirectory', () {
         test('existsAsDirectory', () {
-          Directory tmp = fs.systemTempDirectory;
+          var tmp = fs.systemTempDirectory;
           expect(tmp, isDirectory);
           expect(tmp, exists);
         });
@@ -318,7 +320,7 @@
         test('staysAtRootIfSetToParentOfRoot', () {
           fs.currentDirectory =
               List<String>.filled(20, '..').join(fs.path.separator);
-          String cwd = fs.currentDirectory.path;
+          var cwd = fs.currentDirectory.path;
           expect(cwd, fs.path.rootPrefix(cwd));
         });
 
@@ -371,36 +373,36 @@
 
       group('stat', () {
         test('isNotFoundForEmptyPath', () {
-          FileStat stat = fs.statSync('');
+          var stat = fs.statSync('');
           expect(stat.type, FileSystemEntityType.notFound);
         });
 
         test('isNotFoundForPathToNonExistentEntityAtTail', () {
-          FileStat stat = fs.statSync(ns('/foo'));
+          var stat = fs.statSync(ns('/foo'));
           expect(stat.type, FileSystemEntityType.notFound);
         });
 
         test('isNotFoundForPathToNonExistentEntityInTraversal', () {
-          FileStat stat = fs.statSync(ns('/foo/bar'));
+          var stat = fs.statSync(ns('/foo/bar'));
           expect(stat.type, FileSystemEntityType.notFound);
         });
 
         test('isDirectoryForDirectory', () {
           fs.directory(ns('/foo')).createSync();
-          FileStat stat = fs.statSync(ns('/foo'));
+          var stat = fs.statSync(ns('/foo'));
           expect(stat.type, FileSystemEntityType.directory);
         });
 
         test('isFileForFile', () {
           fs.file(ns('/foo')).createSync();
-          FileStat stat = fs.statSync(ns('/foo'));
+          var stat = fs.statSync(ns('/foo'));
           expect(stat.type, FileSystemEntityType.file);
         });
 
         test('isFileForLinkToFile', () {
           fs.file(ns('/foo')).createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          FileStat stat = fs.statSync(ns('/bar'));
+          var stat = fs.statSync(ns('/bar'));
           expect(stat.type, FileSystemEntityType.file);
         });
 
@@ -408,7 +410,7 @@
           fs.link(ns('/foo')).createSync(ns('/bar'));
           fs.link(ns('/bar')).createSync(ns('/baz'));
           fs.link(ns('/baz')).createSync(ns('/foo'));
-          FileStat stat = fs.statSync(ns('/foo'));
+          var stat = fs.statSync(ns('/foo'));
           expect(stat.type, FileSystemEntityType.notFound);
         });
       });
@@ -454,18 +456,18 @@
       group('type', () {
         test('isFileForFile', () {
           fs.file(ns('/foo')).createSync();
-          FileSystemEntityType type = fs.typeSync(ns('/foo'));
+          var type = fs.typeSync(ns('/foo'));
           expect(type, FileSystemEntityType.file);
         });
 
         test('isDirectoryForDirectory', () {
           fs.directory(ns('/foo')).createSync();
-          FileSystemEntityType type = fs.typeSync(ns('/foo'));
+          var type = fs.typeSync(ns('/foo'));
           expect(type, FileSystemEntityType.directory);
         });
 
         test('isDirectoryForAncestorOfRoot', () {
-          FileSystemEntityType type = fs
+          var type = fs
               .typeSync(List<String>.filled(20, '..').join(fs.path.separator));
           expect(type, FileSystemEntityType.directory);
         });
@@ -473,15 +475,14 @@
         test('isFileForLinkToFileAndFollowLinksTrue', () {
           fs.file(ns('/foo')).createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          FileSystemEntityType type = fs.typeSync(ns('/bar'));
+          var type = fs.typeSync(ns('/bar'));
           expect(type, FileSystemEntityType.file);
         });
 
         test('isLinkForLinkToFileAndFollowLinksFalse', () {
           fs.file(ns('/foo')).createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          FileSystemEntityType type =
-              fs.typeSync(ns('/bar'), followLinks: false);
+          var type = fs.typeSync(ns('/bar'), followLinks: false);
           expect(type, FileSystemEntityType.link);
         });
 
@@ -489,17 +490,17 @@
           fs.link(ns('/foo')).createSync(ns('/bar'));
           fs.link(ns('/bar')).createSync(ns('/baz'));
           fs.link(ns('/baz')).createSync(ns('/foo'));
-          FileSystemEntityType type = fs.typeSync(ns('/foo'));
+          var type = fs.typeSync(ns('/foo'));
           expect(type, FileSystemEntityType.notFound);
         });
 
         test('isNotFoundForNoEntityAtTail', () {
-          FileSystemEntityType type = fs.typeSync(ns('/foo'));
+          var type = fs.typeSync(ns('/foo'));
           expect(type, FileSystemEntityType.notFound);
         });
 
         test('isNotFoundForNoDirectoryInTraversal', () {
-          FileSystemEntityType type = fs.typeSync(ns('/foo/bar/baz'));
+          var type = fs.typeSync(ns('/foo/bar/baz'));
           expect(type, FileSystemEntityType.notFound);
         });
       });
@@ -676,8 +677,8 @@
         });
 
         test('succeedsIfDestinationDoesntExist', () {
-          Directory src = fs.directory(ns('/foo'))..createSync();
-          Directory dest = src.renameSync(ns('/bar'));
+          var src = fs.directory(ns('/foo'))..createSync();
+          var dest = src.renameSync(ns('/bar'));
           expect(dest.path, ns('/bar'));
           expect(dest, exists);
         });
@@ -686,8 +687,8 @@
           'succeedsIfDestinationIsEmptyDirectory',
           () {
             fs.directory(ns('/bar')).createSync();
-            Directory src = fs.directory(ns('/foo'))..createSync();
-            Directory dest = src.renameSync(ns('/bar'));
+            var src = fs.directory(ns('/foo'))..createSync();
+            var dest = src.renameSync(ns('/bar'));
             expect(src, isNot(exists));
             expect(dest, exists);
           },
@@ -697,14 +698,14 @@
 
         test('throwsIfDestinationIsFile', () {
           fs.file(ns('/bar')).createSync();
-          Directory src = fs.directory(ns('/foo'))..createSync();
+          var src = fs.directory(ns('/foo'))..createSync();
           expectFileSystemException(ErrorCodes.ENOTDIR, () {
             src.renameSync(ns('/bar'));
           });
         });
 
         test('throwsIfDestinationParentFolderDoesntExist', () {
-          Directory src = fs.directory(ns('/foo'))..createSync();
+          var src = fs.directory(ns('/foo'))..createSync();
           expectFileSystemException(ErrorCodes.ENOENT, () {
             src.renameSync(ns('/bar/baz'));
           });
@@ -712,7 +713,7 @@
 
         test('throwsIfDestinationIsNonEmptyDirectory', () {
           fs.file(ns('/bar/baz')).createSync(recursive: true);
-          Directory src = fs.directory(ns('/foo'))..createSync();
+          var src = fs.directory(ns('/foo'))..createSync();
           // The error will be 'Directory not empty' on OS X, but it will be
           // 'File exists' on Linux.
           expectFileSystemException(
@@ -749,7 +750,7 @@
         });
 
         test('throwsIfDestinationIsLinkToNotFound', () {
-          Directory src = fs.directory(ns('/foo'))..createSync();
+          var src = fs.directory(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/baz'));
           expectFileSystemException(ErrorCodes.ENOTDIR, () {
             src.renameSync(ns('/bar'));
@@ -757,7 +758,7 @@
         });
 
         test('throwsIfDestinationIsLinkToEmptyDirectory', () {
-          Directory src = fs.directory(ns('/foo'))..createSync();
+          var src = fs.directory(ns('/foo'))..createSync();
           fs.directory(ns('/bar')).createSync();
           fs.link(ns('/baz')).createSync(ns('/bar'));
           expectFileSystemException(ErrorCodes.ENOTDIR, () {
@@ -766,7 +767,7 @@
         });
 
         test('succeedsIfDestinationIsInDifferentDirectory', () {
-          Directory src = fs.directory(ns('/foo'))..createSync();
+          var src = fs.directory(ns('/foo'))..createSync();
           fs.directory(ns('/bar')).createSync();
           src.renameSync(ns('/bar/baz'));
           expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound);
@@ -790,24 +791,24 @@
 
       group('delete', () {
         test('returnsCovariantType', () async {
-          Directory dir = fs.directory(ns('/foo'))..createSync();
+          var dir = fs.directory(ns('/foo'))..createSync();
           expect(await dir.delete(), isDirectory);
         });
 
         test('succeedsIfEmptyDirectoryExistsAndRecursiveFalse', () {
-          Directory dir = fs.directory(ns('/foo'))..createSync();
+          var dir = fs.directory(ns('/foo'))..createSync();
           dir.deleteSync();
           expect(dir, isNot(exists));
         });
 
         test('succeedsIfEmptyDirectoryExistsAndRecursiveTrue', () {
-          Directory dir = fs.directory(ns('/foo'))..createSync();
+          var dir = fs.directory(ns('/foo'))..createSync();
           dir.deleteSync(recursive: true);
           expect(dir, isNot(exists));
         });
 
         test('throwsIfNonEmptyDirectoryExistsAndRecursiveFalse', () {
-          Directory dir = fs.directory(ns('/foo'))..createSync();
+          var dir = fs.directory(ns('/foo'))..createSync();
           fs.file(ns('/foo/bar')).createSync();
           expectFileSystemException(ErrorCodes.ENOTEMPTY, () {
             dir.deleteSync();
@@ -815,7 +816,7 @@
         });
 
         test('succeedsIfNonEmptyDirectoryExistsAndRecursiveTrue', () {
-          Directory dir = fs.directory(ns('/foo'))..createSync();
+          var dir = fs.directory(ns('/foo'))..createSync();
           fs.file(ns('/foo/bar')).createSync();
           dir.deleteSync(recursive: true);
           expect(fs.directory(ns('/foo')), isNot(exists));
@@ -997,7 +998,7 @@
         test('handlesParentAndThisFolderReferences', () {
           fs.directory(ns('/foo/bar/baz')).createSync(recursive: true);
           fs.link(ns('/foo/bar/baz/qux')).createSync(fs.path.join('..', '..'));
-          String resolved = fs
+          var resolved = fs
               .directory(ns('/foo/./bar/baz/../baz/qux/bar'))
               .resolveSymbolicLinksSync();
           expect(resolved, ns('/foo/bar'));
@@ -1015,7 +1016,7 @@
               .createSync(fs.path.join('..', '..', 'qux'), recursive: true);
           fs.link(ns('/qux')).createSync('quux');
           fs.link(ns('/quux/quuz')).createSync(ns('/foo'), recursive: true);
-          String resolved = fs
+          var resolved = fs
               .directory(ns('/foo//bar/./baz/quuz/bar/..///bar/baz/'))
               .resolveSymbolicLinksSync();
           expect(resolved, ns('/quux'));
@@ -1069,29 +1070,29 @@
 
         test('resolvesNameCollisions', () {
           fs.directory(ns('/foo/bar')).createSync(recursive: true);
-          Directory tmp = fs.directory(ns('/foo')).createTempSync('bar');
+          var tmp = fs.directory(ns('/foo')).createTempSync('bar');
           expect(tmp.path,
               allOf(isNot(ns('/foo/bar')), startsWith(ns('/foo/bar'))));
         });
 
         test('succeedsWithoutPrefix', () {
-          Directory dir = fs.directory(ns('/foo'))..createSync();
+          var dir = fs.directory(ns('/foo'))..createSync();
           expect(dir.createTempSync().path, startsWith(ns('/foo/')));
         });
 
         test('succeedsWithPrefix', () {
-          Directory dir = fs.directory(ns('/foo'))..createSync();
+          var dir = fs.directory(ns('/foo'))..createSync();
           expect(dir.createTempSync('bar').path, startsWith(ns('/foo/bar')));
         });
 
         test('succeedsWithNestedPathPrefixThatExists', () {
           fs.directory(ns('/foo/bar')).createSync(recursive: true);
-          Directory tmp = fs.directory(ns('/foo')).createTempSync('bar/baz');
+          var tmp = fs.directory(ns('/foo')).createTempSync('bar/baz');
           expect(tmp.path, startsWith(ns('/foo/bar/baz')));
         });
 
         test('throwsWithNestedPathPrefixThatDoesntExist', () {
-          Directory dir = fs.directory(ns('/foo'))..createSync();
+          var dir = fs.directory(ns('/foo'))..createSync();
           expectFileSystemException(ErrorCodes.ENOENT, () {
             dir.createTempSync('bar/baz');
           });
@@ -1123,7 +1124,7 @@
         });
 
         test('returnsEmptyListForEmptyDirectory', () {
-          Directory empty = fs.directory(ns('/bar'))..createSync();
+          var empty = fs.directory(ns('/bar'))..createSync();
           expect(empty.listSync(), isEmpty);
         });
 
@@ -1134,7 +1135,7 @@
         });
 
         test('returnsLinkObjectsIfFollowLinksFalse', () {
-          List<FileSystemEntity> list = dir.listSync(followLinks: false);
+          var list = dir.listSync(followLinks: false);
           expect(list, hasLength(3));
           expect(list, contains(allOf(isFile, hasPath(ns('/foo/bar')))));
           expect(list, contains(allOf(isDirectory, hasPath(ns('/foo/baz')))));
@@ -1142,7 +1143,7 @@
         });
 
         test('followsLinksIfFollowLinksTrue', () {
-          List<FileSystemEntity> list = dir.listSync();
+          var list = dir.listSync();
           expect(list, hasLength(3));
           expect(list, contains(allOf(isFile, hasPath(ns('/foo/bar')))));
           expect(list, contains(allOf(isDirectory, hasPath(ns('/foo/baz')))));
@@ -1189,8 +1190,7 @@
         test('childEntriesNotNormalized', () {
           dir = fs.directory(ns('/bar/baz'))..createSync(recursive: true);
           fs.file(ns('/bar/baz/qux')).createSync();
-          List<FileSystemEntity> list =
-              fs.directory(ns('/bar//../bar/./baz')).listSync();
+          var list = fs.directory(ns('/bar//../bar/./baz')).listSync();
           expect(list, hasLength(1));
           expect(list[0], allOf(isFile, hasPath(ns('/bar//../bar/./baz/qux'))));
         });
@@ -1198,9 +1198,8 @@
         test('symlinksToNotFoundAlwaysReturnedAsLinks', () {
           dir = fs.directory(ns('/bar'))..createSync();
           fs.link(ns('/bar/baz')).createSync('qux');
-          for (bool followLinks in const <bool>[true, false]) {
-            List<FileSystemEntity> list =
-                dir.listSync(followLinks: followLinks);
+          for (var followLinks in const <bool>[true, false]) {
+            var list = dir.listSync(followLinks: followLinks);
             expect(list, hasLength(1));
             expect(list[0], allOf(isLink, hasPath(ns('/bar/baz'))));
           }
@@ -1208,7 +1207,7 @@
       });
 
       test('childEntities', () {
-        Directory dir = fs.directory(ns('/foo'))..createSync();
+        var dir = fs.directory(ns('/foo'))..createSync();
         dir.childDirectory('bar').createSync();
         dir.childFile('baz').createSync();
         dir.childLink('qux').createSync('bar');
@@ -1321,22 +1320,22 @@
         });
 
         test('succeedsIfDestinationDoesntExistAtTail', () {
-          File src = fs.file(ns('/foo'))..createSync();
-          File dest = src.renameSync(ns('/bar'));
+          var src = fs.file(ns('/foo'))..createSync();
+          var dest = src.renameSync(ns('/bar'));
           expect(fs.file(ns('/foo')), isNot(exists));
           expect(fs.file(ns('/bar')), exists);
           expect(dest.path, ns('/bar'));
         });
 
         test('throwsIfDestinationDoesntExistViaTraversal', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           expectFileSystemException(ErrorCodes.ENOENT, () {
             f.renameSync(ns('/bar/baz'));
           });
         });
 
         test('succeedsIfDestinationExistsAsFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.file(ns('/bar')).createSync();
           f.renameSync(ns('/bar'));
           expect(fs.file(ns('/foo')), isNot(exists));
@@ -1344,7 +1343,7 @@
         });
 
         test('throwsIfDestinationExistsAsDirectory', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.directory(ns('/bar')).createSync();
           expectFileSystemException(ErrorCodes.EISDIR, () {
             f.renameSync(ns('/bar'));
@@ -1352,7 +1351,7 @@
         });
 
         test('succeedsIfDestinationExistsAsLinkToFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.file(ns('/bar')).createSync();
           fs.link(ns('/baz')).createSync(ns('/bar'));
           f.renameSync(ns('/baz'));
@@ -1364,7 +1363,7 @@
         });
 
         test('throwsIfDestinationExistsAsLinkToDirectory', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.directory(ns('/bar')).createSync();
           fs.link(ns('/baz')).createSync(ns('/bar'));
           expectFileSystemException(ErrorCodes.EISDIR, () {
@@ -1373,7 +1372,7 @@
         });
 
         test('succeedsIfDestinationExistsAsLinkToNotFound', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/baz'));
           f.renameSync(ns('/bar'));
           expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound);
@@ -1429,7 +1428,7 @@
         });
 
         test('succeedsIfDestinationDoesntExistAtTail', () {
-          File f = fs.file(ns('/foo'))
+          var f = fs.file(ns('/foo'))
             ..createSync()
             ..writeAsStringSync('foo');
           f.copySync(ns('/bar'));
@@ -1439,14 +1438,14 @@
         });
 
         test('throwsIfDestinationDoesntExistViaTraversal', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           expectFileSystemException(ErrorCodes.ENOENT, () {
             f.copySync(ns('/bar/baz'));
           });
         });
 
         test('succeedsIfDestinationExistsAsFile', () {
-          File f = fs.file(ns('/foo'))
+          var f = fs.file(ns('/foo'))
             ..createSync()
             ..writeAsStringSync('foo');
           fs.file(ns('/bar'))
@@ -1460,7 +1459,7 @@
         });
 
         test('throwsIfDestinationExistsAsDirectory', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.directory(ns('/bar')).createSync();
           expectFileSystemException(ErrorCodes.EISDIR, () {
             f.copySync(ns('/bar'));
@@ -1468,7 +1467,7 @@
         });
 
         test('succeedsIfDestinationExistsAsLinkToFile', () {
-          File f = fs.file(ns('/foo'))
+          var f = fs.file(ns('/foo'))
             ..createSync()
             ..writeAsStringSync('foo');
           fs.file(ns('/bar'))
@@ -1487,7 +1486,7 @@
         }, skip: io.Platform.isWindows /* No links on Windows */);
 
         test('throwsIfDestinationExistsAsLinkToDirectory', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.directory(ns('/bar')).createSync();
           fs.link(ns('/baz')).createSync(ns('/bar'));
           expectFileSystemException(ErrorCodes.EISDIR, () {
@@ -1525,7 +1524,7 @@
         });
 
         test('succeedsIfDestinationIsInDifferentDirectoryThanSource', () {
-          File f = fs.file(ns('/foo/bar'))
+          var f = fs.file(ns('/foo/bar'))
             ..createSync(recursive: true)
             ..writeAsStringSync('foo');
           fs.directory(ns('/baz')).createSync();
@@ -1587,12 +1586,12 @@
         });
 
         test('returnsZeroForNewlyCreatedFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           expect(f.lengthSync(), 0);
         });
 
         test('writeNBytesReturnsLengthN', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsBytesSync(<int>[1, 2, 3, 4], flush: true);
           expect(f.lengthSync(), 4);
         });
@@ -1616,10 +1615,10 @@
 
       group('lastAccessed', () {
         test('isNowForNewlyCreatedFile', () {
-          DateTime before = downstairs();
-          File f = fs.file(ns('/foo'))..createSync();
-          DateTime after = ceil();
-          DateTime accessed = f.lastAccessedSync();
+          var before = downstairs();
+          var f = fs.file(ns('/foo'))..createSync();
+          var after = ceil();
+          var accessed = f.lastAccessedSync();
           expect(accessed, isSameOrAfter(before));
           expect(accessed, isSameOrBefore(after));
         });
@@ -1638,18 +1637,18 @@
         });
 
         test('succeedsIfExistsAsLinkToFile', () {
-          DateTime before = downstairs();
+          var before = downstairs();
           fs.file(ns('/foo')).createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          DateTime after = ceil();
-          DateTime accessed = fs.file(ns('/bar')).lastAccessedSync();
+          var after = ceil();
+          var accessed = fs.file(ns('/bar')).lastAccessedSync();
           expect(accessed, isSameOrAfter(before));
           expect(accessed, isSameOrBefore(after));
         });
       });
 
       group('setLastAccessed', () {
-        final DateTime time = DateTime(1999);
+        final time = DateTime(1999);
 
         test('throwsIfDoesntExist', () {
           expectFileSystemException(ErrorCodes.ENOENT, () {
@@ -1665,13 +1664,13 @@
         });
 
         test('succeedsIfExistsAsFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.setLastAccessedSync(time);
           expect(fs.file(ns('/foo')).lastAccessedSync(), time);
         });
 
         test('succeedsIfExistsAsLinkToFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
           f.setLastAccessedSync(time);
           expect(fs.file(ns('/bar')).lastAccessedSync(), time);
@@ -1680,10 +1679,10 @@
 
       group('lastModified', () {
         test('isNowForNewlyCreatedFile', () {
-          DateTime before = downstairs();
-          File f = fs.file(ns('/foo'))..createSync();
-          DateTime after = ceil();
-          DateTime modified = f.lastModifiedSync();
+          var before = downstairs();
+          var f = fs.file(ns('/foo'))..createSync();
+          var after = ceil();
+          var modified = f.lastModifiedSync();
           expect(modified, isSameOrAfter(before));
           expect(modified, isSameOrBefore(after));
         });
@@ -1702,18 +1701,18 @@
         });
 
         test('succeedsIfExistsAsLinkToFile', () {
-          DateTime before = downstairs();
+          var before = downstairs();
           fs.file(ns('/foo')).createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          DateTime after = ceil();
-          DateTime modified = fs.file(ns('/bar')).lastModifiedSync();
+          var after = ceil();
+          var modified = fs.file(ns('/bar')).lastModifiedSync();
           expect(modified, isSameOrAfter(before));
           expect(modified, isSameOrBefore(after));
         });
       });
 
       group('setLastModified', () {
-        final DateTime time = DateTime(1999);
+        final time = DateTime(1999);
 
         test('throwsIfDoesntExist', () {
           expectFileSystemException(ErrorCodes.ENOENT, () {
@@ -1729,13 +1728,13 @@
         });
 
         test('succeedsIfExistsAsFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.setLastModifiedSync(time);
           expect(fs.file(ns('/foo')).lastModifiedSync(), time);
         });
 
         test('succeedsIfExistsAsLinkToFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
           f.setLastModifiedSync(time);
           expect(fs.file(ns('/bar')).lastModifiedSync(), time);
@@ -1752,7 +1751,7 @@
             });
           } else {
             test('createsFileIfDoesntExistAtTail', () {
-              RandomAccessFile raf = fs.file(ns('/bar')).openSync(mode: mode);
+              var raf = fs.file(ns('/bar')).openSync(mode: mode);
               raf.closeSync();
               expect(fs.file(ns('/bar')), exists);
             });
@@ -1877,39 +1876,39 @@
                 });
 
                 test('readIntoWithBufferLargerThanContent', () {
-                  List<int> buffer = List<int>.filled(1024, 0);
-                  int numRead = raf.readIntoSync(buffer);
+                  var buffer = List<int>.filled(1024, 0);
+                  var numRead = raf.readIntoSync(buffer);
                   expect(numRead, 21);
                   expect(utf8.decode(buffer.sublist(0, 21)),
                       'pre-existing content\n');
                 });
 
                 test('readIntoWithBufferSmallerThanContent', () {
-                  List<int> buffer = List<int>.filled(10, 0);
-                  int numRead = raf.readIntoSync(buffer);
+                  var buffer = List<int>.filled(10, 0);
+                  var numRead = raf.readIntoSync(buffer);
                   expect(numRead, 10);
                   expect(utf8.decode(buffer), 'pre-existi');
                 });
 
                 test('readIntoWithStart', () {
-                  List<int> buffer = List<int>.filled(10, 0);
-                  int numRead = raf.readIntoSync(buffer, 2);
+                  var buffer = List<int>.filled(10, 0);
+                  var numRead = raf.readIntoSync(buffer, 2);
                   expect(numRead, 8);
                   expect(utf8.decode(buffer.sublist(2)), 'pre-exis');
                 });
 
                 test('readIntoWithStartAndEnd', () {
-                  List<int> buffer = List<int>.filled(10, 0);
-                  int numRead = raf.readIntoSync(buffer, 2, 5);
+                  var buffer = List<int>.filled(10, 0);
+                  var numRead = raf.readIntoSync(buffer, 2, 5);
                   expect(numRead, 3);
                   expect(utf8.decode(buffer.sublist(2, 5)), 'pre');
                 });
 
                 test('openReadHandleDoesNotChange', () {
-                  final String initial = utf8.decode(raf.readSync(4));
+                  final initial = utf8.decode(raf.readSync(4));
                   expect(initial, 'pre-');
-                  final File newFile = f.renameSync(ns('/bar'));
-                  String rest = utf8.decode(raf.readSync(1024));
+                  final newFile = f.renameSync(ns('/bar'));
+                  var rest = utf8.decode(raf.readSync(1024));
                   expect(rest, 'existing content\n');
 
                   assert(newFile.path != f.path);
@@ -1942,13 +1941,13 @@
               });
             } else {
               test('lengthGrowsAsDataIsWritten', () {
-                int lengthBefore = f.lengthSync();
+                var lengthBefore = f.lengthSync();
                 raf.writeByteSync(0xFACE);
                 expect(raf.lengthSync(), lengthBefore + 1);
               });
 
               test('flush', () {
-                int lengthBefore = f.lengthSync();
+                var lengthBefore = f.lengthSync();
                 raf.writeByteSync(0xFACE);
                 raf.flushSync();
                 expect(f.lengthSync(), lengthBefore + 1);
@@ -2009,10 +2008,10 @@
 
               test('openWriteHandleDoesNotChange', () {
                 raf.writeStringSync('Hello ');
-                final File newFile = f.renameSync(ns('/bar'));
+                final newFile = f.renameSync(ns('/bar'));
                 raf.writeStringSync('world');
 
-                final String contents = newFile.readAsStringSync();
+                final contents = newFile.readAsStringSync();
                 if (mode == FileMode.write || mode == FileMode.writeOnly) {
                   expect(contents, 'Hello world');
                 } else {
@@ -2067,7 +2066,7 @@
                 });
               } else {
                 test('growsAfterWrite', () {
-                  int positionBefore = raf.positionSync();
+                  var positionBefore = raf.positionSync();
                   raf.writeStringSync('Hello world');
                   expect(raf.positionSync(), positionBefore + 11);
                 });
@@ -2165,42 +2164,42 @@
 
       group('openRead', () {
         test('throwsIfDoesntExist', () {
-          Stream<List<int>> stream = fs.file(ns('/foo')).openRead();
+          var stream = fs.file(ns('/foo')).openRead();
           expect(stream.drain<void>(),
               throwsFileSystemException(ErrorCodes.ENOENT));
         });
 
         test('succeedsIfExistsAsFile', () async {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('Hello world', flush: true);
-          Stream<List<int>> stream = f.openRead();
-          List<List<int>> data = await stream.toList();
+          var stream = f.openRead();
+          var data = await stream.toList();
           expect(data, hasLength(1));
           expect(utf8.decode(data[0]), 'Hello world');
         });
 
         test('throwsIfExistsAsDirectory', () {
           fs.directory(ns('/foo')).createSync();
-          Stream<List<int>> stream = fs.file(ns('/foo')).openRead();
+          var stream = fs.file(ns('/foo')).openRead();
           expect(stream.drain<void>(),
               throwsFileSystemException(ErrorCodes.EISDIR));
         });
 
         test('succeedsIfExistsAsLinkToFile', () async {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
           f.writeAsStringSync('Hello world', flush: true);
-          Stream<List<int>> stream = fs.file(ns('/bar')).openRead();
-          List<List<int>> data = await stream.toList();
+          var stream = fs.file(ns('/bar')).openRead();
+          var data = await stream.toList();
           expect(data, hasLength(1));
           expect(utf8.decode(data[0]), 'Hello world');
         });
 
         test('respectsStartAndEndParameters', () async {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('Hello world', flush: true);
-          Stream<List<int>> stream = f.openRead(2);
-          List<List<int>> data = await stream.toList();
+          var stream = f.openRead(2);
+          var data = await stream.toList();
           expect(data, hasLength(1));
           expect(utf8.decode(data[0]), 'llo world');
           stream = f.openRead(2, 5);
@@ -2210,24 +2209,24 @@
         });
 
         test('throwsIfStartParameterIsNegative', () async {
-          File f = fs.file(ns('/foo'))..createSync();
-          Stream<List<int>> stream = f.openRead(-2);
+          var f = fs.file(ns('/foo'))..createSync();
+          var stream = f.openRead(-2);
           expect(stream.drain<void>(), throwsRangeError);
         });
 
         test('stopsAtEndOfFileIfEndParameterIsPastEndOfFile', () async {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('Hello world', flush: true);
-          Stream<List<int>> stream = f.openRead(2, 1024);
-          List<List<int>> data = await stream.toList();
+          var stream = f.openRead(2, 1024);
+          var data = await stream.toList();
           expect(data, hasLength(1));
           expect(utf8.decode(data[0]), 'llo world');
         });
 
         test('providesSingleSubscriptionStream', () async {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('Hello world', flush: true);
-          Stream<List<int>> stream = f.openRead();
+          var stream = f.openRead();
           expect(stream.isBroadcast, isFalse);
           await stream.drain<void>();
         });
@@ -2237,20 +2236,20 @@
           // split across multiple chunks in the [Stream].  However, there
           // doesn't seem to be a good way to determine the chunk size used by
           // [io.File].
-          final List<int> data = List<int>.generate(
+          final data = List<int>.generate(
             1024 * 256,
             (int index) => index & 0xFF,
             growable: false,
           );
 
-          final File f = fs.file(ns('/foo'))..createSync();
+          final f = fs.file(ns('/foo'))..createSync();
 
           f.writeAsBytesSync(data, flush: true);
-          final Stream<List<int>> stream = f.openRead();
+          final stream = f.openRead();
 
           File? newFile;
           List<int>? initialChunk;
-          final List<int> remainingChunks = <int>[];
+          final remainingChunks = <int>[];
 
           await for (List<int> chunk in stream) {
             if (initialChunk == null) {
@@ -2276,7 +2275,7 @@
 
         test('openReadCompatibleWithUtf8Decoder', () async {
           const content = 'Hello world!';
-          File file = fs.file(ns('/foo'))
+          var file = fs.file(ns('/foo'))
             ..createSync()
             ..writeAsStringSync(content);
           expect(
@@ -2315,8 +2314,8 @@
         });
 
         test('succeedsIfExistsAsEmptyFile', () async {
-          File f = fs.file(ns('/foo'))..createSync();
-          IOSink sink = f.openWrite();
+          var f = fs.file(ns('/foo'))..createSync();
+          var sink = f.openWrite();
           sink.write('Hello world');
           await sink.flush();
           await sink.close();
@@ -2326,7 +2325,7 @@
         test('succeedsIfExistsAsLinkToFile', () async {
           fs.file(ns('/foo')).createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          IOSink sink = fs.file(ns('/bar')).openWrite();
+          var sink = fs.file(ns('/bar')).openWrite();
           sink.write('Hello world');
           await sink.flush();
           await sink.close();
@@ -2334,9 +2333,9 @@
         });
 
         test('overwritesContentInWriteMode', () async {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('Hello');
-          IOSink sink = f.openWrite();
+          var sink = f.openWrite();
           sink.write('Goodbye');
           await sink.flush();
           await sink.close();
@@ -2344,9 +2343,9 @@
         });
 
         test('appendsContentInAppendMode', () async {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('Hello');
-          IOSink sink = f.openWrite(mode: FileMode.append);
+          var sink = f.openWrite(mode: FileMode.append);
           sink.write('Goodbye');
           await sink.flush();
           await sink.close();
@@ -2354,12 +2353,12 @@
         });
 
         test('openWriteHandleDoesNotChange', () async {
-          File f = fs.file(ns('/foo'))..createSync();
-          IOSink sink = f.openWrite();
+          var f = fs.file(ns('/foo'))..createSync();
+          var sink = f.openWrite();
           sink.write('Hello');
           await sink.flush();
 
-          final File newFile = f.renameSync(ns('/bar'));
+          final newFile = f.renameSync(ns('/bar'));
           sink.write('Goodbye');
           await sink.flush();
           await sink.close();
@@ -2377,7 +2376,7 @@
           late bool isSinkClosed;
 
           Future<dynamic> closeSink() {
-            Future<dynamic> future = sink.close();
+            var future = sink.close();
             isSinkClosed = true;
             return future;
           }
@@ -2448,13 +2447,13 @@
 
           test('ignoresCloseAfterAlreadyClosed', () async {
             sink.write('Hello world');
-            Future<dynamic> f1 = closeSink();
-            Future<dynamic> f2 = closeSink();
+            var f1 = closeSink();
+            var f2 = closeSink();
             await Future.wait<dynamic>(<Future<dynamic>>[f1, f2]);
           });
 
           test('returnsAccurateDoneFuture', () async {
-            bool done = false;
+            var done = false;
             // ignore: unawaited_futures
             sink.done.then((dynamic _) => done = true);
             expect(done, isFalse);
@@ -2469,7 +2468,7 @@
             late bool isControllerClosed;
 
             Future<dynamic> closeController() {
-              Future<dynamic> future = controller.close();
+              var future = controller.close();
               isControllerClosed = true;
               return future;
             }
@@ -2543,7 +2542,7 @@
         });
 
         test('succeedsIfExistsAsFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsBytesSync(<int>[1, 2, 3, 4]);
           expect(f.readAsBytesSync(), <int>[1, 2, 3, 4]);
         });
@@ -2556,12 +2555,12 @@
         });
 
         test('returnsEmptyListForZeroByteFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           expect(f.readAsBytesSync(), isEmpty);
         });
 
         test('returns a copy, not a view, of the file content', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsBytesSync(<int>[1, 2, 3, 4]);
           List<int> result = f.readAsBytesSync();
           expect(result, <int>[1, 2, 3, 4]);
@@ -2593,7 +2592,7 @@
         });
 
         test('succeedsIfExistsAsFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('Hello world');
           expect(f.readAsStringSync(), 'Hello world');
         });
@@ -2606,14 +2605,14 @@
         });
 
         test('returnsEmptyStringForZeroByteFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           expect(f.readAsStringSync(), isEmpty);
         });
       });
 
       group('readAsLines', () {
-        const String testString = 'Hello world\nHow are you?\nI am fine';
-        final List<String> expectedLines = <String>[
+        const testString = 'Hello world\nHow are you?\nI am fine';
+        final expectedLines = <String>[
           'Hello world',
           'How are you?',
           'I am fine',
@@ -2641,25 +2640,25 @@
         });
 
         test('succeedsIfExistsAsFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync(testString);
           expect(f.readAsLinesSync(), expectedLines);
         });
 
         test('succeedsIfExistsAsLinkToFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
           f.writeAsStringSync(testString);
           expect(f.readAsLinesSync(), expectedLines);
         });
 
         test('returnsEmptyListForZeroByteFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           expect(f.readAsLinesSync(), isEmpty);
         });
 
         test('isTrailingNewlineAgnostic', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('$testString\n');
           expect(f.readAsLinesSync(), expectedLines);
 
@@ -2677,7 +2676,7 @@
         });
 
         test('createsFileIfDoesntExist', () {
-          File f = fs.file(ns('/foo'));
+          var f = fs.file(ns('/foo'));
           expect(f, isNot(exists));
           f.writeAsBytesSync(<int>[1, 2, 3, 4]);
           expect(f, exists);
@@ -2699,21 +2698,21 @@
         });
 
         test('succeedsIfExistsAsLinkToFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
           fs.file(ns('/bar')).writeAsBytesSync(<int>[1, 2, 3, 4]);
           expect(f.readAsBytesSync(), <int>[1, 2, 3, 4]);
         });
 
         test('throwsIfFileModeRead', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           expectFileSystemException(ErrorCodes.EBADF, () {
             f.writeAsBytesSync(<int>[1], mode: FileMode.read);
           });
         });
 
         test('overwritesContentIfFileModeWrite', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsBytesSync(<int>[1, 2]);
           expect(f.readAsBytesSync(), <int>[1, 2]);
           f.writeAsBytesSync(<int>[3, 4]);
@@ -2721,7 +2720,7 @@
         });
 
         test('appendsContentIfFileModeAppend', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsBytesSync(<int>[1, 2], mode: FileMode.append);
           expect(f.readAsBytesSync(), <int>[1, 2]);
           f.writeAsBytesSync(<int>[3, 4], mode: FileMode.append);
@@ -2729,17 +2728,17 @@
         });
 
         test('acceptsEmptyBytesList', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsBytesSync(<int>[]);
           expect(f.readAsBytesSync(), <int>[]);
         });
 
         test('updatesLastModifiedTime', () async {
-          File f = fs.file(ns('/foo'))..createSync();
-          DateTime before = f.statSync().modified;
+          var f = fs.file(ns('/foo'))..createSync();
+          var before = f.statSync().modified;
           await Future<void>.delayed(const Duration(seconds: 2));
           f.writeAsBytesSync(<int>[1, 2, 3]);
-          DateTime after = f.statSync().modified;
+          var after = f.statSync().modified;
           expect(after, isAfter(before));
         });
       });
@@ -2750,7 +2749,7 @@
         });
 
         test('createsFileIfDoesntExist', () {
-          File f = fs.file(ns('/foo'));
+          var f = fs.file(ns('/foo'));
           expect(f, isNot(exists));
           f.writeAsStringSync('Hello world');
           expect(f, exists);
@@ -2772,21 +2771,21 @@
         });
 
         test('succeedsIfExistsAsLinkToFile', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
           fs.file(ns('/bar')).writeAsStringSync('Hello world');
           expect(f.readAsStringSync(), 'Hello world');
         });
 
         test('throwsIfFileModeRead', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           expectFileSystemException(ErrorCodes.EBADF, () {
             f.writeAsStringSync('Hello world', mode: FileMode.read);
           });
         });
 
         test('overwritesContentIfFileModeWrite', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('Hello world');
           expect(f.readAsStringSync(), 'Hello world');
           f.writeAsStringSync('Goodbye cruel world');
@@ -2794,7 +2793,7 @@
         });
 
         test('appendsContentIfFileModeAppend', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('Hello', mode: FileMode.append);
           expect(f.readAsStringSync(), 'Hello');
           f.writeAsStringSync('Goodbye', mode: FileMode.append);
@@ -2802,7 +2801,7 @@
         });
 
         test('acceptsEmptyString', () {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           f.writeAsStringSync('');
           expect(f.readAsStringSync(), isEmpty);
         });
@@ -2847,38 +2846,38 @@
 
       group('stat', () {
         test('isNotFoundIfDoesntExistAtTail', () {
-          FileStat stat = fs.file(ns('/foo')).statSync();
+          var stat = fs.file(ns('/foo')).statSync();
           expect(stat.type, FileSystemEntityType.notFound);
         });
 
         test('isNotFoundIfDoesntExistViaTraversal', () {
-          FileStat stat = fs.file(ns('/foo/bar')).statSync();
+          var stat = fs.file(ns('/foo/bar')).statSync();
           expect(stat.type, FileSystemEntityType.notFound);
         });
 
         test('isDirectoryIfExistsAsDirectory', () {
           fs.directory(ns('/foo')).createSync();
-          FileStat stat = fs.file(ns('/foo')).statSync();
+          var stat = fs.file(ns('/foo')).statSync();
           expect(stat.type, FileSystemEntityType.directory);
         });
 
         test('isFileIfExistsAsFile', () {
           fs.file(ns('/foo')).createSync();
-          FileStat stat = fs.file(ns('/foo')).statSync();
+          var stat = fs.file(ns('/foo')).statSync();
           expect(stat.type, FileSystemEntityType.file);
         });
 
         test('isFileIfExistsAsLinkToFile', () {
           fs.file(ns('/foo')).createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          FileStat stat = fs.file(ns('/bar')).statSync();
+          var stat = fs.file(ns('/bar')).statSync();
           expect(stat.type, FileSystemEntityType.file);
         });
       });
 
       group('delete', () {
         test('returnsCovariantType', () async {
-          File f = fs.file(ns('/foo'))..createSync();
+          var f = fs.file(ns('/foo'))..createSync();
           expect(await f.delete(), isFile);
         });
 
@@ -2953,14 +2952,14 @@
       group('uri', () {
         test('whenTargetIsDirectory', () {
           fs.directory(ns('/foo')).createSync();
-          Link l = fs.link(ns('/bar'))..createSync(ns('/foo'));
+          var l = fs.link(ns('/bar'))..createSync(ns('/foo'));
           expect(l.uri, fs.path.toUri(ns('/bar')));
           expect(fs.link('bar').uri.toString(), 'bar');
         });
 
         test('whenTargetIsFile', () {
           fs.file(ns('/foo')).createSync();
-          Link l = fs.link(ns('/bar'))..createSync(ns('/foo'));
+          var l = fs.link(ns('/bar'))..createSync(ns('/foo'));
           expect(l.uri, fs.path.toUri(ns('/bar')));
           expect(fs.link('bar').uri.toString(), 'bar');
         });
@@ -2991,24 +2990,24 @@
         });
 
         test('isTrueIfTargetIsNotFound', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           expect(l, exists);
         });
 
         test('isTrueIfTargetIsFile', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.file(ns('/bar')).createSync();
           expect(l, exists);
         });
 
         test('isTrueIfTargetIsDirectory', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.directory(ns('/bar')).createSync();
           expect(l, exists);
         });
 
         test('isTrueIfTargetIsLinkLoop', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.link(ns('/bar')).createSync(ns('/foo'));
           expect(l, exists);
         });
@@ -3038,29 +3037,29 @@
         });
 
         test('isNotFoundIfTargetNotFoundAtTail', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           expect(l.statSync().type, FileSystemEntityType.notFound);
         });
 
         test('isNotFoundIfTargetNotFoundViaTraversal', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar/baz'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar/baz'));
           expect(l.statSync().type, FileSystemEntityType.notFound);
         });
 
         test('isNotFoundIfTargetIsLinkLoop', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.link(ns('/bar')).createSync(ns('/foo'));
           expect(l.statSync().type, FileSystemEntityType.notFound);
         });
 
         test('isFileIfTargetIsFile', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.file(ns('/bar')).createSync();
           expect(l.statSync().type, FileSystemEntityType.file);
         });
 
         test('isDirectoryIfTargetIsDirectory', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.directory(ns('/bar')).createSync();
           expect(l.statSync().type, FileSystemEntityType.directory);
         });
@@ -3068,7 +3067,7 @@
 
       group('delete', () {
         test('returnsCovariantType', () async {
-          Link link = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var link = fs.link(ns('/foo'))..createSync(ns('/bar'));
           expect(await link.delete(), isLink);
         });
 
@@ -3118,7 +3117,7 @@
         });
 
         test('unlinksIfTargetIsFileAndRecursiveFalse', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.file(ns('/bar')).createSync();
           l.deleteSync();
           expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3128,7 +3127,7 @@
         });
 
         test('unlinksIfTargetIsFileAndRecursiveTrue', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.file(ns('/bar')).createSync();
           l.deleteSync(recursive: true);
           expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3138,7 +3137,7 @@
         });
 
         test('unlinksIfTargetIsDirectoryAndRecursiveFalse', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.directory(ns('/bar')).createSync();
           l.deleteSync();
           expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3148,7 +3147,7 @@
         });
 
         test('unlinksIfTargetIsDirectoryAndRecursiveTrue', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.directory(ns('/bar')).createSync();
           l.deleteSync(recursive: true);
           expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3158,7 +3157,7 @@
         });
 
         test('unlinksIfTargetIsLinkLoop', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.link(ns('/bar')).createSync(ns('/foo'));
           l.deleteSync();
           expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3178,7 +3177,7 @@
         });
 
         test('ignoresLinkTarget', () {
-          Link l = fs.link(ns('/foo/bar'))
+          var l = fs.link(ns('/foo/bar'))
             ..createSync(ns('/baz/qux'), recursive: true);
           expect(l.parent.path, ns('/foo'));
         });
@@ -3190,7 +3189,7 @@
         });
 
         test('succeedsIfLinkDoesntExistAtTail', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           expect(fs.typeSync(ns('/foo'), followLinks: false),
               FileSystemEntityType.link);
           expect(l.targetSync(), ns('/bar'));
@@ -3203,7 +3202,7 @@
         });
 
         test('succeedsIfLinkDoesntExistViaTraversalAndRecursiveTrue', () {
-          Link l = fs.link(ns('/foo/bar'))..createSync('baz', recursive: true);
+          var l = fs.link(ns('/foo/bar'))..createSync('baz', recursive: true);
           expect(fs.typeSync(ns('/foo'), followLinks: false),
               FileSystemEntityType.directory);
           expect(fs.typeSync(ns('/foo/bar'), followLinks: false),
@@ -3242,7 +3241,7 @@
 
       group('update', () {
         test('returnsCovariantType', () async {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           expect(await l.update(ns('/baz')), isLink);
         });
 
@@ -3336,24 +3335,24 @@
         });
 
         test('succeedsIfTargetIsNotFound', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           expect(l.targetSync(), ns('/bar'));
         });
 
         test('succeedsIfTargetIsFile', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.file(ns('/bar')).createSync();
           expect(l.targetSync(), ns('/bar'));
         });
 
         test('succeedsIfTargetIsDirectory', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.directory(ns('/bar')).createSync();
           expect(l.targetSync(), ns('/bar'));
         });
 
         test('succeedsIfTargetIsLinkLoop', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.link(ns('/bar')).createSync(ns('/foo'));
           expect(l.targetSync(), ns('/bar'));
         });
@@ -3393,9 +3392,9 @@
         });
 
         test('succeedsIfSourceIsLinkToFile', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.file(ns('/bar')).createSync();
-          Link renamed = l.renameSync(ns('/baz'));
+          var renamed = l.renameSync(ns('/baz'));
           expect(renamed.path, ns('/baz'));
           expect(fs.typeSync(ns('/foo'), followLinks: false),
               FileSystemEntityType.notFound);
@@ -3407,8 +3406,8 @@
         });
 
         test('succeedsIfSourceIsLinkToNotFound', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
-          Link renamed = l.renameSync(ns('/baz'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var renamed = l.renameSync(ns('/baz'));
           expect(renamed.path, ns('/baz'));
           expect(fs.typeSync(ns('/foo'), followLinks: false),
               FileSystemEntityType.notFound);
@@ -3418,9 +3417,9 @@
         });
 
         test('succeedsIfSourceIsLinkToDirectory', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.directory(ns('/bar')).createSync();
-          Link renamed = l.renameSync(ns('/baz'));
+          var renamed = l.renameSync(ns('/baz'));
           expect(renamed.path, ns('/baz'));
           expect(fs.typeSync(ns('/foo'), followLinks: false),
               FileSystemEntityType.notFound);
@@ -3432,9 +3431,9 @@
         });
 
         test('succeedsIfSourceIsLinkLoop', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          Link renamed = l.renameSync(ns('/baz'));
+          var renamed = l.renameSync(ns('/baz'));
           expect(renamed.path, ns('/baz'));
           expect(fs.typeSync(ns('/foo'), followLinks: false),
               FileSystemEntityType.notFound);
@@ -3446,22 +3445,22 @@
         });
 
         test('succeedsIfDestinationDoesntExistAtTail', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
-          Link renamed = l.renameSync(ns('/baz'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var renamed = l.renameSync(ns('/baz'));
           expect(renamed.path, ns('/baz'));
           expect(fs.link(ns('/foo')), isNot(exists));
           expect(fs.link(ns('/baz')), exists);
         });
 
         test('throwsIfDestinationDoesntExistViaTraversal', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           expectFileSystemException(ErrorCodes.ENOENT, () {
             l.renameSync(ns('/baz/qux'));
           });
         });
 
         test('throwsIfDestinationExistsAsFile', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.file(ns('/baz')).createSync();
           expectFileSystemException(ErrorCodes.EINVAL, () {
             l.renameSync(ns('/baz'));
@@ -3469,7 +3468,7 @@
         });
 
         test('throwsIfDestinationExistsAsDirectory', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.directory(ns('/baz')).createSync();
           expectFileSystemException(ErrorCodes.EINVAL, () {
             l.renameSync(ns('/baz'));
@@ -3477,7 +3476,7 @@
         });
 
         test('succeedsIfDestinationExistsAsLinkToFile', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.file(ns('/baz')).createSync();
           fs.link(ns('/qux')).createSync(ns('/baz'));
           l.renameSync(ns('/qux'));
@@ -3490,7 +3489,7 @@
         });
 
         test('throwsIfDestinationExistsAsLinkToDirectory', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.directory(ns('/baz')).createSync();
           fs.link(ns('/qux')).createSync(ns('/baz'));
           l.renameSync(ns('/qux'));
@@ -3503,7 +3502,7 @@
         });
 
         test('succeedsIfDestinationExistsAsLinkToNotFound', () {
-          Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+          var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
           fs.link(ns('/baz')).createSync(ns('/qux'));
           l.renameSync(ns('/baz'));
           expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound);
diff --git a/pkgs/file/test/local_test.dart b/pkgs/file/test/local_test.dart
index e1618d2..b794ccd 100644
--- a/pkgs/file/test/local_test.dart
+++ b/pkgs/file/test/local_test.dart
@@ -2,7 +2,11 @@
 // 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.
 
+// ignore_for_file: lines_longer_than_80_chars
+
 @TestOn('vm')
+library;
+
 import 'dart:io' as io;
 
 import 'package:file/local.dart';
@@ -33,7 +37,7 @@
     setUpAll(() {
       if (io.Platform.isWindows) {
         // TODO(tvolkert): Remove once all more serious test failures are fixed
-        // https://github.com/google/file.dart/issues/56
+        // https://github.com/dart-lang/tools/issues/618
         ignoreOsErrorCodes = true;
       }
     });
@@ -42,7 +46,7 @@
       ignoreOsErrorCodes = false;
     });
 
-    Map<String, List<String>> skipOnPlatform = <String, List<String>>{
+    var skipOnPlatform = <String, List<String>>{
       'windows': <String>[
         'FileSystem > currentDirectory > throwsIfHasNonExistentPathInComplexChain',
         'FileSystem > currentDirectory > resolvesLinksIfEncountered',
diff --git a/pkgs/file/test/memory_operations_test.dart b/pkgs/file/test/memory_operations_test.dart
index 5e27843..916707c 100644
--- a/pkgs/file/test/memory_operations_test.dart
+++ b/pkgs/file/test/memory_operations_test.dart
@@ -2,22 +2,21 @@
 // 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:file/file.dart';
 import 'package:file/memory.dart';
 import 'package:test/test.dart';
 
 void main() {
   test('Read operations invoke opHandle', () async {
-    List<String> contexts = <String>[];
-    List<FileSystemOp> operations = <FileSystemOp>[];
-    MemoryFileSystem fs = MemoryFileSystem.test(
+    var contexts = <String>[];
+    var operations = <FileSystemOp>[];
+    var fs = MemoryFileSystem.test(
         opHandle: (String context, FileSystemOp operation) {
       if (operation == FileSystemOp.read) {
         contexts.add(context);
         operations.add(operation);
       }
     });
-    final File file = fs.file('test')..createSync();
+    final file = fs.file('test')..createSync();
 
     await file.readAsBytes();
     file.readAsBytesSync();
@@ -34,16 +33,16 @@
   });
 
   test('Write operations invoke opHandle', () async {
-    List<String> contexts = <String>[];
-    List<FileSystemOp> operations = <FileSystemOp>[];
-    MemoryFileSystem fs = MemoryFileSystem.test(
+    var contexts = <String>[];
+    var operations = <FileSystemOp>[];
+    var fs = MemoryFileSystem.test(
         opHandle: (String context, FileSystemOp operation) {
       if (operation == FileSystemOp.write) {
         contexts.add(context);
         operations.add(operation);
       }
     });
-    final File file = fs.file('test')..createSync();
+    final file = fs.file('test')..createSync();
 
     await file.writeAsBytes(<int>[]);
     file.writeAsBytesSync(<int>[]);
@@ -60,18 +59,18 @@
   });
 
   test('Delete operations invoke opHandle', () async {
-    List<String> contexts = <String>[];
-    List<FileSystemOp> operations = <FileSystemOp>[];
-    MemoryFileSystem fs = MemoryFileSystem.test(
+    var contexts = <String>[];
+    var operations = <FileSystemOp>[];
+    var fs = MemoryFileSystem.test(
         opHandle: (String context, FileSystemOp operation) {
       if (operation == FileSystemOp.delete) {
         contexts.add(context);
         operations.add(operation);
       }
     });
-    final File file = fs.file('test')..createSync();
-    final Directory directory = fs.directory('testDir')..createSync();
-    final Link link = fs.link('testLink')..createSync('foo');
+    final file = fs.file('test')..createSync();
+    final directory = fs.directory('testDir')..createSync();
+    final link = fs.link('testLink')..createSync('foo');
 
     await file.delete();
     file.createSync();
@@ -98,9 +97,9 @@
   });
 
   test('Create operations invoke opHandle', () async {
-    List<String> contexts = <String>[];
-    List<FileSystemOp> operations = <FileSystemOp>[];
-    MemoryFileSystem fs = MemoryFileSystem.test(
+    var contexts = <String>[];
+    var operations = <FileSystemOp>[];
+    var fs = MemoryFileSystem.test(
         opHandle: (String context, FileSystemOp operation) {
       if (operation == FileSystemOp.create) {
         contexts.add(context);
@@ -139,16 +138,16 @@
   });
 
   test('Open operations invoke opHandle', () async {
-    List<String> contexts = <String>[];
-    List<FileSystemOp> operations = <FileSystemOp>[];
-    MemoryFileSystem fs = MemoryFileSystem.test(
+    var contexts = <String>[];
+    var operations = <FileSystemOp>[];
+    var fs = MemoryFileSystem.test(
         opHandle: (String context, FileSystemOp operation) {
       if (operation == FileSystemOp.open) {
         contexts.add(context);
         operations.add(operation);
       }
     });
-    final File file = fs.file('test')..createSync();
+    final file = fs.file('test')..createSync();
 
     await file.open();
     file.openSync();
@@ -165,16 +164,16 @@
   });
 
   test('Copy operations invoke opHandle', () async {
-    List<String> contexts = <String>[];
-    List<FileSystemOp> operations = <FileSystemOp>[];
-    MemoryFileSystem fs = MemoryFileSystem.test(
+    var contexts = <String>[];
+    var operations = <FileSystemOp>[];
+    var fs = MemoryFileSystem.test(
         opHandle: (String context, FileSystemOp operation) {
       if (operation == FileSystemOp.copy) {
         contexts.add(context);
         operations.add(operation);
       }
     });
-    final File file = fs.file('test')..createSync();
+    final file = fs.file('test')..createSync();
 
     await file.copy('A');
     file.copySync('B');
@@ -187,9 +186,9 @@
   });
 
   test('Exists operations invoke opHandle', () async {
-    List<String> contexts = <String>[];
-    List<FileSystemOp> operations = <FileSystemOp>[];
-    MemoryFileSystem fs = MemoryFileSystem.test(
+    var contexts = <String>[];
+    var operations = <FileSystemOp>[];
+    var fs = MemoryFileSystem.test(
         opHandle: (String context, FileSystemOp operation) {
       if (operation == FileSystemOp.exists) {
         contexts.add(context);
diff --git a/pkgs/file/test/memory_test.dart b/pkgs/file/test/memory_test.dart
index f3b324e..ce8675f 100644
--- a/pkgs/file/test/memory_test.dart
+++ b/pkgs/file/test/memory_test.dart
@@ -66,8 +66,7 @@
   });
 
   test('MemoryFileSystem.test', () {
-    final MemoryFileSystem fs =
-        MemoryFileSystem.test(); // creates root directory
+    final fs = MemoryFileSystem.test(); // creates root directory
     fs.file('/test1.txt').createSync(); // creates file
     fs.file('/test2.txt').createSync(); // creates file
     expect(fs.directory('/').statSync().modified, DateTime(2000, 1, 1, 0, 1));
@@ -95,10 +94,10 @@
   });
 
   test('MemoryFile.openSync returns a MemoryRandomAccessFile', () async {
-    final MemoryFileSystem fs = MemoryFileSystem.test();
+    final fs = MemoryFileSystem.test();
     final io.File file = fs.file('/test1')..createSync();
 
-    io.RandomAccessFile raf = file.openSync();
+    var raf = file.openSync();
     try {
       expect(raf, isA<MemoryRandomAccessFile>());
     } finally {
@@ -114,7 +113,7 @@
   });
 
   test('MemoryFileSystem.systemTempDirectory test', () {
-    final MemoryFileSystem fs = MemoryFileSystem.test();
+    final fs = MemoryFileSystem.test();
 
     final io.Directory fooA = fs.systemTempDirectory.createTempSync('foo');
     final io.Directory fooB = fs.systemTempDirectory.createTempSync('foo');
@@ -122,7 +121,7 @@
     expect(fooA.path, '/.tmp_rand0/foorand0');
     expect(fooB.path, '/.tmp_rand0/foorand1');
 
-    final MemoryFileSystem secondFs = MemoryFileSystem.test();
+    final secondFs = MemoryFileSystem.test();
 
     final io.Directory fooAA =
         secondFs.systemTempDirectory.createTempSync('foo');
@@ -136,16 +135,16 @@
 
   test('Failed UTF8 decoding in MemoryFileSystem throws a FileSystemException',
       () {
-    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
-    final File file = fileSystem.file('foo')
+    final fileSystem = MemoryFileSystem.test();
+    final file = fileSystem.file('foo')
       ..writeAsBytesSync(<int>[0xFFFE]); // Invalid UTF8
 
     expect(file.readAsStringSync, throwsA(isA<FileSystemException>()));
   });
 
   test('Creating a temporary directory actually creates the directory', () {
-    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
-    final Directory tempDir = fileSystem.currentDirectory.createTempSync('foo');
+    final fileSystem = MemoryFileSystem.test();
+    final tempDir = fileSystem.currentDirectory.createTempSync('foo');
 
     expect(tempDir.existsSync(), true);
   });
diff --git a/pkgs/file/test/utils.dart b/pkgs/file/test/utils.dart
index 231312f..797ec9d 100644
--- a/pkgs/file/test/utils.dart
+++ b/pkgs/file/test/utils.dart
@@ -25,7 +25,7 @@
 /// If [time] is not specified, it will default to the current time.
 DateTime ceil([DateTime? time]) {
   time ??= DateTime.now();
-  int microseconds = (1000 * time.millisecond) + time.microsecond;
+  var microseconds = (1000 * time.millisecond) + time.microsecond;
   return (microseconds == 0)
       ? time
       // Add just enough milliseconds and microseconds to reach the next second.
@@ -78,7 +78,7 @@
     bool verbose,
   ) {
     if (item is DateTime) {
-      Duration diff = item.difference(_time).abs();
+      var diff = item.difference(_time).abs();
       return description.add('is $mismatchAdjective $_time by $diff');
     } else {
       return description.add('is not a DateTime');
diff --git a/pkgs/file/test/utils_test.dart b/pkgs/file/test/utils_test.dart
index 75293bf..23788e9 100644
--- a/pkgs/file/test/utils_test.dart
+++ b/pkgs/file/test/utils_test.dart
@@ -8,9 +8,9 @@
 
 void main() {
   test('floorAndCeilProduceExactSecondDateTime', () {
-    DateTime time = DateTime.fromMicrosecondsSinceEpoch(1001);
-    DateTime lower = floor(time);
-    DateTime upper = ceil(time);
+    var time = DateTime.fromMicrosecondsSinceEpoch(1001);
+    var lower = floor(time);
+    var upper = ceil(time);
     expect(lower.millisecond, 0);
     expect(upper.millisecond, 0);
     expect(lower.microsecond, 0);
@@ -18,26 +18,26 @@
   });
 
   test('floorAndCeilWorkWithNow', () {
-    DateTime time = DateTime.now();
-    int lower = time.difference(floor(time)).inMicroseconds;
-    int upper = ceil(time).difference(time).inMicroseconds;
+    var time = DateTime.now();
+    var lower = time.difference(floor(time)).inMicroseconds;
+    var upper = ceil(time).difference(time).inMicroseconds;
     expect(lower, lessThan(1000000));
     expect(upper, lessThanOrEqualTo(1000000));
   });
 
   test('floorAndCeilWorkWithExactSecondDateTime', () {
-    DateTime time = DateTime.parse('1999-12-31 23:59:59');
-    DateTime lower = floor(time);
-    DateTime upper = ceil(time);
+    var time = DateTime.parse('1999-12-31 23:59:59');
+    var lower = floor(time);
+    var upper = ceil(time);
     expect(lower, time);
     expect(upper, time);
   });
 
   test('floorAndCeilWorkWithInexactSecondDateTime', () {
-    DateTime time = DateTime.parse('1999-12-31 23:59:59.500');
-    DateTime lower = floor(time);
-    DateTime upper = ceil(time);
-    Duration difference = upper.difference(lower);
+    var time = DateTime.parse('1999-12-31 23:59:59.500');
+    var lower = floor(time);
+    var upper = ceil(time);
+    var difference = upper.difference(lower);
     expect(difference.inMicroseconds, 1000000);
   });
 }
diff --git a/pkgs/file_testing/CHANGELOG.md b/pkgs/file_testing/CHANGELOG.md
index 0af779d..17039ee 100644
--- a/pkgs/file_testing/CHANGELOG.md
+++ b/pkgs/file_testing/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 3.1.0-wip
+
+* Changed the type of several matchers to `TypeMatcher` which allows cascading
+  their usage with `.having` and similar. 
+
 ## 3.0.2
 
 * Require Dart 3.1.
diff --git a/pkgs/file_testing/analysis_options.yaml b/pkgs/file_testing/analysis_options.yaml
index 8fbd2e4..d978f81 100644
--- a/pkgs/file_testing/analysis_options.yaml
+++ b/pkgs/file_testing/analysis_options.yaml
@@ -1,6 +1 @@
-include: package:lints/recommended.yaml
-
-analyzer:
-  errors:
-    # Allow having TODOs in the code
-    todo: ignore
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/file_testing/lib/src/testing/core_matchers.dart b/pkgs/file_testing/lib/src/testing/core_matchers.dart
index f58539f..801209e 100644
--- a/pkgs/file_testing/lib/src/testing/core_matchers.dart
+++ b/pkgs/file_testing/lib/src/testing/core_matchers.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// ignore_for_file: comment_references
+
 import 'dart:io';
 
 import 'package:test/test.dart';
@@ -9,26 +11,27 @@
 import 'internal.dart';
 
 /// Matcher that successfully matches against any instance of [Directory].
-const Matcher isDirectory = TypeMatcher<Directory>();
+const isDirectory = TypeMatcher<Directory>();
 
 /// Matcher that successfully matches against any instance of [File].
-const Matcher isFile = TypeMatcher<File>();
+const isFile = TypeMatcher<File>();
 
 /// Matcher that successfully matches against any instance of [Link].
-const Matcher isLink = TypeMatcher<Link>();
+const isLink = TypeMatcher<Link>();
 
 /// Matcher that successfully matches against any instance of
 /// [FileSystemEntity].
-const Matcher isFileSystemEntity = TypeMatcher<FileSystemEntity>();
+const isFileSystemEntity = TypeMatcher<FileSystemEntity>();
 
 /// Matcher that successfully matches against any instance of [FileStat].
-const Matcher isFileStat = TypeMatcher<FileStat>();
+const isFileStat = TypeMatcher<FileStat>();
 
 /// Returns a [Matcher] that matches [path] against an entity's path.
 ///
 /// [path] may be a String, a predicate function, or a [Matcher]. If it is
 /// a String, it will be wrapped in an equality matcher.
-Matcher hasPath(dynamic path) => _HasPath(path);
+TypeMatcher<FileSystemEntity> hasPath(dynamic path) =>
+    isFileSystemEntity.having((e) => e.path, 'path', path);
 
 /// Returns a [Matcher] that successfully matches against an instance of
 /// [FileSystemException].
@@ -39,7 +42,8 @@
 /// [osErrorCode] may be an `int`, a predicate function, or a [Matcher]. If it
 /// is an `int`, it will be wrapped in an equality matcher.
 Matcher isFileSystemException([dynamic osErrorCode]) =>
-    _FileSystemException(osErrorCode);
+    const TypeMatcher<FileSystemException>().having((e) => e.osError?.errorCode,
+        'osError.errorCode', _fileExceptionWrapMatcher(osErrorCode));
 
 /// Returns a matcher that successfully matches against a future or function
 /// that throws a [FileSystemException].
@@ -67,89 +71,10 @@
 
 /// Matcher that successfully matches against a [FileSystemEntity] that
 /// exists ([FileSystemEntity.existsSync] returns true).
-const Matcher exists = _Exists();
+final TypeMatcher<FileSystemEntity> exists =
+    isFileSystemEntity.having((e) => e.existsSync(), 'existsSync', true);
 
-class _FileSystemException extends Matcher {
-  _FileSystemException(dynamic osErrorCode)
-      : _matcher = _wrapMatcher(osErrorCode);
-
-  final Matcher? _matcher;
-
-  static Matcher? _wrapMatcher(dynamic osErrorCode) {
-    if (osErrorCode == null) {
-      return null;
-    }
-    return ignoreOsErrorCodes ? anything : wrapMatcher(osErrorCode);
-  }
-
-  @override
-  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
-    if (item is FileSystemException) {
-      return _matcher == null ||
-          _matcher!.matches(item.osError?.errorCode, matchState);
-    }
-    return false;
-  }
-
-  @override
-  Description describe(Description desc) {
-    if (_matcher == null) {
-      return desc.add('FileSystemException');
-    } else {
-      desc.add('FileSystemException with osError.errorCode: ');
-      return _matcher!.describe(desc);
-    }
-  }
-}
-
-class _HasPath extends Matcher {
-  _HasPath(dynamic path) : _matcher = wrapMatcher(path);
-
-  final Matcher _matcher;
-
-  @override
-  bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
-      _matcher.matches(item.path, matchState);
-
-  @override
-  Description describe(Description desc) {
-    desc.add('has path: ');
-    return _matcher.describe(desc);
-  }
-
-  @override
-  Description describeMismatch(
-    dynamic item,
-    Description desc,
-    Map<dynamic, dynamic> matchState,
-    bool verbose,
-  ) {
-    desc.add('has path: \'${item.path}\'').add('\n   Which: ');
-    final Description pathDesc = StringDescription();
-    _matcher.describeMismatch(item.path, pathDesc, matchState, verbose);
-    desc.add(pathDesc.toString());
-    return desc;
-  }
-}
-
-class _Exists extends Matcher {
-  const _Exists();
-
-  @override
-  bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
-      item is FileSystemEntity && item.existsSync();
-
-  @override
-  Description describe(Description description) =>
-      description.add('a file system entity that exists');
-
-  @override
-  Description describeMismatch(
-    dynamic item,
-    Description description,
-    Map<dynamic, dynamic> matchState,
-    bool verbose,
-  ) {
-    return description.add('does not exist');
-  }
-}
+Matcher? _fileExceptionWrapMatcher(dynamic osErrorCode) =>
+    (osErrorCode == null || ignoreOsErrorCodes)
+        ? anything
+        : wrapMatcher(osErrorCode);
diff --git a/pkgs/file_testing/pubspec.yaml b/pkgs/file_testing/pubspec.yaml
index 691efa0..895826a 100644
--- a/pkgs/file_testing/pubspec.yaml
+++ b/pkgs/file_testing/pubspec.yaml
@@ -1,5 +1,5 @@
 name: file_testing
-version: 3.0.2
+version: 3.1.0-wip
 description: Testing utilities for package:file.
 repository: https://github.com/dart-lang/tools/tree/main/pkgs/file_testing
 issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Afile_testing
@@ -10,5 +10,5 @@
 dependencies:
   test: ^1.23.1
 
-dev_dependencies: 
-  lints: ^5.0.0
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml
index bdbda2f..e25ee85 100644
--- a/pkgs/graphs/pubspec.yaml
+++ b/pkgs/graphs/pubspec.yaml
@@ -15,6 +15,6 @@
   test: ^1.21.6
 
   # For examples
-  analyzer: '>=5.2.0 <7.0.0'
+  analyzer: '>=5.2.0 <8.0.0'
   path: ^1.8.0
   pool: ^1.5.0
diff --git a/pkgs/io/.gitignore b/pkgs/io/.gitignore
new file mode 100644
index 0000000..01d42c0
--- /dev/null
+++ b/pkgs/io/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.pub/
+.packages
+pubspec.lock
diff --git a/pkgs/io/AUTHORS b/pkgs/io/AUTHORS
new file mode 100644
index 0000000..ff09364
--- /dev/null
+++ b/pkgs/io/AUTHORS
@@ -0,0 +1,7 @@
+# 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/io/CHANGELOG.md b/pkgs/io/CHANGELOG.md
new file mode 100644
index 0000000..e0631fa
--- /dev/null
+++ b/pkgs/io/CHANGELOG.md
@@ -0,0 +1,119 @@
+## 1.0.5
+
+* Require Dart 3.4.
+* Move to `dart-lang/tools` monorepo.
+
+## 1.0.4
+
+* Updates to the readme.
+
+## 1.0.3
+
+* Revert `meta` constraint to `^1.3.0`.
+
+## 1.0.2
+
+* Update `meta` constraint to `>=1.3.0 <3.0.0`.
+
+## 1.0.1
+
+* Update code examples to call the unified `dart` developer tool.
+
+## 1.0.0
+
+* Migrate this package to null-safety.
+* Require Dart >=2.12.
+
+## 0.3.5
+
+* Require Dart >=2.1.
+* Remove dependency on `package:charcode`.
+
+## 0.3.4
+
+* Fix a number of issues affecting the package score on `pub.dev`.
+
+## 0.3.3
+
+* Updates for Dart 2 constants. Require at least Dart `2.0.0-dev.54`.
+
+* Fix the type of `StartProcess` typedef to match `Process.start` from
+  `dart:io`.
+
+## 0.3.2+1
+
+* `ansi.dart`
+
+  * The "forScript" code paths now ignore the `ansiOutputEnabled` value. Affects
+    the `escapeForScript` property on `AnsiCode` and the `wrap` and `wrapWith`
+    functions when `forScript` is true.
+
+## 0.3.2
+
+* `ansi.dart`
+
+  * Added `forScript` named argument to top-level `wrapWith` function.
+
+  * `AnsiCode`
+
+    * Added `String get escapeForScript` property.
+
+    * Added `forScript` named argument to `wrap` function.
+
+## 0.3.1
+
+- Added `SharedStdIn.nextLine` (similar to `readLineSync`) and `lines`:
+
+```dart
+main() async {
+  // Prints the first line entered on stdin.
+  print(await sharedStdIn.nextLine());
+
+  // Prints all remaining lines.
+  await for (final line in sharedStdIn.lines) {
+    print(line);
+  }
+}
+```
+
+- Added a `copyPath` and `copyPathSync` function, similar to `cp -R`.
+
+- Added a dependency on `package:path`.
+
+- Added the remaining missing arguments to `ProcessManager.spawnX` which
+  forward to `Process.start`. It is now an interchangeable function for running
+  a process.
+
+## 0.3.0
+
+- **BREAKING CHANGE**: The `arguments` argument to `ProcessManager.spawn` is
+  now positional (not named) and required. This makes it more similar to the
+  built-in `Process.start`, and easier to use as a drop in replacement:
+
+```dart
+main() {
+  processManager.spawn('dart', ['--version']);
+}
+```
+
+- Fixed a bug where processes created from `ProcessManager.spawn` could not
+  have their `stdout`/`stderr` read through their respective getters (a runtime
+  error was always thrown).
+
+- Added `ProcessMangaer#spawnBackground`, which does not forward `stdin`.
+
+- Added `ProcessManager#spawnDetached`, which does not forward any I/O.
+
+- Added the `shellSplit()` function, which parses a list of arguments in the
+  same manner as [the POSIX shell][what_is_posix_shell].
+
+[what_is_posix_shell]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html
+
+## 0.2.0
+
+- Initial commit of...
+   - `FutureOr<bool> String isExecutable(path)`.
+   - `ExitCode`
+   - `ProcessManager` and `Spawn`
+   - `sharedStdIn` and `SharedStdIn`
+   - `ansi.dart` library with support for formatting terminal output
diff --git a/pkgs/io/LICENSE b/pkgs/io/LICENSE
new file mode 100644
index 0000000..03af64a
--- /dev/null
+++ b/pkgs/io/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2017, 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/io/README.md b/pkgs/io/README.md
new file mode 100644
index 0000000..adbc941
--- /dev/null
+++ b/pkgs/io/README.md
@@ -0,0 +1,104 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/io.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/io.yaml)
+[![pub package](https://img.shields.io/pub/v/io.svg)](https://pub.dev/packages/io)
+[![package publisher](https://img.shields.io/pub/publisher/io.svg)](https://pub.dev/packages/io/publisher)
+
+Contains utilities for the Dart VM's `dart:io`.
+
+## Usage - `io.dart`
+
+### Files
+
+#### `isExecutable`
+ 
+Returns whether a provided file path is considered _executable_ on the host
+operating system.
+
+### Processes
+
+#### `ExitCode`
+
+An `enum`-like class that contains known exit codes.
+
+#### `ProcessManager`
+
+A higher-level service for spawning and communicating with processes.
+
+##### Use `spawn` to create a process with std[in|out|err] forwarded by default
+
+```dart
+Future<void> main() async {
+  final manager = ProcessManager();
+
+  // Print `dart` tool version to stdout.
+  print('** Running `dart --version`');
+  var spawn = await manager.spawn('dart', ['--version']);
+  await spawn.exitCode;
+
+  // Check formatting and print the result to stdout.
+  print('** Running `dart format --output=none .`');
+  spawn = await manager.spawn('dart', ['format', '--output=none', '.']);
+  await spawn.exitCode;
+
+  // Check if a package is ready for publishing.
+  // Upon hitting a blocking stdin state, you may directly
+  // output to the processes's stdin via your own, similar to how a bash or
+  // shell script would spawn a process.
+  print('** Running pub publish');
+  spawn = await manager.spawn('dart', ['pub', 'publish', '--dry-run']);
+  await spawn.exitCode;
+
+  // Closes stdin for the entire program.
+  await sharedStdIn.terminate();
+}
+```
+
+#### `sharedStdIn`
+
+A safer version of the default `stdin` stream from `dart:io` that allows a
+subscriber to cancel their subscription, and then allows a _new_ subscriber to
+start listening. This differs from the default behavior where only a single
+listener is ever allowed in the application lifecycle:
+
+```dart
+test('should allow multiple subscribers', () async {
+  final logs = <String>[];
+  final asUtf8 = sharedStdIn.transform(UTF8.decoder);
+  // Wait for input for the user.
+  logs.add(await asUtf8.first);
+  // Wait for more input for the user.
+  logs.add(await asUtf8.first);
+  expect(logs, ['Hello World', 'Goodbye World']);
+});
+```
+
+For testing, an instance of `SharedStdIn` may be created directly.
+
+## Usage - `ansi.dart`
+
+```dart
+import 'dart:io' as io;
+import 'package:io/ansi.dart';
+
+void main() {
+  // To use one style, call the `wrap` method on one of the provided top-level
+  // values.
+  io.stderr.writeln(red.wrap("Bad error!"));
+
+  // To use multiple styles, call `wrapWith`.
+  print(wrapWith('** Important **', [red, styleBold, styleUnderlined]));
+
+  // The wrap functions will simply return the provided value unchanged if
+  // `ansiOutputEnabled` is false.
+  //
+  // You can override the value `ansiOutputEnabled` by wrapping code in
+  // `overrideAnsiOutput`.
+  overrideAnsiOutput(false, () {
+    assert('Normal text' == green.wrap('Normal text'));
+  });
+}
+```
+
+## Publishing automation
+
+For information about our publishing automation and release process, see
+https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.
diff --git a/pkgs/io/analysis_options.yaml b/pkgs/io/analysis_options.yaml
new file mode 100644
index 0000000..6d74ee9
--- /dev/null
+++ b/pkgs/io/analysis_options.yaml
@@ -0,0 +1,32 @@
+# 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
+    - 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
+    - prefer_final_locals
+    - unnecessary_await_in_return
+    - unnecessary_breaks
+    - use_if_null_to_convert_nulls_to_bools
+    - use_raw_strings
+    - use_string_buffers
diff --git a/pkgs/io/example/example.dart b/pkgs/io/example/example.dart
new file mode 100644
index 0000000..8e358fd
--- /dev/null
+++ b/pkgs/io/example/example.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, 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';
+
+import 'package:io/ansi.dart';
+
+/// Prints a sample of all of the `AnsiCode` values.
+void main(List<String> args) {
+  final forScript = args.contains('--for-script');
+
+  if (!ansiOutputEnabled) {
+    print('`ansiOutputEnabled` is `false`.');
+    print("Don't expect pretty output.");
+  }
+  _preview('Foreground', foregroundColors, forScript);
+  _preview('Background', backgroundColors, forScript);
+  _preview('Styles', styles, forScript);
+}
+
+void _preview(String name, List<AnsiCode> values, bool forScript) {
+  print('');
+  final longest = values.map((ac) => ac.name.length).reduce(max);
+
+  print(wrapWith('** $name **', [styleBold, styleUnderlined]));
+  for (var code in values) {
+    final header =
+        '${code.name.padRight(longest)} ${code.code.toString().padLeft(3)}';
+
+    print("$header: ${code.wrap('Sample', forScript: forScript)}");
+  }
+}
diff --git a/pkgs/io/example/spawn_process_example.dart b/pkgs/io/example/spawn_process_example.dart
new file mode 100644
index 0000000..b7ba247
--- /dev/null
+++ b/pkgs/io/example/spawn_process_example.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, 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:io/io.dart';
+
+/// Runs a few subcommands in the `dart` command.
+Future<void> main() async {
+  final manager = ProcessManager();
+
+  // Print `dart` tool version to stdout.
+  print('** Running `dart --version`');
+  var spawn = await manager.spawn('dart', ['--version']);
+  await spawn.exitCode;
+
+  // Check formatting and print the result to stdout.
+  print('** Running `dart format --output=none .`');
+  spawn = await manager.spawn('dart', ['format', '--output=none', '.']);
+  await spawn.exitCode;
+
+  // Check if a package is ready for publishing.
+  // Upon hitting a blocking stdin state, you may directly
+  // output to the processes's stdin via your own, similar to how a bash or
+  // shell script would spawn a process.
+  print('** Running pub publish');
+  spawn = await manager.spawn('dart', ['pub', 'publish', '--dry-run']);
+  await spawn.exitCode;
+
+  // Closes stdin for the entire program.
+  await sharedStdIn.terminate();
+}
diff --git a/pkgs/io/lib/ansi.dart b/pkgs/io/lib/ansi.dart
new file mode 100644
index 0000000..a2adbe7
--- /dev/null
+++ b/pkgs/io/lib/ansi.dart
@@ -0,0 +1,5 @@
+// Copyright (c) 2017, 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/ansi_code.dart';
diff --git a/pkgs/io/lib/io.dart b/pkgs/io/lib/io.dart
new file mode 100644
index 0000000..8ee0843
--- /dev/null
+++ b/pkgs/io/lib/io.dart
@@ -0,0 +1,10 @@
+// Copyright (c) 2017, 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/copy_path.dart' show copyPath, copyPathSync;
+export 'src/exit_code.dart' show ExitCode;
+export 'src/permissions.dart' show isExecutable;
+export 'src/process_manager.dart' show ProcessManager, Spawn, StartProcess;
+export 'src/shared_stdin.dart' show SharedStdIn, sharedStdIn;
+export 'src/shell_words.dart' show shellSplit;
diff --git a/pkgs/io/lib/src/ansi_code.dart b/pkgs/io/lib/src/ansi_code.dart
new file mode 100644
index 0000000..c9a22c5
--- /dev/null
+++ b/pkgs/io/lib/src/ansi_code.dart
@@ -0,0 +1,316 @@
+// Copyright (c) 2017, 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:io' as io;
+
+const _ansiEscapeLiteral = '\x1B';
+const _ansiEscapeForScript = r'\033';
+
+/// Whether formatted ANSI output is enabled for [wrapWith] and [AnsiCode.wrap].
+///
+/// By default, returns `true` if both `stdout.supportsAnsiEscapes` and
+/// `stderr.supportsAnsiEscapes` from `dart:io` are `true`.
+///
+/// The default can be overridden by setting the [Zone] variable [AnsiCode] to
+/// either `true` or `false`.
+///
+/// [overrideAnsiOutput] is provided to make this easy.
+bool get ansiOutputEnabled =>
+    Zone.current[AnsiCode] as bool? ??
+    (io.stdout.supportsAnsiEscapes && io.stderr.supportsAnsiEscapes);
+
+/// Returns `true` no formatting is required for [input].
+bool _isNoop(bool skip, String? input, bool? forScript) =>
+    skip ||
+    input == null ||
+    input.isEmpty ||
+    !((forScript ?? false) || ansiOutputEnabled);
+
+/// Allows overriding [ansiOutputEnabled] to [enableAnsiOutput] for the code run
+/// within [body].
+T overrideAnsiOutput<T>(bool enableAnsiOutput, T Function() body) =>
+    runZoned(body, zoneValues: <Object, Object>{AnsiCode: enableAnsiOutput});
+
+/// The type of code represented by [AnsiCode].
+class AnsiCodeType {
+  final String _name;
+
+  /// A foreground color.
+  static const AnsiCodeType foreground = AnsiCodeType._('foreground');
+
+  /// A style.
+  static const AnsiCodeType style = AnsiCodeType._('style');
+
+  /// A background color.
+  static const AnsiCodeType background = AnsiCodeType._('background');
+
+  /// A reset value.
+  static const AnsiCodeType reset = AnsiCodeType._('reset');
+
+  const AnsiCodeType._(this._name);
+
+  @override
+  String toString() => 'AnsiType.$_name';
+}
+
+/// Standard ANSI escape code for customizing terminal text output.
+///
+/// [Source](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
+class AnsiCode {
+  /// The numeric value associated with this code.
+  final int code;
+
+  /// The [AnsiCode] that resets this value, if one exists.
+  ///
+  /// Otherwise, `null`.
+  final AnsiCode? reset;
+
+  /// A description of this code.
+  final String name;
+
+  /// The type of code that is represented.
+  final AnsiCodeType type;
+
+  const AnsiCode._(this.name, this.type, this.code, this.reset);
+
+  /// Represents the value escaped for use in terminal output.
+  String get escape => '$_ansiEscapeLiteral[${code}m';
+
+  /// Represents the value as an unescaped literal suitable for scripts.
+  String get escapeForScript => '$_ansiEscapeForScript[${code}m';
+
+  String _escapeValue({bool forScript = false}) =>
+      forScript ? escapeForScript : escape;
+
+  /// Wraps [value] with the [escape] value for this code, followed by
+  /// [resetAll].
+  ///
+  /// If [forScript] is `true`, the return value is an unescaped literal. The
+  /// value of [ansiOutputEnabled] is also ignored.
+  ///
+  /// Returns `value` unchanged if
+  ///   * [value] is `null` or empty
+  ///   * both [ansiOutputEnabled] and [forScript] are `false`.
+  ///   * [type] is [AnsiCodeType.reset]
+  String? wrap(String? value, {bool forScript = false}) =>
+      _isNoop(type == AnsiCodeType.reset, value, forScript)
+          ? value
+          : '${_escapeValue(forScript: forScript)}$value'
+              '${reset!._escapeValue(forScript: forScript)}';
+
+  @override
+  String toString() => '$name ${type._name} ($code)';
+}
+
+/// Returns a [String] formatted with [codes].
+///
+/// If [forScript] is `true`, the return value is an unescaped literal. The
+/// value of [ansiOutputEnabled] is also ignored.
+///
+/// Returns `value` unchanged if
+///   * [value] is `null` or empty.
+///   * both [ansiOutputEnabled] and [forScript] are `false`.
+///   * [codes] is empty.
+///
+/// Throws an [ArgumentError] if
+///   * [codes] contains more than one value of type [AnsiCodeType.foreground].
+///   * [codes] contains more than one value of type [AnsiCodeType.background].
+///   * [codes] contains any value of type [AnsiCodeType.reset].
+String? wrapWith(String? value, Iterable<AnsiCode> codes,
+    {bool forScript = false}) {
+  // Eliminate duplicates
+  final myCodes = codes.toSet();
+
+  if (_isNoop(myCodes.isEmpty, value, forScript)) {
+    return value;
+  }
+
+  var foreground = 0, background = 0;
+  for (var code in myCodes) {
+    switch (code.type) {
+      case AnsiCodeType.foreground:
+        foreground++;
+        if (foreground > 1) {
+          throw ArgumentError.value(codes, 'codes',
+              'Cannot contain more than one foreground color code.');
+        }
+      case AnsiCodeType.background:
+        background++;
+        if (background > 1) {
+          throw ArgumentError.value(codes, 'codes',
+              'Cannot contain more than one foreground color code.');
+        }
+      case AnsiCodeType.reset:
+        throw ArgumentError.value(
+            codes, 'codes', 'Cannot contain reset codes.');
+      case AnsiCodeType.style:
+        // Ignore.
+        break;
+    }
+  }
+
+  final sortedCodes = myCodes.map((ac) => ac.code).toList()..sort();
+  final escapeValue = forScript ? _ansiEscapeForScript : _ansiEscapeLiteral;
+
+  return "$escapeValue[${sortedCodes.join(';')}m$value"
+      '${resetAll._escapeValue(forScript: forScript)}';
+}
+
+//
+// Style values
+//
+
+const styleBold = AnsiCode._('bold', AnsiCodeType.style, 1, resetBold);
+const styleDim = AnsiCode._('dim', AnsiCodeType.style, 2, resetDim);
+const styleItalic = AnsiCode._('italic', AnsiCodeType.style, 3, resetItalic);
+const styleUnderlined =
+    AnsiCode._('underlined', AnsiCodeType.style, 4, resetUnderlined);
+const styleBlink = AnsiCode._('blink', AnsiCodeType.style, 5, resetBlink);
+const styleReverse = AnsiCode._('reverse', AnsiCodeType.style, 7, resetReverse);
+
+/// Not widely supported.
+const styleHidden = AnsiCode._('hidden', AnsiCodeType.style, 8, resetHidden);
+
+/// Not widely supported.
+const styleCrossedOut =
+    AnsiCode._('crossed out', AnsiCodeType.style, 9, resetCrossedOut);
+
+//
+// Reset values
+//
+
+const resetAll = AnsiCode._('all', AnsiCodeType.reset, 0, null);
+
+// NOTE: bold is weird. The reset code seems to be 22 sometimes – not 21
+// See https://gitlab.com/gnachman/iterm2/issues/3208
+const resetBold = AnsiCode._('bold', AnsiCodeType.reset, 22, null);
+const resetDim = AnsiCode._('dim', AnsiCodeType.reset, 22, null);
+const resetItalic = AnsiCode._('italic', AnsiCodeType.reset, 23, null);
+const resetUnderlined = AnsiCode._('underlined', AnsiCodeType.reset, 24, null);
+const resetBlink = AnsiCode._('blink', AnsiCodeType.reset, 25, null);
+const resetReverse = AnsiCode._('reverse', AnsiCodeType.reset, 27, null);
+const resetHidden = AnsiCode._('hidden', AnsiCodeType.reset, 28, null);
+const resetCrossedOut = AnsiCode._('crossed out', AnsiCodeType.reset, 29, null);
+
+//
+// Foreground values
+//
+
+const black = AnsiCode._('black', AnsiCodeType.foreground, 30, resetAll);
+const red = AnsiCode._('red', AnsiCodeType.foreground, 31, resetAll);
+const green = AnsiCode._('green', AnsiCodeType.foreground, 32, resetAll);
+const yellow = AnsiCode._('yellow', AnsiCodeType.foreground, 33, resetAll);
+const blue = AnsiCode._('blue', AnsiCodeType.foreground, 34, resetAll);
+const magenta = AnsiCode._('magenta', AnsiCodeType.foreground, 35, resetAll);
+const cyan = AnsiCode._('cyan', AnsiCodeType.foreground, 36, resetAll);
+const lightGray =
+    AnsiCode._('light gray', AnsiCodeType.foreground, 37, resetAll);
+const defaultForeground =
+    AnsiCode._('default', AnsiCodeType.foreground, 39, resetAll);
+const darkGray = AnsiCode._('dark gray', AnsiCodeType.foreground, 90, resetAll);
+const lightRed = AnsiCode._('light red', AnsiCodeType.foreground, 91, resetAll);
+const lightGreen =
+    AnsiCode._('light green', AnsiCodeType.foreground, 92, resetAll);
+const lightYellow =
+    AnsiCode._('light yellow', AnsiCodeType.foreground, 93, resetAll);
+const lightBlue =
+    AnsiCode._('light blue', AnsiCodeType.foreground, 94, resetAll);
+const lightMagenta =
+    AnsiCode._('light magenta', AnsiCodeType.foreground, 95, resetAll);
+const lightCyan =
+    AnsiCode._('light cyan', AnsiCodeType.foreground, 96, resetAll);
+const white = AnsiCode._('white', AnsiCodeType.foreground, 97, resetAll);
+
+//
+// Background values
+//
+
+const backgroundBlack =
+    AnsiCode._('black', AnsiCodeType.background, 40, resetAll);
+const backgroundRed = AnsiCode._('red', AnsiCodeType.background, 41, resetAll);
+const backgroundGreen =
+    AnsiCode._('green', AnsiCodeType.background, 42, resetAll);
+const backgroundYellow =
+    AnsiCode._('yellow', AnsiCodeType.background, 43, resetAll);
+const backgroundBlue =
+    AnsiCode._('blue', AnsiCodeType.background, 44, resetAll);
+const backgroundMagenta =
+    AnsiCode._('magenta', AnsiCodeType.background, 45, resetAll);
+const backgroundCyan =
+    AnsiCode._('cyan', AnsiCodeType.background, 46, resetAll);
+const backgroundLightGray =
+    AnsiCode._('light gray', AnsiCodeType.background, 47, resetAll);
+const backgroundDefault =
+    AnsiCode._('default', AnsiCodeType.background, 49, resetAll);
+const backgroundDarkGray =
+    AnsiCode._('dark gray', AnsiCodeType.background, 100, resetAll);
+const backgroundLightRed =
+    AnsiCode._('light red', AnsiCodeType.background, 101, resetAll);
+const backgroundLightGreen =
+    AnsiCode._('light green', AnsiCodeType.background, 102, resetAll);
+const backgroundLightYellow =
+    AnsiCode._('light yellow', AnsiCodeType.background, 103, resetAll);
+const backgroundLightBlue =
+    AnsiCode._('light blue', AnsiCodeType.background, 104, resetAll);
+const backgroundLightMagenta =
+    AnsiCode._('light magenta', AnsiCodeType.background, 105, resetAll);
+const backgroundLightCyan =
+    AnsiCode._('light cyan', AnsiCodeType.background, 106, resetAll);
+const backgroundWhite =
+    AnsiCode._('white', AnsiCodeType.background, 107, resetAll);
+
+/// All of the [AnsiCode] values that represent [AnsiCodeType.style].
+const List<AnsiCode> styles = [
+  styleBold,
+  styleDim,
+  styleItalic,
+  styleUnderlined,
+  styleBlink,
+  styleReverse,
+  styleHidden,
+  styleCrossedOut
+];
+
+/// All of the [AnsiCode] values that represent [AnsiCodeType.foreground].
+const List<AnsiCode> foregroundColors = [
+  black,
+  red,
+  green,
+  yellow,
+  blue,
+  magenta,
+  cyan,
+  lightGray,
+  defaultForeground,
+  darkGray,
+  lightRed,
+  lightGreen,
+  lightYellow,
+  lightBlue,
+  lightMagenta,
+  lightCyan,
+  white
+];
+
+/// All of the [AnsiCode] values that represent [AnsiCodeType.background].
+const List<AnsiCode> backgroundColors = [
+  backgroundBlack,
+  backgroundRed,
+  backgroundGreen,
+  backgroundYellow,
+  backgroundBlue,
+  backgroundMagenta,
+  backgroundCyan,
+  backgroundLightGray,
+  backgroundDefault,
+  backgroundDarkGray,
+  backgroundLightRed,
+  backgroundLightGreen,
+  backgroundLightYellow,
+  backgroundLightBlue,
+  backgroundLightMagenta,
+  backgroundLightCyan,
+  backgroundWhite
+];
diff --git a/pkgs/io/lib/src/charcodes.dart b/pkgs/io/lib/src/charcodes.dart
new file mode 100644
index 0000000..4acaf0a
--- /dev/null
+++ b/pkgs/io/lib/src/charcodes.dart
@@ -0,0 +1,34 @@
+// 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.
+
+// Generated using:
+//  pub global run charcode \$=dollar \'=single_quote \"=double_quote \
+//      \' '\\\n"$`# \t'
+
+/// "Horizontal Tab" control character, common name.
+const int $tab = 0x09;
+
+/// "Line feed" control character.
+const int $lf = 0x0a;
+
+/// Space character.
+const int $space = 0x20;
+
+/// Character `"`, short name.
+const int $doubleQuote = 0x22;
+
+/// Character `#`.
+const int $hash = 0x23;
+
+/// Character `$`.
+const int $dollar = 0x24;
+
+/// Character "'".
+const int $singleQuote = 0x27;
+
+/// Character `\`.
+const int $backslash = 0x5c;
+
+/// Character `` ` ``.
+const int $backquote = 0x60;
diff --git a/pkgs/io/lib/src/copy_path.dart b/pkgs/io/lib/src/copy_path.dart
new file mode 100644
index 0000000..3a999b6
--- /dev/null
+++ b/pkgs/io/lib/src/copy_path.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+bool _doNothing(String from, String to) {
+  if (p.canonicalize(from) == p.canonicalize(to)) {
+    return true;
+  }
+  if (p.isWithin(from, to)) {
+    throw ArgumentError('Cannot copy from $from to $to');
+  }
+  return false;
+}
+
+/// Copies all of the files in the [from] directory to [to].
+///
+/// This is similar to `cp -R <from> <to>`:
+/// * Symlinks are supported.
+/// * Existing files are over-written, if any.
+/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
+/// * If [from] and [to] are canonically the same, no operation occurs.
+///
+/// Returns a future that completes when complete.
+Future<void> copyPath(String from, String to) async {
+  if (_doNothing(from, to)) {
+    return;
+  }
+  await Directory(to).create(recursive: true);
+  await for (final file in Directory(from).list(recursive: true)) {
+    final copyTo = p.join(to, p.relative(file.path, from: from));
+    if (file is Directory) {
+      await Directory(copyTo).create(recursive: true);
+    } else if (file is File) {
+      await File(file.path).copy(copyTo);
+    } else if (file is Link) {
+      await Link(copyTo).create(await file.target(), recursive: true);
+    }
+  }
+}
+
+/// Copies all of the files in the [from] directory to [to].
+///
+/// This is similar to `cp -R <from> <to>`:
+/// * Symlinks are supported.
+/// * Existing files are over-written, if any.
+/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
+/// * If [from] and [to] are canonically the same, no operation occurs.
+///
+/// This action is performed synchronously (blocking I/O).
+void copyPathSync(String from, String to) {
+  if (_doNothing(from, to)) {
+    return;
+  }
+  Directory(to).createSync(recursive: true);
+  for (final file in Directory(from).listSync(recursive: true)) {
+    final copyTo = p.join(to, p.relative(file.path, from: from));
+    if (file is Directory) {
+      Directory(copyTo).createSync(recursive: true);
+    } else if (file is File) {
+      File(file.path).copySync(copyTo);
+    } else if (file is Link) {
+      Link(copyTo).createSync(file.targetSync(), recursive: true);
+    }
+  }
+}
diff --git a/pkgs/io/lib/src/exit_code.dart b/pkgs/io/lib/src/exit_code.dart
new file mode 100644
index 0000000..d405558
--- /dev/null
+++ b/pkgs/io/lib/src/exit_code.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2017, 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.
+
+/// Exit code constants.
+///
+/// [Source](https://www.freebsd.org/cgi/man.cgi?query=sysexits).
+class ExitCode {
+  /// Command completed successfully.
+  static const success = ExitCode._(0, 'success');
+
+  /// Command was used incorrectly.
+  ///
+  /// This may occur if the wrong number of arguments was used, a bad flag, or
+  /// bad syntax in a parameter.
+  static const usage = ExitCode._(64, 'usage');
+
+  /// Input data was used incorrectly.
+  ///
+  /// This should occur only for user data (not system files).
+  static const data = ExitCode._(65, 'data');
+
+  /// An input file (not a system file) did not exist or was not readable.
+  static const noInput = ExitCode._(66, 'noInput');
+
+  /// User specified did not exist.
+  static const noUser = ExitCode._(67, 'noUser');
+
+  /// Host specified did not exist.
+  static const noHost = ExitCode._(68, 'noHost');
+
+  /// A service is unavailable.
+  ///
+  /// This may occur if a support program or file does not exist. This may also
+  /// be used as a catch-all error when something you wanted to do does not
+  /// work, but you do not know why.
+  static const unavailable = ExitCode._(69, 'unavailable');
+
+  /// An internal software error has been detected.
+  ///
+  /// This should be limited to non-operating system related errors as possible.
+  static const software = ExitCode._(70, 'software');
+
+  /// An operating system error has been detected.
+  ///
+  /// This intended to be used for such thing as `cannot fork` or `cannot pipe`.
+  static const osError = ExitCode._(71, 'osError');
+
+  /// Some system file (e.g. `/etc/passwd`) does not exist or could not be read.
+  static const osFile = ExitCode._(72, 'osFile');
+
+  /// A (user specified) output file cannot be created.
+  static const cantCreate = ExitCode._(73, 'cantCreate');
+
+  /// An error occurred doing I/O on some file.
+  static const ioError = ExitCode._(74, 'ioError');
+
+  /// Temporary failure, indicating something is not really an error.
+  ///
+  /// In some cases, this can be re-attempted and will succeed later.
+  static const tempFail = ExitCode._(75, 'tempFail');
+
+  /// You did not have sufficient permissions to perform the operation.
+  ///
+  /// This is not intended for file system problems, which should use [noInput]
+  /// or [cantCreate], but rather for higher-level permissions.
+  static const noPerm = ExitCode._(77, 'noPerm');
+
+  /// Something was found in an unconfigured or misconfigured state.
+  static const config = ExitCode._(78, 'config');
+
+  /// Exit code value.
+  final int code;
+
+  /// Name of the exit code.
+  final String _name;
+
+  const ExitCode._(this.code, this._name);
+
+  @override
+  String toString() => '$_name: $code';
+}
diff --git a/pkgs/io/lib/src/permissions.dart b/pkgs/io/lib/src/permissions.dart
new file mode 100644
index 0000000..c516943
--- /dev/null
+++ b/pkgs/io/lib/src/permissions.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2017, 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:io';
+
+/// What type of permission is granted to a file based on file permission roles.
+enum _FilePermission {
+  execute,
+  // Although these two values are unused, their positions in the enum are
+  // meaningful.
+  write, // ignore: unused_field
+  read, // ignore: unused_field
+  setGid,
+  setUid,
+  sticky,
+}
+
+/// What type of role is assigned to a file.
+enum _FilePermissionRole {
+  world,
+  group,
+  user,
+}
+
+/// Returns whether file [stat] has [permission] for a [role] type.
+bool _hasPermission(
+  FileStat stat,
+  _FilePermission permission, {
+  _FilePermissionRole role = _FilePermissionRole.world,
+}) {
+  final index = _permissionBitIndex(permission, role);
+  return (stat.mode & (1 << index)) != 0;
+}
+
+int _permissionBitIndex(_FilePermission permission, _FilePermissionRole role) =>
+    switch (permission) {
+      _FilePermission.setUid => 11,
+      _FilePermission.setGid => 10,
+      _FilePermission.sticky => 9,
+      _ => (role.index * 3) + permission.index
+    };
+
+/// Returns whether [path] is considered an executable file on this OS.
+///
+/// May optionally define how to implement [getStat] or whether to execute based
+/// on whether this is the windows platform ([isWindows]) - if not set it is
+/// automatically extracted from `dart:io#Platform`.
+///
+/// **NOTE**: On windows this always returns `true`.
+FutureOr<bool> isExecutable(
+  String path, {
+  bool? isWindows,
+  FutureOr<FileStat> Function(String path) getStat = FileStat.stat,
+}) {
+  // Windows has no concept of executable.
+  if (isWindows ?? Platform.isWindows) return true;
+  final stat = getStat(path);
+  if (stat is FileStat) {
+    return _isExecutable(stat);
+  }
+  return stat.then(_isExecutable);
+}
+
+bool _isExecutable(FileStat stat) =>
+    stat.type == FileSystemEntityType.file &&
+    _FilePermissionRole.values.any(
+        (role) => _hasPermission(stat, _FilePermission.execute, role: role));
diff --git a/pkgs/io/lib/src/process_manager.dart b/pkgs/io/lib/src/process_manager.dart
new file mode 100644
index 0000000..84d22ec
--- /dev/null
+++ b/pkgs/io/lib/src/process_manager.dart
@@ -0,0 +1,255 @@
+// Copyright (c) 2017, 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.
+
+// ignore_for_file: close_sinks,cancel_subscriptions
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:meta/meta.dart';
+
+import 'shared_stdin.dart';
+
+/// Type definition for both [io.Process.start] and [ProcessManager.spawn].
+///
+/// Useful for taking different implementations of this base functionality.
+typedef StartProcess = Future<io.Process> Function(
+  String executable,
+  List<String> arguments, {
+  String workingDirectory,
+  Map<String, String> environment,
+  bool includeParentEnvironment,
+  bool runInShell,
+  io.ProcessStartMode mode,
+});
+
+/// A high-level abstraction around using and managing processes on the system.
+abstract class ProcessManager {
+  /// Terminates the global `stdin` listener, making future listens impossible.
+  ///
+  /// This method should be invoked only at the _end_ of a program's execution.
+  static Future<void> terminateStdIn() async {
+    await sharedStdIn.terminate();
+  }
+
+  /// Create a new instance of [ProcessManager] for the current platform.
+  ///
+  /// May manually specify whether the current platform [isWindows], otherwise
+  /// this is derived from the Dart runtime (i.e. [io.Platform.isWindows]).
+  factory ProcessManager({
+    Stream<List<int>>? stdin,
+    io.IOSink? stdout,
+    io.IOSink? stderr,
+    bool? isWindows,
+  }) {
+    stdin ??= sharedStdIn;
+    stdout ??= io.stdout;
+    stderr ??= io.stderr;
+    isWindows ??= io.Platform.isWindows;
+    if (isWindows) {
+      return _WindowsProcessManager(stdin, stdout, stderr);
+    }
+    return _UnixProcessManager(stdin, stdout, stderr);
+  }
+
+  final Stream<List<int>> _stdin;
+  final io.IOSink _stdout;
+  final io.IOSink _stderr;
+
+  const ProcessManager._(this._stdin, this._stdout, this._stderr);
+
+  /// Spawns a process by invoking [executable] with [arguments].
+  ///
+  /// This is _similar_ to [io.Process.start], but all standard input and output
+  /// is forwarded/routed between the process and the host, similar to how a
+  /// shell script works.
+  ///
+  /// Returns a future that completes with a handle to the spawned process.
+  Future<io.Process> spawn(
+    String executable,
+    Iterable<String> arguments, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    io.ProcessStartMode mode = io.ProcessStartMode.normal,
+  }) async {
+    final process = io.Process.start(
+      executable,
+      arguments.toList(),
+      workingDirectory: workingDirectory,
+      environment: environment,
+      includeParentEnvironment: includeParentEnvironment,
+      runInShell: runInShell,
+      mode: mode,
+    );
+    return _ForwardingSpawn(await process, _stdin, _stdout, _stderr);
+  }
+
+  /// Spawns a process by invoking [executable] with [arguments].
+  ///
+  /// This is _similar_ to [io.Process.start], but `stdout` and `stderr` is
+  /// forwarded/routed between the process and host, similar to how a shell
+  /// script works.
+  ///
+  /// Returns a future that completes with a handle to the spawned process.
+  Future<io.Process> spawnBackground(
+    String executable,
+    Iterable<String> arguments, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    io.ProcessStartMode mode = io.ProcessStartMode.normal,
+  }) async {
+    final process = io.Process.start(
+      executable,
+      arguments.toList(),
+      workingDirectory: workingDirectory,
+      environment: environment,
+      includeParentEnvironment: includeParentEnvironment,
+      runInShell: runInShell,
+      mode: mode,
+    );
+    return _ForwardingSpawn(
+      await process,
+      const Stream.empty(),
+      _stdout,
+      _stderr,
+    );
+  }
+
+  /// Spawns a process by invoking [executable] with [arguments].
+  ///
+  /// This is _identical to [io.Process.start] (no forwarding of I/O).
+  ///
+  /// Returns a future that completes with a handle to the spawned process.
+  Future<io.Process> spawnDetached(
+    String executable,
+    Iterable<String> arguments, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    io.ProcessStartMode mode = io.ProcessStartMode.normal,
+  }) async =>
+      io.Process.start(
+        executable,
+        arguments.toList(),
+        workingDirectory: workingDirectory,
+        environment: environment,
+        includeParentEnvironment: includeParentEnvironment,
+        runInShell: runInShell,
+        mode: mode,
+      );
+}
+
+/// A process instance created and managed through [ProcessManager].
+///
+/// Unlike one created directly by [io.Process.start] or [io.Process.run], a
+/// spawned process works more like executing a command in a shell script.
+class Spawn implements io.Process {
+  final io.Process _delegate;
+
+  Spawn._(this._delegate) {
+    _delegate.exitCode.then((_) => _onClosed());
+  }
+
+  @mustCallSuper
+  void _onClosed() {}
+
+  @override
+  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) =>
+      _delegate.kill(signal);
+
+  @override
+  Future<int> get exitCode => _delegate.exitCode;
+
+  @override
+  int get pid => _delegate.pid;
+
+  @override
+  Stream<List<int>> get stderr => _delegate.stderr;
+
+  @override
+  io.IOSink get stdin => _delegate.stdin;
+
+  @override
+  Stream<List<int>> get stdout => _delegate.stdout;
+}
+
+/// Forwards `stdin`/`stdout`/`stderr` to/from the host.
+class _ForwardingSpawn extends Spawn {
+  final StreamSubscription<List<int>> _stdInSub;
+  final StreamSubscription<List<int>> _stdOutSub;
+  final StreamSubscription<List<int>> _stdErrSub;
+  final StreamController<List<int>> _stdOut;
+  final StreamController<List<int>> _stdErr;
+
+  factory _ForwardingSpawn(
+    io.Process delegate,
+    Stream<List<int>> stdin,
+    io.IOSink stdout,
+    io.IOSink stderr,
+  ) {
+    final stdoutSelf = StreamController<List<int>>();
+    final stderrSelf = StreamController<List<int>>();
+    final stdInSub = stdin.listen(delegate.stdin.add);
+    final stdOutSub = delegate.stdout.listen((event) {
+      stdout.add(event);
+      stdoutSelf.add(event);
+    });
+    final stdErrSub = delegate.stderr.listen((event) {
+      stderr.add(event);
+      stderrSelf.add(event);
+    });
+    return _ForwardingSpawn._delegate(
+      delegate,
+      stdInSub,
+      stdOutSub,
+      stdErrSub,
+      stdoutSelf,
+      stderrSelf,
+    );
+  }
+
+  _ForwardingSpawn._delegate(
+    super.delegate,
+    this._stdInSub,
+    this._stdOutSub,
+    this._stdErrSub,
+    this._stdOut,
+    this._stdErr,
+  ) : super._();
+
+  @override
+  void _onClosed() {
+    _stdInSub.cancel();
+    _stdOutSub.cancel();
+    _stdErrSub.cancel();
+    super._onClosed();
+  }
+
+  @override
+  Stream<List<int>> get stdout => _stdOut.stream;
+
+  @override
+  Stream<List<int>> get stderr => _stdErr.stream;
+}
+
+class _UnixProcessManager extends ProcessManager {
+  const _UnixProcessManager(
+    super.stdin,
+    super.stdout,
+    super.stderr,
+  ) : super._();
+}
+
+class _WindowsProcessManager extends ProcessManager {
+  const _WindowsProcessManager(
+    super.stdin,
+    super.stdout,
+    super.stderr,
+  ) : super._();
+}
diff --git a/pkgs/io/lib/src/shared_stdin.dart b/pkgs/io/lib/src/shared_stdin.dart
new file mode 100644
index 0000000..72bb50c
--- /dev/null
+++ b/pkgs/io/lib/src/shared_stdin.dart
@@ -0,0 +1,99 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+
+/// A shared singleton instance of `dart:io`'s [stdin] stream.
+///
+/// _Unlike_ the normal [stdin] stream, [sharedStdIn] may switch subscribers
+/// as long as the previous subscriber cancels before the new subscriber starts
+/// listening.
+///
+/// [SharedStdIn.terminate] *must* be invoked in order to close the underlying
+/// connection to [stdin], allowing your program to close automatically without
+/// hanging.
+final SharedStdIn sharedStdIn = SharedStdIn(stdin);
+
+/// A singleton wrapper around `stdin` that allows new subscribers.
+///
+/// This class is visible in order to be used as a test harness for mock
+/// implementations of `stdin`. In normal programs, [sharedStdIn] should be
+/// used directly.
+@visibleForTesting
+class SharedStdIn extends Stream<List<int>> {
+  StreamController<List<int>>? _current;
+  StreamSubscription<List<int>>? _sub;
+
+  SharedStdIn([Stream<List<int>>? stream]) {
+    _sub = (stream ??= stdin).listen(_onInput);
+  }
+
+  /// Returns a future that completes with the next line.
+  ///
+  /// This is similar to the standard [Stdin.readLineSync], but asynchronous.
+  Future<String> nextLine({Encoding encoding = systemEncoding}) =>
+      lines(encoding: encoding).first;
+
+  /// Returns the stream transformed as UTF8 strings separated by line breaks.
+  ///
+  /// This is similar to synchronous code using [Stdin.readLineSync]:
+  /// ```dart
+  /// while (true) {
+  ///   var line = stdin.readLineSync();
+  ///   // ...
+  /// }
+  /// ```
+  ///
+  /// ... but asynchronous.
+  Stream<String> lines({Encoding encoding = systemEncoding}) =>
+      transform(utf8.decoder).transform(const LineSplitter());
+
+  void _onInput(List<int> event) => _getCurrent().add(event);
+
+  StreamController<List<int>> _getCurrent() =>
+      _current ??= StreamController<List<int>>(
+          onCancel: () {
+            _current = null;
+          },
+          sync: true);
+
+  @override
+  StreamSubscription<List<int>> listen(
+    void Function(List<int> event)? onData, {
+    Function? onError,
+    void Function()? onDone,
+    bool? cancelOnError,
+  }) {
+    if (_sub == null) {
+      throw StateError('Stdin has already been terminated.');
+    }
+    // ignore: close_sinks
+    final controller = _getCurrent();
+    if (controller.hasListener) {
+      throw StateError(''
+          'Subscriber already listening. The existing subscriber must cancel '
+          'before another may be added.');
+    }
+    return controller.stream.listen(
+      onData,
+      onDone: onDone,
+      onError: onError,
+      cancelOnError: cancelOnError,
+    );
+  }
+
+  /// Terminates the connection to `stdin`, closing all subscription.
+  Future<void> terminate() async {
+    if (_sub == null) {
+      throw StateError('Stdin has already been terminated.');
+    }
+    await _sub?.cancel();
+    await _current?.close();
+    _sub = null;
+  }
+}
diff --git a/pkgs/io/lib/src/shell_words.dart b/pkgs/io/lib/src/shell_words.dart
new file mode 100644
index 0000000..5fca6d9
--- /dev/null
+++ b/pkgs/io/lib/src/shell_words.dart
@@ -0,0 +1,142 @@
+// Copyright (c) 2017, 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.
+
+// ignore_for_file: comment_references
+
+import 'package:string_scanner/string_scanner.dart';
+
+import 'charcodes.dart';
+
+/// Splits [command] into tokens according to [the POSIX shell
+/// specification][spec].
+///
+/// [spec]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html
+///
+/// This returns the unquoted values of quoted tokens. For example,
+/// `shellSplit('foo "bar baz"')` returns `["foo", "bar baz"]`. It does not
+/// currently support here-documents. It does *not* treat dynamic features such
+/// as parameter expansion specially. For example, `shellSplit("foo $(bar
+/// baz)")` returns `["foo", "$(bar", "baz)"]`.
+///
+/// This will discard any comments at the end of [command].
+///
+/// Throws a [FormatException] if [command] isn't a valid shell command.
+List<String> shellSplit(String command) {
+  final scanner = StringScanner(command);
+  final results = <String>[];
+  final token = StringBuffer();
+
+  // Whether a token is being parsed, as opposed to a separator character. This
+  // is different than just [token.isEmpty], because empty quoted tokens can
+  // exist.
+  var hasToken = false;
+
+  while (!scanner.isDone) {
+    final next = scanner.readChar();
+    switch (next) {
+      case $backslash:
+        // Section 2.2.1: A <backslash> that is not quoted shall preserve the
+        // literal value of the following character, with the exception of a
+        // <newline>. If a <newline> follows the <backslash>, the shell shall
+        // interpret this as line continuation. The <backslash> and <newline>
+        // shall be removed before splitting the input into tokens. Since the
+        // escaped <newline> is removed entirely from the input and is not
+        // replaced by any white space, it cannot serve as a token separator.
+        if (scanner.scanChar($lf)) break;
+
+        hasToken = true;
+        token.writeCharCode(scanner.readChar());
+
+      case $singleQuote:
+        hasToken = true;
+        // Section 2.2.2: Enclosing characters in single-quotes ( '' ) shall
+        // preserve the literal value of each character within the
+        // single-quotes. A single-quote cannot occur within single-quotes.
+        final firstQuote = scanner.position - 1;
+        while (!scanner.scanChar($singleQuote)) {
+          _checkUnmatchedQuote(scanner, firstQuote);
+          token.writeCharCode(scanner.readChar());
+        }
+
+      case $doubleQuote:
+        hasToken = true;
+        // Section 2.2.3: Enclosing characters in double-quotes ( "" ) shall
+        // preserve the literal value of all characters within the
+        // double-quotes, with the exception of the characters backquote,
+        // <dollar-sign>, and <backslash>.
+        //
+        // (Note that this code doesn't preserve special behavior of backquote
+        // or dollar sign within double quotes, since those are dynamic
+        // features.)
+        final firstQuote = scanner.position - 1;
+        while (!scanner.scanChar($doubleQuote)) {
+          _checkUnmatchedQuote(scanner, firstQuote);
+
+          if (scanner.scanChar($backslash)) {
+            _checkUnmatchedQuote(scanner, firstQuote);
+
+            // The <backslash> shall retain its special meaning as an escape
+            // character (see Escape Character (Backslash)) only when followed
+            // by one of the following characters when considered special:
+            //
+            //     $ ` " \ <newline>
+            final next = scanner.readChar();
+            if (next == $lf) continue;
+            if (next == $dollar ||
+                next == $backquote ||
+                next == $doubleQuote ||
+                next == $backslash) {
+              token.writeCharCode(next);
+            } else {
+              token
+                ..writeCharCode($backslash)
+                ..writeCharCode(next);
+            }
+          } else {
+            token.writeCharCode(scanner.readChar());
+          }
+        }
+
+      case $hash:
+        // Section 2.3: If the current character is a '#' [and the previous
+        // characters was not part of a word], it and all subsequent characters
+        // up to, but excluding, the next <newline> shall be discarded as a
+        // comment. The <newline> that ends the line is not considered part of
+        // the comment.
+        if (hasToken) {
+          token.writeCharCode($hash);
+          break;
+        }
+
+        while (!scanner.isDone && scanner.peekChar() != $lf) {
+          scanner.readChar();
+        }
+
+      case $space:
+      case $tab:
+      case $lf:
+        // ignore: invariant_booleans
+        if (hasToken) results.add(token.toString());
+        hasToken = false;
+        token.clear();
+
+      default:
+        hasToken = true;
+        token.writeCharCode(next);
+    }
+  }
+
+  if (hasToken) results.add(token.toString());
+  return results;
+}
+
+/// Throws a [FormatException] if [scanner] is done indicating that a closing
+/// quote matching the one at position [openingQuote] is missing.
+void _checkUnmatchedQuote(StringScanner scanner, int openingQuote) {
+  if (!scanner.isDone) return;
+  final type = scanner.substring(openingQuote, openingQuote + 1) == '"'
+      ? 'double'
+      : 'single';
+  scanner.error('Unmatched $type quote.', position: openingQuote, length: 1);
+}
diff --git a/pkgs/io/pubspec.yaml b/pkgs/io/pubspec.yaml
new file mode 100644
index 0000000..7e00d99
--- /dev/null
+++ b/pkgs/io/pubspec.yaml
@@ -0,0 +1,19 @@
+name: io
+description: >-
+  Utilities for the Dart VM Runtime including support for ANSI colors, file
+  copying, and standard exit code values.
+version: 1.0.5
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/io
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  meta: ^1.3.0
+  path: ^1.8.0
+  string_scanner: ^1.1.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.6
+  test_descriptor: ^2.0.0
diff --git a/pkgs/io/test/_files/is_executable.sh b/pkgs/io/test/_files/is_executable.sh
new file mode 100755
index 0000000..f1f641a
--- /dev/null
+++ b/pkgs/io/test/_files/is_executable.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/pkgs/io/test/_files/is_not_executable.sh b/pkgs/io/test/_files/is_not_executable.sh
new file mode 100644
index 0000000..f1f641a
--- /dev/null
+++ b/pkgs/io/test/_files/is_not_executable.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/pkgs/io/test/_files/stderr_hello.dart b/pkgs/io/test/_files/stderr_hello.dart
new file mode 100644
index 0000000..ac7a7d3
--- /dev/null
+++ b/pkgs/io/test/_files/stderr_hello.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, 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';
+
+void main() => stderr.write('Hello');
diff --git a/pkgs/io/test/_files/stdin_echo.dart b/pkgs/io/test/_files/stdin_echo.dart
new file mode 100644
index 0000000..256e0ee
--- /dev/null
+++ b/pkgs/io/test/_files/stdin_echo.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, 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';
+
+void main() => stdout.writeln('You said: ${stdin.readLineSync()}');
diff --git a/pkgs/io/test/_files/stdout_hello.dart b/pkgs/io/test/_files/stdout_hello.dart
new file mode 100644
index 0000000..af3bf51
--- /dev/null
+++ b/pkgs/io/test/_files/stdout_hello.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, 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';
+
+void main() => stdout.write('Hello');
diff --git a/pkgs/io/test/ansi_code_test.dart b/pkgs/io/test/ansi_code_test.dart
new file mode 100644
index 0000000..98ae68b
--- /dev/null
+++ b/pkgs/io/test/ansi_code_test.dart
@@ -0,0 +1,187 @@
+// Copyright (c) 2017, 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:io/ansi.dart';
+import 'package:test/test.dart';
+
+const _ansiEscapeLiteral = '\x1B';
+const _ansiEscapeForScript = r'\033';
+const sampleInput = 'sample input';
+
+void main() {
+  group('ansiOutputEnabled', () {
+    test('default value matches dart:io', () {
+      expect(ansiOutputEnabled,
+          stdout.supportsAnsiEscapes && stderr.supportsAnsiEscapes);
+    });
+
+    test('override true', () {
+      overrideAnsiOutput(true, () {
+        expect(ansiOutputEnabled, isTrue);
+      });
+    });
+
+    test('override false', () {
+      overrideAnsiOutput(false, () {
+        expect(ansiOutputEnabled, isFalse);
+      });
+    });
+
+    test('forScript variaents ignore `ansiOutputEnabled`', () {
+      const expected =
+          '$_ansiEscapeForScript[34m$sampleInput$_ansiEscapeForScript[0m';
+
+      for (var override in [true, false]) {
+        overrideAnsiOutput(override, () {
+          expect(blue.escapeForScript, '$_ansiEscapeForScript[34m');
+          expect(blue.wrap(sampleInput, forScript: true), expected);
+          expect(wrapWith(sampleInput, [blue], forScript: true), expected);
+        });
+      }
+    });
+  });
+
+  test('foreground and background colors match', () {
+    expect(foregroundColors, hasLength(backgroundColors.length));
+
+    for (var i = 0; i < foregroundColors.length; i++) {
+      final foreground = foregroundColors[i];
+      expect(foreground.type, AnsiCodeType.foreground);
+      expect(foreground.name.toLowerCase(), foreground.name,
+          reason: 'All names should be lower case');
+      final background = backgroundColors[i];
+      expect(background.type, AnsiCodeType.background);
+      expect(background.name.toLowerCase(), background.name,
+          reason: 'All names should be lower case');
+
+      expect(foreground.name, background.name);
+
+      // The last base-10 digit also matches – good to sanity check
+      expect(foreground.code % 10, background.code % 10);
+    }
+  });
+
+  test('all styles are styles', () {
+    for (var style in styles) {
+      expect(style.type, AnsiCodeType.style);
+      expect(style.name.toLowerCase(), style.name,
+          reason: 'All names should be lower case');
+      if (style == styleBold) {
+        expect(style.reset, resetBold);
+      } else {
+        expect(style.reset!.code, equals(style.code + 20));
+      }
+      expect(style.name, equals(style.reset!.name));
+    }
+  });
+
+  for (var forScript in [true, false]) {
+    group(forScript ? 'forScript' : 'escaped', () {
+      final escapeLiteral =
+          forScript ? _ansiEscapeForScript : _ansiEscapeLiteral;
+
+      group('wrap', () {
+        _test('color', () {
+          final expected = '$escapeLiteral[34m$sampleInput$escapeLiteral[0m';
+
+          expect(blue.wrap(sampleInput, forScript: forScript), expected);
+        });
+
+        _test('style', () {
+          final expected = '$escapeLiteral[1m$sampleInput$escapeLiteral[22m';
+
+          expect(styleBold.wrap(sampleInput, forScript: forScript), expected);
+        });
+
+        _test('style', () {
+          final expected = '$escapeLiteral[34m$sampleInput$escapeLiteral[0m';
+
+          expect(blue.wrap(sampleInput, forScript: forScript), expected);
+        });
+
+        test('empty', () {
+          expect(blue.wrap('', forScript: forScript), '');
+        });
+
+        test(null, () {
+          expect(blue.wrap(null, forScript: forScript), isNull);
+        });
+      });
+
+      group('wrapWith', () {
+        _test('foreground', () {
+          final expected = '$escapeLiteral[34m$sampleInput$escapeLiteral[0m';
+
+          expect(wrapWith(sampleInput, [blue], forScript: forScript), expected);
+        });
+
+        _test('background', () {
+          final expected = '$escapeLiteral[44m$sampleInput$escapeLiteral[0m';
+
+          expect(wrapWith(sampleInput, [backgroundBlue], forScript: forScript),
+              expected);
+        });
+
+        _test('style', () {
+          final expected = '$escapeLiteral[1m$sampleInput$escapeLiteral[0m';
+
+          expect(wrapWith(sampleInput, [styleBold], forScript: forScript),
+              expected);
+        });
+
+        _test('2 styles', () {
+          final expected = '$escapeLiteral[1;3m$sampleInput$escapeLiteral[0m';
+
+          expect(
+              wrapWith(sampleInput, [styleBold, styleItalic],
+                  forScript: forScript),
+              expected);
+        });
+
+        _test('2 foregrounds', () {
+          expect(
+              () => wrapWith(sampleInput, [blue, white], forScript: forScript),
+              throwsArgumentError);
+        });
+
+        _test('multi', () {
+          final expected =
+              '$escapeLiteral[1;4;34;107m$sampleInput$escapeLiteral[0m';
+
+          expect(
+              wrapWith(sampleInput,
+                  [blue, backgroundWhite, styleBold, styleUnderlined],
+                  forScript: forScript),
+              expected);
+        });
+
+        test('no codes', () {
+          expect(wrapWith(sampleInput, []), sampleInput);
+        });
+
+        _test('empty', () {
+          expect(
+              wrapWith('', [blue, backgroundWhite, styleBold],
+                  forScript: forScript),
+              '');
+        });
+
+        _test('null', () {
+          expect(
+              wrapWith(null, [blue, backgroundWhite, styleBold],
+                  forScript: forScript),
+              isNull);
+        });
+      });
+    });
+  }
+}
+
+void _test<T>(String name, T Function() body) =>
+    test(name, () => overrideAnsiOutput<T>(true, body));
diff --git a/pkgs/io/test/copy_path_test.dart b/pkgs/io/test/copy_path_test.dart
new file mode 100644
index 0000000..fd1e9ce
--- /dev/null
+++ b/pkgs/io/test/copy_path_test.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2017, 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:io/io.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+void main() {
+  test('should copy a directory (async)', () async {
+    await _create();
+    await copyPath(p.join(d.sandbox, 'parent'), p.join(d.sandbox, 'copy'));
+    await _validate();
+  });
+
+  test('should copy a directory (sync)', () async {
+    await _create();
+    copyPathSync(p.join(d.sandbox, 'parent'), p.join(d.sandbox, 'copy'));
+    await _validate();
+  });
+
+  test('should catch an infinite operation', () async {
+    await _create();
+    expect(
+      copyPath(
+        p.join(d.sandbox, 'parent'),
+        p.join(d.sandbox, 'parent', 'child'),
+      ),
+      throwsArgumentError,
+    );
+  });
+}
+
+d.DirectoryDescriptor _struct() => d.dir('parent', [
+      d.dir('child', [
+        d.file('foo.txt'),
+      ]),
+    ]);
+
+Future<void> _create() => _struct().create();
+Future<void> _validate() => _struct().validate();
diff --git a/pkgs/io/test/permissions_test.dart b/pkgs/io/test/permissions_test.dart
new file mode 100644
index 0000000..478e8df
--- /dev/null
+++ b/pkgs/io/test/permissions_test.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2017, 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:io/io.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('isExecutable', () {
+    const files = 'test/_files';
+    const shellIsExec = '$files/is_executable.sh';
+    const shellNotExec = '$files/is_not_executable.sh';
+
+    group('on shell scripts', () {
+      test('should return true for "is_executable.sh"', () async {
+        expect(await isExecutable(shellIsExec), isTrue);
+      });
+
+      test('should return false for "is_not_executable.sh"', () async {
+        expect(await isExecutable(shellNotExec), isFalse);
+      });
+    }, testOn: '!windows');
+
+    group('on shell scripts [windows]', () {
+      test('should return true for "is_executable.sh"', () async {
+        expect(await isExecutable(shellIsExec, isWindows: true), isTrue);
+      });
+
+      test('should return true for "is_not_executable.sh"', () async {
+        expect(await isExecutable(shellNotExec, isWindows: true), isTrue);
+      });
+    });
+  });
+}
diff --git a/pkgs/io/test/process_manager_test.dart b/pkgs/io/test/process_manager_test.dart
new file mode 100644
index 0000000..9871a77
--- /dev/null
+++ b/pkgs/io/test/process_manager_test.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2017, 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.
+
+// ignore_for_file: close_sinks
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:io/io.dart' hide sharedStdIn;
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+  StreamController<String> fakeStdIn;
+  late ProcessManager processManager;
+  SharedStdIn sharedStdIn;
+  late List<String> stdoutLog;
+  late List<String> stderrLog;
+
+  test('spawn functions should match the type definition of Process.start', () {
+    const isStartProcess = TypeMatcher<StartProcess>();
+    expect(Process.start, isStartProcess);
+    final manager = ProcessManager();
+    expect(manager.spawn, isStartProcess);
+    expect(manager.spawnBackground, isStartProcess);
+    expect(manager.spawnDetached, isStartProcess);
+  });
+
+  group('spawn', () {
+    setUp(() async {
+      fakeStdIn = StreamController<String>(sync: true);
+      sharedStdIn = SharedStdIn(fakeStdIn.stream.map((s) => s.codeUnits));
+      stdoutLog = <String>[];
+      stderrLog = <String>[];
+
+      final stdoutController = StreamController<List<int>>(sync: true);
+      stdoutController.stream.map(utf8.decode).listen(stdoutLog.add);
+      final stdout = IOSink(stdoutController);
+      final stderrController = StreamController<List<int>>(sync: true);
+      stderrController.stream.map(utf8.decode).listen(stderrLog.add);
+      final stderr = IOSink(stderrController);
+
+      processManager = ProcessManager(
+        stdin: sharedStdIn,
+        stdout: stdout,
+        stderr: stderr,
+      );
+    });
+
+    final dart = Platform.executable;
+
+    test('should output Hello from another process [via stdout]', () async {
+      final spawn = await processManager.spawn(
+        dart,
+        [p.join('test', '_files', 'stdout_hello.dart')],
+      );
+      await spawn.exitCode;
+      expect(stdoutLog, ['Hello']);
+    });
+
+    test('should output Hello from another process [via stderr]', () async {
+      final spawn = await processManager.spawn(
+        dart,
+        [p.join('test', '_files', 'stderr_hello.dart')],
+      );
+      await spawn.exitCode;
+      expect(stderrLog, ['Hello']);
+    });
+
+    test('should forward stdin to another process', () async {
+      final spawn = await processManager.spawn(
+        dart,
+        [p.join('test', '_files', 'stdin_echo.dart')],
+      );
+      spawn.stdin.writeln('Ping');
+      await spawn.exitCode;
+      expect(stdoutLog.join(), contains('You said: Ping'));
+    });
+
+    group('should return a Process where', () {
+      test('.stdout is readable', () async {
+        final spawn = await processManager.spawn(
+          dart,
+          [p.join('test', '_files', 'stdout_hello.dart')],
+        );
+        expect(await spawn.stdout.transform(utf8.decoder).first, 'Hello');
+      });
+
+      test('.stderr is readable', () async {
+        final spawn = await processManager.spawn(
+          dart,
+          [p.join('test', '_files', 'stderr_hello.dart')],
+        );
+        expect(await spawn.stderr.transform(utf8.decoder).first, 'Hello');
+      });
+    });
+  });
+}
diff --git a/pkgs/io/test/shared_stdin_test.dart b/pkgs/io/test/shared_stdin_test.dart
new file mode 100644
index 0000000..71629ec
--- /dev/null
+++ b/pkgs/io/test/shared_stdin_test.dart
@@ -0,0 +1,80 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:io/io.dart' hide sharedStdIn;
+import 'package:test/test.dart';
+
+void main() {
+  // ignore: close_sinks
+  late StreamController<String> fakeStdIn;
+  late SharedStdIn sharedStdIn;
+
+  setUp(() {
+    fakeStdIn = StreamController<String>(sync: true);
+    sharedStdIn = SharedStdIn(fakeStdIn.stream.map((s) => s.codeUnits));
+  });
+
+  test('should allow a single subscriber', () async {
+    final logs = <String>[];
+    final sub = sharedStdIn.transform(utf8.decoder).listen(logs.add);
+    fakeStdIn.add('Hello World');
+    await sub.cancel();
+    expect(logs, ['Hello World']);
+  });
+
+  test('should allow multiple subscribers', () async {
+    final logs = <String>[];
+    final asUtf8 = sharedStdIn.transform(utf8.decoder);
+    var sub = asUtf8.listen(logs.add);
+    fakeStdIn.add('Hello World');
+    await sub.cancel();
+    sub = asUtf8.listen(logs.add);
+    fakeStdIn.add('Goodbye World');
+    await sub.cancel();
+    expect(logs, ['Hello World', 'Goodbye World']);
+  });
+
+  test('should throw if a subscriber is still active', () async {
+    final active = sharedStdIn.listen((_) {});
+    expect(() => sharedStdIn.listen((_) {}), throwsStateError);
+    await active.cancel();
+    expect(() => sharedStdIn.listen((_) {}), returnsNormally);
+  });
+
+  test('should return a stream of lines', () async {
+    expect(
+      sharedStdIn.lines(),
+      emitsInOrder(<dynamic>[
+        'I',
+        'Think',
+        'Therefore',
+        'I',
+        'Am',
+      ]),
+    );
+    [
+      'I\nThink\n',
+      'Therefore\n',
+      'I\n',
+      'Am\n',
+    ].forEach(fakeStdIn.add);
+  });
+
+  test('should return the next line', () {
+    expect(sharedStdIn.nextLine(), completion('Hello World'));
+    fakeStdIn.add('Hello World\n');
+  });
+
+  test('should allow listening for new lines multiple times', () async {
+    expect(sharedStdIn.nextLine(), completion('Hello World'));
+    fakeStdIn.add('Hello World\n');
+    await Future<void>.value();
+
+    expect(sharedStdIn.nextLine(), completion('Hello World'));
+    fakeStdIn.add('Hello World\n');
+  });
+}
diff --git a/pkgs/io/test/shell_words_test.dart b/pkgs/io/test/shell_words_test.dart
new file mode 100644
index 0000000..dc4441c
--- /dev/null
+++ b/pkgs/io/test/shell_words_test.dart
@@ -0,0 +1,185 @@
+// Copyright (c) 2017, 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:io/io.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('shellSplit()', () {
+    group('returns an empty list for', () {
+      test('an empty string', () {
+        expect(shellSplit(''), isEmpty);
+      });
+
+      test('spaces', () {
+        expect(shellSplit('    '), isEmpty);
+      });
+
+      test('tabs', () {
+        expect(shellSplit('\t\t\t'), isEmpty);
+      });
+
+      test('newlines', () {
+        expect(shellSplit('\n\n\n'), isEmpty);
+      });
+
+      test('a comment', () {
+        expect(shellSplit('#foo bar baz'), isEmpty);
+      });
+
+      test('a mix', () {
+        expect(shellSplit(' \t\n# foo'), isEmpty);
+      });
+    });
+
+    group('parses unquoted', () {
+      test('a single token', () {
+        expect(shellSplit('foo'), equals(['foo']));
+      });
+
+      test('multiple tokens', () {
+        expect(shellSplit('foo bar baz'), equals(['foo', 'bar', 'baz']));
+      });
+
+      test('tokens separated by tabs', () {
+        expect(shellSplit('foo\tbar\tbaz'), equals(['foo', 'bar', 'baz']));
+      });
+
+      test('tokens separated by newlines', () {
+        expect(shellSplit('foo\nbar\nbaz'), equals(['foo', 'bar', 'baz']));
+      });
+
+      test('a token after whitespace', () {
+        expect(shellSplit(' \t\nfoo'), equals(['foo']));
+      });
+
+      test('a token before whitespace', () {
+        expect(shellSplit('foo \t\n'), equals(['foo']));
+      });
+
+      test('a token with a hash', () {
+        expect(shellSplit('foo#bar'), equals(['foo#bar']));
+      });
+
+      test('a token before a comment', () {
+        expect(shellSplit('foo #bar'), equals(['foo']));
+      });
+
+      test('dynamic shell features', () {
+        expect(
+            shellSplit(r'foo $(bar baz)'), equals(['foo', r'$(bar', 'baz)']));
+        expect(shellSplit('foo `bar baz`'), equals(['foo', '`bar', 'baz`']));
+        expect(shellSplit(r'foo $bar | baz'),
+            equals(['foo', r'$bar', '|', 'baz']));
+      });
+    });
+
+    group('parses a backslash', () {
+      test('before a normal character', () {
+        expect(shellSplit(r'foo\bar'), equals(['foobar']));
+      });
+
+      test('before a dynamic shell feature', () {
+        expect(shellSplit(r'foo\$bar'), equals([r'foo$bar']));
+      });
+
+      test('before a single quote', () {
+        expect(shellSplit(r"foo\'bar"), equals(["foo'bar"]));
+      });
+
+      test('before a double quote', () {
+        expect(shellSplit(r'foo\"bar'), equals(['foo"bar']));
+      });
+
+      test('before a space', () {
+        expect(shellSplit(r'foo\ bar'), equals(['foo bar']));
+      });
+
+      test('at the beginning of a token', () {
+        expect(shellSplit(r'\ foo'), equals([' foo']));
+      });
+
+      test('before whitespace followed by a hash', () {
+        expect(shellSplit(r'\ #foo'), equals([' #foo']));
+      });
+
+      test('before a newline in a token', () {
+        expect(shellSplit('foo\\\nbar'), equals(['foobar']));
+      });
+
+      test('before a newline outside a token', () {
+        expect(shellSplit('foo \\\n bar'), equals(['foo', 'bar']));
+      });
+
+      test('before a backslash', () {
+        expect(shellSplit(r'foo\\bar'), equals([r'foo\bar']));
+      });
+    });
+
+    group('parses single quotes', () {
+      test('that are empty', () {
+        expect(shellSplit("''"), equals(['']));
+      });
+
+      test('that contain normal characters', () {
+        expect(shellSplit("'foo'"), equals(['foo']));
+      });
+
+      test('that contain active characters', () {
+        expect(shellSplit("'\" \\#'"), equals([r'" \#']));
+      });
+
+      test('before a hash', () {
+        expect(shellSplit("''#foo"), equals([r'#foo']));
+      });
+
+      test('inside a token', () {
+        expect(shellSplit("foo'bar baz'qux"), equals([r'foobar bazqux']));
+      });
+
+      test('without a closing quote', () {
+        expect(() => shellSplit("'foo bar"), throwsFormatException);
+      });
+    });
+
+    group('parses double quotes', () {
+      test('that are empty', () {
+        expect(shellSplit('""'), equals(['']));
+      });
+
+      test('that contain normal characters', () {
+        expect(shellSplit('"foo"'), equals(['foo']));
+      });
+
+      test('that contain otherwise-active characters', () {
+        expect(shellSplit('"\' #"'), equals(["' #"]));
+      });
+
+      test('that contain escaped characters', () {
+        expect(shellSplit(r'"\$\`\"\\"'), equals([r'$`"\']));
+      });
+
+      test('that contain an escaped newline', () {
+        expect(shellSplit('"\\\n"'), equals(['']));
+      });
+
+      test("that contain a backslash that's not an escape", () {
+        expect(shellSplit(r'"f\oo"'), equals([r'f\oo']));
+      });
+
+      test('before a hash', () {
+        expect(shellSplit('""#foo'), equals([r'#foo']));
+      });
+
+      test('inside a token', () {
+        expect(shellSplit('foo"bar baz"qux'), equals([r'foobar bazqux']));
+      });
+
+      test('without a closing quote', () {
+        expect(() => shellSplit('"foo bar'), throwsFormatException);
+        expect(() => shellSplit(r'"foo bar\'), throwsFormatException);
+      });
+    });
+  });
+}
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)
+          ])));
+    });
+  });
+}
diff --git a/pkgs/pubspec_parse/.gitignore b/pkgs/pubspec_parse/.gitignore
new file mode 100644
index 0000000..ec8eae3
--- /dev/null
+++ b/pkgs/pubspec_parse/.gitignore
@@ -0,0 +1,4 @@
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+pubspec.lock
diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md
new file mode 100644
index 0000000..5aeb498
--- /dev/null
+++ b/pkgs/pubspec_parse/CHANGELOG.md
@@ -0,0 +1,110 @@
+## 1.5.0
+
+- Added fields to `Pubspec`: `executables`, `resolution`, `workspace`.
+- Require Dart 3.6
+- Update dependencies.
+
+## 1.4.0
+
+- Require Dart 3.2
+- Seal the `Dependency` class.
+- Set `Pubspec.environment` to non-nullable.
+- Remove deprecated package_api_docs rule
+- Move to `dart-lang/tools` monorepo.
+
+## 1.3.0
+
+- Require Dart 3.0
+- Added support for `ignored_advisories` field.
+- Added structural equality for `Dependency` subclasses and `HostedDetails`.
+
+## 1.2.3
+
+- Added topics to `pubspec.yaml`.
+
+## 1.2.2
+
+- Require Dart SDK >= 2.18.0
+- Required `json_annotation: ^4.8.0`
+- Added support for `topics` field.
+
+## 1.2.1
+
+- Added support for `funding` field.
+
+## 1.2.0
+
+- Added support for `screenshots` field.
+- Update `HostedDetails` to reflect how `hosted` dependencies are parsed in
+  Dart 2.15:
+   - Add `HostedDetails.declaredName` as the (optional) `name` property in a
+     `hosted` block.
+   - `HostedDetails.name` now falls back to the name of the dependency if no
+      name is declared in the block.
+- Require Dart SDK >= 2.14.0
+
+## 1.1.0
+
+- Export `HostedDetails` publicly.
+
+## 1.0.0
+
+- Migrate to null-safety.
+- Pubspec: `author` and `authors` are both now deprecated.
+  See https://dart.dev/tools/pub/pubspec#authorauthors
+
+## 0.1.8
+
+- Allow the latest `package:pub_semver`.
+
+## 0.1.7
+
+- Allow `package:yaml` `v3.x`.
+
+## 0.1.6
+
+- Update SDK requirement to `>=2.7.0 <3.0.0`.
+- Allow `package:json_annotation` `v4.x`.
+
+## 0.1.5
+
+- Update SDK requirement to `>=2.2.0 <3.0.0`.
+- Support the latest `package:json_annotation`.
+
+## 0.1.4
+
+- Added `lenient` named argument to `Pubspec.fromJson` to ignore format and type errors.
+
+## 0.1.3
+
+- Added support for `flutter`, `issue_tracker`, `publish_to`, and `repository`
+  fields.
+
+## 0.1.2+3
+
+- Support the latest version of `package:json_annotation`.
+
+## 0.1.2+2
+
+- Support `package:json_annotation` v1.
+
+## 0.1.2+1
+
+- Support the Dart 2 stable release.
+
+## 0.1.2
+
+- Allow superfluous `version` keys with `git` and `path` dependencies.
+- Improve errors when unsupported keys are provided in dependencies.
+- Provide better errors with invalid `sdk` dependency values.
+- Support "scp-like syntax" for Git SSH URIs in the form
+  `[user@]host.xz:path/to/repo.git/`.
+
+## 0.1.1
+
+- Fixed name collision with error type in latest `package:json_annotation`.
+- Improved parsing of hosted dependencies and environment constraints.
+
+## 0.1.0
+
+- Initial release.
diff --git a/pkgs/pubspec_parse/LICENSE b/pkgs/pubspec_parse/LICENSE
new file mode 100644
index 0000000..4d1ad40
--- /dev/null
+++ b/pkgs/pubspec_parse/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2018, 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/pubspec_parse/README.md b/pkgs/pubspec_parse/README.md
new file mode 100644
index 0000000..1d04aa4
--- /dev/null
+++ b/pkgs/pubspec_parse/README.md
@@ -0,0 +1,12 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/pubspec_parse.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/pubspec_parse.yaml)
+[![pub package](https://img.shields.io/pub/v/pubspec_parse.svg)](https://pub.dev/packages/pubspec_parse)
+[![package publisher](https://img.shields.io/pub/publisher/pubspec_parse.svg)](https://pub.dev/packages/pubspec_parse/publisher)
+
+## What's this?
+
+Supports parsing `pubspec.yaml` files with robust error reporting and support
+for most of the documented features.
+
+## More information
+
+Read more about the [pubspec format](https://dart.dev/tools/pub/pubspec).
diff --git a/pkgs/pubspec_parse/analysis_options.yaml b/pkgs/pubspec_parse/analysis_options.yaml
new file mode 100644
index 0000000..93eeebf
--- /dev/null
+++ b/pkgs/pubspec_parse/analysis_options.yaml
@@ -0,0 +1,30 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+    strict-inference: 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
+    - prefer_final_locals
+    - require_trailing_commas
+    - unnecessary_await_in_return
+    - use_string_buffers
diff --git a/pkgs/pubspec_parse/build.yaml b/pkgs/pubspec_parse/build.yaml
new file mode 100644
index 0000000..2003bc2
--- /dev/null
+++ b/pkgs/pubspec_parse/build.yaml
@@ -0,0 +1,25 @@
+# Read about `build.yaml` at https://pub.dev/packages/build_config
+# To update generated code, run `pub run build_runner build`
+targets:
+  $default:
+    builders:
+      json_serializable:
+        generate_for:
+          - lib/src/pubspec.dart
+          - lib/src/dependency.dart
+        options:
+          any_map: true
+          checked: true
+          create_to_json: false
+          field_rename: snake
+
+      # The end-user of a builder which applies "source_gen|combining_builder"
+      # may configure the builder to ignore specific lints for their project
+      source_gen|combining_builder:
+        options:
+          ignore_for_file:
+          - deprecated_member_use_from_same_package
+          - lines_longer_than_80_chars
+          - require_trailing_commas
+          # https://github.com/google/json_serializable.dart/issues/945
+          - unnecessary_cast
diff --git a/pkgs/pubspec_parse/dart_test.yaml b/pkgs/pubspec_parse/dart_test.yaml
new file mode 100644
index 0000000..1d7ac69
--- /dev/null
+++ b/pkgs/pubspec_parse/dart_test.yaml
@@ -0,0 +1,3 @@
+tags:
+  presubmit-only:
+    skip: "Should only be run during presubmit"
diff --git a/pkgs/pubspec_parse/lib/pubspec_parse.dart b/pkgs/pubspec_parse/lib/pubspec_parse.dart
new file mode 100644
index 0000000..b5c12e4
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/pubspec_parse.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2018, 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/dependency.dart'
+    show
+        Dependency,
+        GitDependency,
+        HostedDependency,
+        HostedDetails,
+        PathDependency,
+        SdkDependency;
+export 'src/pubspec.dart' show Pubspec;
+export 'src/screenshot.dart' show Screenshot;
diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart
new file mode 100644
index 0000000..24c65ea
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/dependency.dart
@@ -0,0 +1,277 @@
+// Copyright (c) 2018, 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 'package:json_annotation/json_annotation.dart';
+import 'package:pub_semver/pub_semver.dart';
+import 'package:yaml/yaml.dart';
+
+part 'dependency.g.dart';
+
+Map<String, Dependency> parseDeps(Map? source) =>
+    source?.map((k, v) {
+      final key = k as String;
+      Dependency? value;
+      try {
+        value = _fromJson(v, k);
+      } on CheckedFromJsonException catch (e) {
+        if (e.map is! YamlMap) {
+          // This is likely a "synthetic" map created from a String value
+          // Use `source` to throw this exception with an actual YamlMap and
+          // extract the associated error information.
+          throw CheckedFromJsonException(source, key, e.className!, e.message);
+        }
+        rethrow;
+      }
+
+      if (value == null) {
+        throw CheckedFromJsonException(
+          source,
+          key,
+          'Pubspec',
+          'Not a valid dependency value.',
+        );
+      }
+      return MapEntry(key, value);
+    }) ??
+    {};
+
+const _sourceKeys = ['sdk', 'git', 'path', 'hosted'];
+
+/// Returns `null` if the data could not be parsed.
+Dependency? _fromJson(Object? data, String name) {
+  if (data is String || data == null) {
+    return _$HostedDependencyFromJson({'version': data});
+  }
+
+  if (data is Map) {
+    final matchedKeys =
+        data.keys.cast<String>().where((key) => key != 'version').toList();
+
+    if (data.isEmpty || (matchedKeys.isEmpty && data.containsKey('version'))) {
+      return _$HostedDependencyFromJson(data);
+    } else {
+      final firstUnrecognizedKey =
+          matchedKeys.firstWhereOrNull((k) => !_sourceKeys.contains(k));
+
+      return $checkedNew<Dependency>('Dependency', data, () {
+        if (firstUnrecognizedKey != null) {
+          throw UnrecognizedKeysException(
+            [firstUnrecognizedKey],
+            data,
+            _sourceKeys,
+          );
+        }
+        if (matchedKeys.length > 1) {
+          throw CheckedFromJsonException(
+            data,
+            matchedKeys[1],
+            'Dependency',
+            'A dependency may only have one source.',
+          );
+        }
+
+        final key = matchedKeys.single;
+
+        return switch (key) {
+          'git' => GitDependency.fromData(data[key]),
+          'path' => PathDependency.fromData(data[key]),
+          'sdk' => _$SdkDependencyFromJson(data),
+          'hosted' => _$HostedDependencyFromJson(data)
+            ..hosted?._nameOfPackage = name,
+          _ => throw StateError('There is a bug in pubspec_parse.'),
+        };
+      });
+    }
+  }
+
+  // Not a String or a Map – return null so parent logic can throw proper error
+  return null;
+}
+
+sealed class Dependency {}
+
+@JsonSerializable()
+class SdkDependency extends Dependency {
+  final String sdk;
+  @JsonKey(fromJson: _constraintFromString)
+  final VersionConstraint version;
+
+  SdkDependency(this.sdk, {VersionConstraint? version})
+      : version = version ?? VersionConstraint.any;
+
+  @override
+  bool operator ==(Object other) =>
+      other is SdkDependency && other.sdk == sdk && other.version == version;
+
+  @override
+  int get hashCode => Object.hash(sdk, version);
+
+  @override
+  String toString() => 'SdkDependency: $sdk';
+}
+
+@JsonSerializable()
+class GitDependency extends Dependency {
+  @JsonKey(fromJson: parseGitUri)
+  final Uri url;
+  final String? ref;
+  final String? path;
+
+  GitDependency(this.url, {this.ref, this.path});
+
+  factory GitDependency.fromData(Object? data) {
+    if (data is String) {
+      data = {'url': data};
+    }
+
+    if (data is Map) {
+      return _$GitDependencyFromJson(data);
+    }
+
+    throw ArgumentError.value(data, 'git', 'Must be a String or a Map.');
+  }
+
+  @override
+  bool operator ==(Object other) =>
+      other is GitDependency &&
+      other.url == url &&
+      other.ref == ref &&
+      other.path == path;
+
+  @override
+  int get hashCode => Object.hash(url, ref, path);
+
+  @override
+  String toString() => 'GitDependency: url@$url';
+}
+
+Uri? parseGitUriOrNull(String? value) =>
+    value == null ? null : parseGitUri(value);
+
+Uri parseGitUri(String value) => _tryParseScpUri(value) ?? Uri.parse(value);
+
+/// Supports URIs like `[user@]host.xz:path/to/repo.git/`
+/// See https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a
+Uri? _tryParseScpUri(String value) {
+  final colonIndex = value.indexOf(':');
+
+  if (colonIndex < 0) {
+    return null;
+  } else if (colonIndex == value.indexOf('://')) {
+    // If the first colon is part of a scheme, it's not an scp-like URI
+    return null;
+  }
+  final slashIndex = value.indexOf('/');
+
+  if (slashIndex >= 0 && slashIndex < colonIndex) {
+    // Per docs: This syntax is only recognized if there are no slashes before
+    // the first colon. This helps differentiate a local path that contains a
+    // colon. For example the local path foo:bar could be specified as an
+    // absolute path or ./foo:bar to avoid being misinterpreted as an ssh url.
+    return null;
+  }
+
+  final atIndex = value.indexOf('@');
+  if (colonIndex > atIndex) {
+    final user = atIndex >= 0 ? value.substring(0, atIndex) : null;
+    final host = value.substring(atIndex + 1, colonIndex);
+    final path = value.substring(colonIndex + 1);
+    return Uri(scheme: 'ssh', userInfo: user, host: host, path: path);
+  }
+  return null;
+}
+
+class PathDependency extends Dependency {
+  final String path;
+
+  PathDependency(this.path);
+
+  factory PathDependency.fromData(Object? data) {
+    if (data is String) {
+      return PathDependency(data);
+    }
+    throw ArgumentError.value(data, 'path', 'Must be a String.');
+  }
+
+  @override
+  bool operator ==(Object other) =>
+      other is PathDependency && other.path == path;
+
+  @override
+  int get hashCode => path.hashCode;
+
+  @override
+  String toString() => 'PathDependency: path@$path';
+}
+
+@JsonSerializable(disallowUnrecognizedKeys: true)
+class HostedDependency extends Dependency {
+  @JsonKey(fromJson: _constraintFromString)
+  final VersionConstraint version;
+
+  @JsonKey(disallowNullValue: true)
+  final HostedDetails? hosted;
+
+  HostedDependency({VersionConstraint? version, this.hosted})
+      : version = version ?? VersionConstraint.any;
+
+  @override
+  bool operator ==(Object other) =>
+      other is HostedDependency &&
+      other.version == version &&
+      other.hosted == hosted;
+
+  @override
+  int get hashCode => Object.hash(version, hosted);
+
+  @override
+  String toString() => 'HostedDependency: $version';
+}
+
+@JsonSerializable(disallowUnrecognizedKeys: true)
+class HostedDetails {
+  /// The name of the target dependency as declared in a `hosted` block.
+  ///
+  /// This may be null if no explicit name is present, for instance because the
+  /// hosted dependency was declared as a string (`hosted: pub.example.org`).
+  @JsonKey(name: 'name')
+  final String? declaredName;
+
+  @JsonKey(fromJson: parseGitUriOrNull, disallowNullValue: true)
+  final Uri? url;
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  String? _nameOfPackage;
+
+  /// The name of this package on the package repository.
+  ///
+  /// If this hosted block has a [declaredName], that one will be used.
+  /// Otherwise, the name will be inferred from the surrounding package name.
+  String get name => declaredName ?? _nameOfPackage!;
+
+  HostedDetails(this.declaredName, this.url);
+
+  factory HostedDetails.fromJson(Object data) {
+    if (data is String) {
+      data = {'url': data};
+    }
+
+    if (data is Map) {
+      return _$HostedDetailsFromJson(data);
+    }
+
+    throw ArgumentError.value(data, 'hosted', 'Must be a Map or String.');
+  }
+
+  @override
+  bool operator ==(Object other) =>
+      other is HostedDetails && other.name == name && other.url == url;
+
+  @override
+  int get hashCode => Object.hash(name, url);
+}
+
+VersionConstraint _constraintFromString(String? input) =>
+    input == null ? VersionConstraint.any : VersionConstraint.parse(input);
diff --git a/pkgs/pubspec_parse/lib/src/dependency.g.dart b/pkgs/pubspec_parse/lib/src/dependency.g.dart
new file mode 100644
index 0000000..1a504f1
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/dependency.g.dart
@@ -0,0 +1,72 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+// ignore_for_file: deprecated_member_use_from_same_package, lines_longer_than_80_chars, require_trailing_commas, unnecessary_cast
+
+part of 'dependency.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+SdkDependency _$SdkDependencyFromJson(Map json) => $checkedCreate(
+      'SdkDependency',
+      json,
+      ($checkedConvert) {
+        final val = SdkDependency(
+          $checkedConvert('sdk', (v) => v as String),
+          version: $checkedConvert(
+              'version', (v) => _constraintFromString(v as String?)),
+        );
+        return val;
+      },
+    );
+
+GitDependency _$GitDependencyFromJson(Map json) => $checkedCreate(
+      'GitDependency',
+      json,
+      ($checkedConvert) {
+        final val = GitDependency(
+          $checkedConvert('url', (v) => parseGitUri(v as String)),
+          ref: $checkedConvert('ref', (v) => v as String?),
+          path: $checkedConvert('path', (v) => v as String?),
+        );
+        return val;
+      },
+    );
+
+HostedDependency _$HostedDependencyFromJson(Map json) => $checkedCreate(
+      'HostedDependency',
+      json,
+      ($checkedConvert) {
+        $checkKeys(
+          json,
+          allowedKeys: const ['version', 'hosted'],
+          disallowNullValues: const ['hosted'],
+        );
+        final val = HostedDependency(
+          version: $checkedConvert(
+              'version', (v) => _constraintFromString(v as String?)),
+          hosted: $checkedConvert('hosted',
+              (v) => v == null ? null : HostedDetails.fromJson(v as Object)),
+        );
+        return val;
+      },
+    );
+
+HostedDetails _$HostedDetailsFromJson(Map json) => $checkedCreate(
+      'HostedDetails',
+      json,
+      ($checkedConvert) {
+        $checkKeys(
+          json,
+          allowedKeys: const ['name', 'url'],
+          disallowNullValues: const ['url'],
+        );
+        final val = HostedDetails(
+          $checkedConvert('name', (v) => v as String?),
+          $checkedConvert('url', (v) => parseGitUriOrNull(v as String?)),
+        );
+        return val;
+      },
+      fieldKeyMap: const {'declaredName': 'name'},
+    );
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart
new file mode 100644
index 0000000..eb77908
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/pubspec.dart
@@ -0,0 +1,258 @@
+// Copyright (c) 2018, 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:checked_yaml/checked_yaml.dart';
+import 'package:json_annotation/json_annotation.dart';
+import 'package:pub_semver/pub_semver.dart';
+
+import 'dependency.dart';
+import 'screenshot.dart';
+
+part 'pubspec.g.dart';
+
+@JsonSerializable()
+class Pubspec {
+  // TODO: executables
+
+  final String name;
+
+  @JsonKey(fromJson: _versionFromString)
+  final Version? version;
+
+  final String? description;
+
+  /// This should be a URL pointing to the website for the package.
+  final String? homepage;
+
+  /// Specifies where to publish this package.
+  ///
+  /// Accepted values: `null`, `'none'` or an `http` or `https` URL.
+  ///
+  /// [More information](https://dart.dev/tools/pub/pubspec#publish_to).
+  final String? publishTo;
+
+  /// Optional field to specify the source code repository of the package.
+  /// Useful when a package has both a home page and a repository.
+  final Uri? repository;
+
+  /// Optional field to a web page where developers can report new issues or
+  /// view existing ones.
+  final Uri? issueTracker;
+
+  /// Optional field to list the URLs where the package authors accept
+  /// support or funding.
+  final List<Uri>? funding;
+
+  /// Optional field to list the topics that this packages belongs to.
+  final List<String>? topics;
+
+  /// Optional field to list advisories to be ignored by the client.
+  final List<String>? ignoredAdvisories;
+
+  /// Optional field for specifying included screenshot files.
+  @JsonKey(fromJson: parseScreenshots)
+  final List<Screenshot>? screenshots;
+
+  /// If there is exactly 1 value in [authors], returns it.
+  ///
+  /// If there are 0 or more than 1, returns `null`.
+  @Deprecated(
+    'See https://dart.dev/tools/pub/pubspec#authorauthors',
+  )
+  String? get author {
+    if (authors.length == 1) {
+      return authors.single;
+    }
+    return null;
+  }
+
+  @Deprecated(
+    'See https://dart.dev/tools/pub/pubspec#authorauthors',
+  )
+  final List<String> authors;
+  final String? documentation;
+
+  @JsonKey(fromJson: _environmentMap)
+  final Map<String, VersionConstraint?> environment;
+
+  @JsonKey(fromJson: parseDeps)
+  final Map<String, Dependency> dependencies;
+
+  @JsonKey(fromJson: parseDeps)
+  final Map<String, Dependency> devDependencies;
+
+  @JsonKey(fromJson: parseDeps)
+  final Map<String, Dependency> dependencyOverrides;
+
+  /// Optional configuration specific to [Flutter](https://flutter.io/)
+  /// packages.
+  ///
+  /// May include
+  /// [assets](https://flutter.io/docs/development/ui/assets-and-images)
+  /// and other settings.
+  final Map<String, dynamic>? flutter;
+
+  /// Optional field to specify executables
+  @JsonKey(fromJson: _executablesMap)
+  final Map<String, String?> executables;
+
+  /// If this package is a Pub Workspace, this field lists the sub-packages.
+  final List<String>? workspace;
+
+  /// Specifies how to resolve dependencies with the surrounding Pub Workspace.
+  final String? resolution;
+
+  /// If [author] and [authors] are both provided, their values are combined
+  /// with duplicates eliminated.
+  Pubspec(
+    this.name, {
+    this.version,
+    this.publishTo,
+    @Deprecated(
+      'See https://dart.dev/tools/pub/pubspec#authorauthors',
+    )
+    String? author,
+    @Deprecated(
+      'See https://dart.dev/tools/pub/pubspec#authorauthors',
+    )
+    List<String>? authors,
+    Map<String, VersionConstraint?>? environment,
+    this.homepage,
+    this.repository,
+    this.issueTracker,
+    this.funding,
+    this.topics,
+    this.ignoredAdvisories,
+    this.screenshots,
+    this.documentation,
+    this.description,
+    this.workspace,
+    this.resolution,
+    Map<String, Dependency>? dependencies,
+    Map<String, Dependency>? devDependencies,
+    Map<String, Dependency>? dependencyOverrides,
+    this.flutter,
+    Map<String, String?>? executables,
+  })  :
+        // ignore: deprecated_member_use_from_same_package
+        authors = _normalizeAuthors(author, authors),
+        environment = environment ?? const {},
+        dependencies = dependencies ?? const {},
+        devDependencies = devDependencies ?? const {},
+        executables = executables ?? const {},
+        dependencyOverrides = dependencyOverrides ?? const {} {
+    if (name.isEmpty) {
+      throw ArgumentError.value(name, 'name', '"name" cannot be empty.');
+    }
+
+    if (publishTo != null && publishTo != 'none') {
+      try {
+        final targetUri = Uri.parse(publishTo!);
+        if (!(targetUri.isScheme('http') || targetUri.isScheme('https'))) {
+          throw const FormatException('Must be an http or https URL.');
+        }
+      } on FormatException catch (e) {
+        throw ArgumentError.value(publishTo, 'publishTo', e.message);
+      }
+    }
+  }
+
+  factory Pubspec.fromJson(Map json, {bool lenient = false}) {
+    if (lenient) {
+      while (json.isNotEmpty) {
+        // Attempting to remove top-level properties that cause parsing errors.
+        try {
+          return _$PubspecFromJson(json);
+        } on CheckedFromJsonException catch (e) {
+          if (e.map == json && json.containsKey(e.key)) {
+            json = Map.from(json)..remove(e.key);
+            continue;
+          }
+          rethrow;
+        }
+      }
+    }
+
+    return _$PubspecFromJson(json);
+  }
+
+  /// Parses source [yaml] into [Pubspec].
+  ///
+  /// When [lenient] is set, top-level property-parsing or type cast errors are
+  /// ignored and `null` values are returned.
+  factory Pubspec.parse(String yaml, {Uri? sourceUrl, bool lenient = false}) =>
+      checkedYamlDecode(
+        yaml,
+        (map) => Pubspec.fromJson(map!, lenient: lenient),
+        sourceUrl: sourceUrl,
+      );
+
+  static List<String> _normalizeAuthors(String? author, List<String>? authors) {
+    final value = <String>{
+      if (author != null) author,
+      ...?authors,
+    };
+    return value.toList();
+  }
+}
+
+Version? _versionFromString(String? input) =>
+    input == null ? null : Version.parse(input);
+
+Map<String, VersionConstraint?> _environmentMap(Map? source) =>
+    source?.map((k, value) {
+      final key = k as String;
+      if (key == 'dart') {
+        // github.com/dart-lang/pub/blob/d84173eeb03c3/lib/src/pubspec.dart#L342
+        // 'dart' is not allowed as a key!
+        throw CheckedFromJsonException(
+          source,
+          'dart',
+          'VersionConstraint',
+          'Use "sdk" to for Dart SDK constraints.',
+          badKey: true,
+        );
+      }
+
+      VersionConstraint? constraint;
+      if (value == null) {
+        constraint = null;
+      } else if (value is String) {
+        try {
+          constraint = VersionConstraint.parse(value);
+        } on FormatException catch (e) {
+          throw CheckedFromJsonException(source, key, 'Pubspec', e.message);
+        }
+
+        return MapEntry(key, constraint);
+      } else {
+        throw CheckedFromJsonException(
+          source,
+          key,
+          'VersionConstraint',
+          '`$value` is not a String.',
+        );
+      }
+
+      return MapEntry(key, constraint);
+    }) ??
+    {};
+
+Map<String, String?> _executablesMap(Map? source) =>
+    source?.map((k, value) {
+      final key = k as String;
+      if (value == null) {
+        return MapEntry(key, null);
+      } else if (value is String) {
+        return MapEntry(key, value);
+      } else {
+        throw CheckedFromJsonException(
+          source,
+          key,
+          'String',
+          '`$value` is not a String.',
+        );
+      }
+    }) ??
+    {};
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
new file mode 100644
index 0000000..58e015a
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
@@ -0,0 +1,69 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+// ignore_for_file: deprecated_member_use_from_same_package, lines_longer_than_80_chars, require_trailing_commas, unnecessary_cast
+
+part of 'pubspec.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+Pubspec _$PubspecFromJson(Map json) => $checkedCreate(
+      'Pubspec',
+      json,
+      ($checkedConvert) {
+        final val = Pubspec(
+          $checkedConvert('name', (v) => v as String),
+          version: $checkedConvert(
+              'version', (v) => _versionFromString(v as String?)),
+          publishTo: $checkedConvert('publish_to', (v) => v as String?),
+          author: $checkedConvert('author', (v) => v as String?),
+          authors: $checkedConvert('authors',
+              (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+          environment:
+              $checkedConvert('environment', (v) => _environmentMap(v as Map?)),
+          homepage: $checkedConvert('homepage', (v) => v as String?),
+          repository: $checkedConvert(
+              'repository', (v) => v == null ? null : Uri.parse(v as String)),
+          issueTracker: $checkedConvert('issue_tracker',
+              (v) => v == null ? null : Uri.parse(v as String)),
+          funding: $checkedConvert(
+              'funding',
+              (v) => (v as List<dynamic>?)
+                  ?.map((e) => Uri.parse(e as String))
+                  .toList()),
+          topics: $checkedConvert('topics',
+              (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+          ignoredAdvisories: $checkedConvert('ignored_advisories',
+              (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+          screenshots: $checkedConvert(
+              'screenshots', (v) => parseScreenshots(v as List?)),
+          documentation: $checkedConvert('documentation', (v) => v as String?),
+          description: $checkedConvert('description', (v) => v as String?),
+          workspace: $checkedConvert('workspace',
+              (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+          resolution: $checkedConvert('resolution', (v) => v as String?),
+          dependencies:
+              $checkedConvert('dependencies', (v) => parseDeps(v as Map?)),
+          devDependencies:
+              $checkedConvert('dev_dependencies', (v) => parseDeps(v as Map?)),
+          dependencyOverrides: $checkedConvert(
+              'dependency_overrides', (v) => parseDeps(v as Map?)),
+          flutter: $checkedConvert(
+              'flutter',
+              (v) => (v as Map?)?.map(
+                    (k, e) => MapEntry(k as String, e),
+                  )),
+          executables:
+              $checkedConvert('executables', (v) => _executablesMap(v as Map?)),
+        );
+        return val;
+      },
+      fieldKeyMap: const {
+        'publishTo': 'publish_to',
+        'issueTracker': 'issue_tracker',
+        'ignoredAdvisories': 'ignored_advisories',
+        'devDependencies': 'dev_dependencies',
+        'dependencyOverrides': 'dependency_overrides'
+      },
+    );
diff --git a/pkgs/pubspec_parse/lib/src/screenshot.dart b/pkgs/pubspec_parse/lib/src/screenshot.dart
new file mode 100644
index 0000000..f5f0be2
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/screenshot.dart
@@ -0,0 +1,65 @@
+// 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 'package:json_annotation/json_annotation.dart';
+
+@JsonSerializable()
+class Screenshot {
+  final String description;
+  final String path;
+
+  Screenshot(this.description, this.path);
+}
+
+List<Screenshot> parseScreenshots(List? input) {
+  final res = <Screenshot>[];
+  if (input == null) {
+    return res;
+  }
+
+  for (final e in input) {
+    if (e is! Map) continue;
+
+    final description = e['description'];
+    if (description == null) {
+      throw CheckedFromJsonException(
+        e,
+        'description',
+        'Screenshot',
+        'Missing required key `description`',
+      );
+    }
+
+    if (description is! String) {
+      throw CheckedFromJsonException(
+        e,
+        'description',
+        'Screenshot',
+        '`$description` is not a String',
+      );
+    }
+
+    final path = e['path'];
+    if (path == null) {
+      throw CheckedFromJsonException(
+        e,
+        'path',
+        'Screenshot',
+        'Missing required key `path`',
+      );
+    }
+
+    if (path is! String) {
+      throw CheckedFromJsonException(
+        e,
+        'path',
+        'Screenshot',
+        '`$path` is not a String',
+      );
+    }
+
+    res.add(Screenshot(description, path));
+  }
+  return res;
+}
diff --git a/pkgs/pubspec_parse/pubspec.yaml b/pkgs/pubspec_parse/pubspec.yaml
new file mode 100644
index 0000000..73a1117
--- /dev/null
+++ b/pkgs/pubspec_parse/pubspec.yaml
@@ -0,0 +1,32 @@
+name: pubspec_parse
+version: 1.5.0
+description: >-
+  Simple package for parsing pubspec.yaml files with a type-safe API and rich
+  error reporting.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/pubspec_parse
+
+topics:
+- dart-pub
+
+environment:
+  sdk: ^3.6.0
+
+dependencies:
+  checked_yaml: ^2.0.1
+  collection: ^1.19.0
+  json_annotation: ^4.9.0
+  pub_semver: ^2.1.4
+  yaml: ^3.0.0
+
+dev_dependencies:
+  build_runner: ^2.4.6
+  build_verify: ^3.0.0
+  dart_flutter_team_lints: ^3.0.0
+  json_serializable: ^6.9.1
+  path: ^1.9.0
+  # Needed because we are configuring `combining_builder`
+  source_gen: ^2.0.0
+  stack_trace: ^1.10.0
+  test: ^1.24.4
+  test_descriptor: ^2.0.0
+  test_process: ^2.0.0
diff --git a/pkgs/pubspec_parse/test/dependency_test.dart b/pkgs/pubspec_parse/test/dependency_test.dart
new file mode 100644
index 0000000..f1e4f57
--- /dev/null
+++ b/pkgs/pubspec_parse/test/dependency_test.dart
@@ -0,0 +1,446 @@
+// Copyright (c) 2018, 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:pub_semver/pub_semver.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('hosted', _hostedDependency);
+  group('git', _gitDependency);
+  group('sdk', _sdkDependency);
+  group('path', _pathDependency);
+
+  group('errors', () {
+    test('List', () {
+      _expectThrows(
+        [],
+        r'''
+line 4, column 10: Unsupported value for "dep". Not a valid dependency value.
+  ╷
+4 │   "dep": []
+  │          ^^
+  ╵''',
+      );
+    });
+
+    test('int', () {
+      _expectThrows(
+        42,
+        r'''
+line 4, column 10: Unsupported value for "dep". Not a valid dependency value.
+  ╷
+4 │     "dep": 42
+  │ ┌──────────^
+5 │ │  }
+  │ └─^
+  ╵''',
+      );
+    });
+
+    test('map with too many keys', () {
+      _expectThrows(
+        {'path': 'a', 'git': 'b'},
+        r'''
+line 6, column 11: Unsupported value for "git". A dependency may only have one source.
+  ╷
+6 │    "git": "b"
+  │           ^^^
+  ╵''',
+      );
+    });
+
+    test('map with unsupported keys', () {
+      _expectThrows(
+        {'bob': 'a', 'jones': 'b'},
+        r'''
+line 5, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
+  ╷
+5 │    "bob": "a",
+  │    ^^^^^
+  ╵''',
+      );
+    });
+  });
+}
+
+void _hostedDependency() {
+  test('null', () async {
+    final dep = await _dependency<HostedDependency>(null);
+    expect(dep.version.toString(), 'any');
+    expect(dep.hosted, isNull);
+    expect(dep.toString(), 'HostedDependency: any');
+  });
+
+  test('empty map', () async {
+    final dep = await _dependency<HostedDependency>({});
+    expect(dep.hosted, isNull);
+    expect(dep.toString(), 'HostedDependency: any');
+  });
+
+  test('string version', () async {
+    final dep = await _dependency<HostedDependency>('^1.0.0');
+    expect(dep.version.toString(), '^1.0.0');
+    expect(dep.hosted, isNull);
+    expect(dep.toString(), 'HostedDependency: ^1.0.0');
+  });
+
+  test('bad string version', () {
+    _expectThrows(
+      'not a version',
+      r'''
+line 4, column 10: Unsupported value for "dep". Could not parse version "not a version". Unknown text at "not a version".
+  ╷
+4 │   "dep": "not a version"
+  │          ^^^^^^^^^^^^^^^
+  ╵''',
+    );
+  });
+
+  test('map w/ just version', () async {
+    final dep = await _dependency<HostedDependency>({'version': '^1.0.0'});
+    expect(dep.version.toString(), '^1.0.0');
+    expect(dep.hosted, isNull);
+    expect(dep.toString(), 'HostedDependency: ^1.0.0');
+  });
+
+  test('map w/ version and hosted as Map', () async {
+    final dep = await _dependency<HostedDependency>({
+      'version': '^1.0.0',
+      'hosted': {'name': 'hosted_name', 'url': 'https://hosted_url'},
+    });
+    expect(dep.version.toString(), '^1.0.0');
+    expect(dep.hosted!.name, 'hosted_name');
+    expect(dep.hosted!.url.toString(), 'https://hosted_url');
+    expect(dep.toString(), 'HostedDependency: ^1.0.0');
+  });
+
+  test('map /w hosted as a map without name', () async {
+    final dep = await _dependency<HostedDependency>(
+      {
+        'version': '^1.0.0',
+        'hosted': {'url': 'https://hosted_url'},
+      },
+      skipTryPub: true, // todo: Unskip once pub supports this syntax
+    );
+    expect(dep.version.toString(), '^1.0.0');
+    expect(dep.hosted!.declaredName, isNull);
+    expect(dep.hosted!.name, 'dep');
+    expect(dep.hosted!.url.toString(), 'https://hosted_url');
+    expect(dep.toString(), 'HostedDependency: ^1.0.0');
+  });
+
+  test('map w/ bad version value', () {
+    _expectThrows(
+      {
+        'version': 'not a version',
+        'hosted': {'name': 'hosted_name', 'url': 'hosted_url'},
+      },
+      r'''
+line 5, column 15: Unsupported value for "version". Could not parse version "not a version". Unknown text at "not a version".
+  ╷
+5 │    "version": "not a version",
+  │               ^^^^^^^^^^^^^^^
+  ╵''',
+    );
+  });
+
+  test('map w/ extra keys should fail', () {
+    _expectThrows(
+      {
+        'version': '^1.0.0',
+        'hosted': {'name': 'hosted_name', 'url': 'hosted_url'},
+        'not_supported': null,
+      },
+      r'''
+line 10, column 4: Unrecognized keys: [not_supported]; supported keys: [sdk, git, path, hosted]
+   ╷
+10 │    "not_supported": null
+   │    ^^^^^^^^^^^^^^^
+   ╵''',
+    );
+  });
+
+  test('map w/ version and hosted as String', () async {
+    final dep = await _dependency<HostedDependency>(
+      {'version': '^1.0.0', 'hosted': 'hosted_url'},
+      skipTryPub: true, // todo: Unskip once put supports this
+    );
+    expect(dep.version.toString(), '^1.0.0');
+    expect(dep.hosted!.declaredName, isNull);
+    expect(dep.hosted!.name, 'dep');
+    expect(dep.hosted!.url, Uri.parse('hosted_url'));
+    expect(dep.toString(), 'HostedDependency: ^1.0.0');
+  });
+
+  test('map w/ hosted as String', () async {
+    final dep = await _dependency<HostedDependency>({'hosted': 'hosted_url'});
+    expect(dep.version, VersionConstraint.any);
+    expect(dep.hosted!.declaredName, isNull);
+    expect(dep.hosted!.name, 'dep');
+    expect(dep.hosted!.url, Uri.parse('hosted_url'));
+    expect(dep.toString(), 'HostedDependency: any');
+  });
+
+  test('map w/ null hosted should error', () {
+    _expectThrows(
+      {'hosted': null},
+      r'''
+line 5, column 4: These keys had `null` values, which is not allowed: [hosted]
+  ╷
+5 │    "hosted": null
+  │    ^^^^^^^^
+  ╵''',
+    );
+  });
+
+  test('map w/ null version is fine', () async {
+    final dep = await _dependency<HostedDependency>({'version': null});
+    expect(dep.version, VersionConstraint.any);
+    expect(dep.hosted, isNull);
+    expect(dep.toString(), 'HostedDependency: any');
+  });
+}
+
+void _sdkDependency() {
+  test('without version', () async {
+    final dep = await _dependency<SdkDependency>({'sdk': 'flutter'});
+    expect(dep.sdk, 'flutter');
+    expect(dep.version, VersionConstraint.any);
+    expect(dep.toString(), 'SdkDependency: flutter');
+  });
+
+  test('with version', () async {
+    final dep = await _dependency<SdkDependency>(
+      {'sdk': 'flutter', 'version': '>=1.2.3 <2.0.0'},
+    );
+    expect(dep.sdk, 'flutter');
+    expect(dep.version.toString(), '>=1.2.3 <2.0.0');
+    expect(dep.toString(), 'SdkDependency: flutter');
+  });
+
+  test('null content', () {
+    _expectThrowsContaining(
+      {'sdk': null},
+      r"type 'Null' is not a subtype of type 'String'",
+    );
+  });
+
+  test('number content', () {
+    _expectThrowsContaining(
+      {'sdk': 42},
+      r"type 'int' is not a subtype of type 'String'",
+    );
+  });
+}
+
+void _gitDependency() {
+  test('string', () async {
+    final dep = await _dependency<GitDependency>({'git': 'url'});
+    expect(dep.url.toString(), 'url');
+    expect(dep.path, isNull);
+    expect(dep.ref, isNull);
+    expect(dep.toString(), 'GitDependency: url@url');
+  });
+
+  test('string with version key is ignored', () async {
+    // Regression test for https://github.com/dart-lang/pubspec_parse/issues/13
+    final dep =
+        await _dependency<GitDependency>({'git': 'url', 'version': '^1.2.3'});
+    expect(dep.url.toString(), 'url');
+    expect(dep.path, isNull);
+    expect(dep.ref, isNull);
+    expect(dep.toString(), 'GitDependency: url@url');
+  });
+
+  test('string with user@ URL', () async {
+    final skipTryParse = Platform.environment.containsKey('TRAVIS');
+    if (skipTryParse) {
+      print('FYI: not validating git@ URI on travis due to failure');
+    }
+    final dep = await _dependency<GitDependency>(
+      {'git': 'git@localhost:dep.git'},
+      skipTryPub: skipTryParse,
+    );
+    expect(dep.url.toString(), 'ssh://git@localhost/dep.git');
+    expect(dep.path, isNull);
+    expect(dep.ref, isNull);
+    expect(dep.toString(), 'GitDependency: url@ssh://git@localhost/dep.git');
+  });
+
+  test('string with random extra key fails', () {
+    _expectThrows(
+      {'git': 'url', 'bob': '^1.2.3'},
+      r'''
+line 6, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
+  ╷
+6 │    "bob": "^1.2.3"
+  │    ^^^^^
+  ╵''',
+    );
+  });
+
+  test('map', () async {
+    final dep = await _dependency<GitDependency>({
+      'git': {'url': 'url', 'path': 'path', 'ref': 'ref'},
+    });
+    expect(dep.url.toString(), 'url');
+    expect(dep.path, 'path');
+    expect(dep.ref, 'ref');
+    expect(dep.toString(), 'GitDependency: url@url');
+  });
+
+  test('git - null content', () {
+    _expectThrows(
+      {'git': null},
+      r'''
+line 5, column 11: Unsupported value for "git". Must be a String or a Map.
+  ╷
+5 │      "git": null
+  │ ┌───────────^
+6 │ │   }
+  │ └──^
+  ╵''',
+    );
+  });
+
+  test('git - int content', () {
+    _expectThrows(
+      {'git': 42},
+      r'''
+line 5, column 11: Unsupported value for "git". Must be a String or a Map.
+  ╷
+5 │      "git": 42
+  │ ┌───────────^
+6 │ │   }
+  │ └──^
+  ╵''',
+    );
+  });
+
+  test('git - empty map', () {
+    _expectThrowsContaining(
+      {'git': <String, dynamic>{}},
+      r"type 'Null' is not a subtype of type 'String'",
+    );
+  });
+
+  test('git - null url', () {
+    _expectThrowsContaining(
+      {
+        'git': {'url': null},
+      },
+      r"type 'Null' is not a subtype of type 'String'",
+    );
+  });
+
+  test('git - int url', () {
+    _expectThrowsContaining(
+      {
+        'git': {'url': 42},
+      },
+      r"type 'int' is not a subtype of type 'String'",
+    );
+  });
+}
+
+void _pathDependency() {
+  test('valid', () async {
+    final dep = await _dependency<PathDependency>({'path': '../path'});
+    expect(dep.path, '../path');
+    expect(dep.toString(), 'PathDependency: path@../path');
+  });
+
+  test('valid with version key is ignored', () async {
+    final dep = await _dependency<PathDependency>(
+      {'path': '../path', 'version': '^1.2.3'},
+    );
+    expect(dep.path, '../path');
+    expect(dep.toString(), 'PathDependency: path@../path');
+  });
+
+  test('valid with random extra key fails', () {
+    _expectThrows(
+      {'path': '../path', 'bob': '^1.2.3'},
+      r'''
+line 6, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
+  ╷
+6 │    "bob": "^1.2.3"
+  │    ^^^^^
+  ╵''',
+    );
+  });
+
+  test('null content', () {
+    _expectThrows(
+      {'path': null},
+      r'''
+line 5, column 12: Unsupported value for "path". Must be a String.
+  ╷
+5 │      "path": null
+  │ ┌────────────^
+6 │ │   }
+  │ └──^
+  ╵''',
+    );
+  });
+
+  test('int content', () {
+    _expectThrows(
+      {'path': 42},
+      r'''
+line 5, column 12: Unsupported value for "path". Must be a String.
+  ╷
+5 │      "path": 42
+  │ ┌────────────^
+6 │ │   }
+  │ └──^
+  ╵''',
+    );
+  });
+}
+
+void _expectThrows(Object content, String expectedError) {
+  expectParseThrows(
+    {
+      'name': 'sample',
+      'dependencies': {'dep': content},
+    },
+    expectedError,
+  );
+}
+
+void _expectThrowsContaining(Object content, String errorText) {
+  expectParseThrowsContaining(
+    {
+      'name': 'sample',
+      'dependencies': {'dep': content},
+    },
+    errorText,
+  );
+}
+
+Future<T> _dependency<T extends Dependency>(
+  Object? content, {
+  bool skipTryPub = false,
+}) async {
+  final value = await parse(
+    {
+      ...defaultPubspec,
+      'dependencies': {'dep': content},
+    },
+    skipTryPub: skipTryPub,
+  );
+  expect(value.name, 'sample');
+  expect(value.dependencies, hasLength(1));
+
+  final entry = value.dependencies.entries.single;
+  expect(entry.key, 'dep');
+
+  return entry.value as T;
+}
diff --git a/pkgs/pubspec_parse/test/ensure_build_test.dart b/pkgs/pubspec_parse/test/ensure_build_test.dart
new file mode 100644
index 0000000..0e4371c
--- /dev/null
+++ b/pkgs/pubspec_parse/test/ensure_build_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2018, 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.
+
+@Timeout.factor(2)
+@TestOn('vm')
+@Tags(['presubmit-only'])
+library;
+
+import 'package:build_verify/build_verify.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test(
+    'ensure_build',
+    () => expectBuildClean(packageRelativeDirectory: 'pkgs/pubspec_parse/'),
+  );
+}
diff --git a/pkgs/pubspec_parse/test/git_uri_test.dart b/pkgs/pubspec_parse/test/git_uri_test.dart
new file mode 100644
index 0000000..be89ba8
--- /dev/null
+++ b/pkgs/pubspec_parse/test/git_uri_test.dart
@@ -0,0 +1,25 @@
+import 'package:pubspec_parse/src/dependency.dart';
+import 'package:test/test.dart';
+
+void main() {
+  for (var item in {
+    'git@github.com:google/grinder.dart.git':
+        'ssh://git@github.com/google/grinder.dart.git',
+    'host.xz:path/to/repo.git/': 'ssh://host.xz/path/to/repo.git/',
+    'http:path/to/repo.git/': 'ssh://http/path/to/repo.git/',
+    'file:path/to/repo.git/': 'ssh://file/path/to/repo.git/',
+    './foo:bar': 'foo%3Abar',
+    '/path/to/repo.git/': '/path/to/repo.git/',
+    'file:///path/to/repo.git/': 'file:///path/to/repo.git/',
+  }.entries) {
+    test(item.key, () {
+      final uri = parseGitUri(item.key);
+
+      printOnFailure(
+        [uri.scheme, uri.userInfo, uri.host, uri.port, uri.path].join('\n'),
+      );
+
+      expect(uri, Uri.parse(item.value));
+    });
+  }
+}
diff --git a/pkgs/pubspec_parse/test/parse_test.dart b/pkgs/pubspec_parse/test/parse_test.dart
new file mode 100644
index 0000000..e0698af
--- /dev/null
+++ b/pkgs/pubspec_parse/test/parse_test.dart
@@ -0,0 +1,827 @@
+// Copyright (c) 2018, 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  test('minimal set values', () async {
+    final value = await parse(defaultPubspec);
+    expect(value.name, 'sample');
+    expect(value.version, isNull);
+    expect(value.publishTo, isNull);
+    expect(value.description, isNull);
+    expect(value.homepage, isNull);
+    expect(value.author, isNull);
+    expect(value.authors, isEmpty);
+    expect(
+      value.environment,
+      {'sdk': VersionConstraint.parse('>=2.12.0 <3.0.0')},
+    );
+    expect(value.documentation, isNull);
+    expect(value.dependencies, isEmpty);
+    expect(value.devDependencies, isEmpty);
+    expect(value.dependencyOverrides, isEmpty);
+    expect(value.flutter, isNull);
+    expect(value.repository, isNull);
+    expect(value.issueTracker, isNull);
+    expect(value.screenshots, isEmpty);
+    expect(value.workspace, isNull);
+    expect(value.resolution, isNull);
+    expect(value.executables, isEmpty);
+  });
+
+  test('all fields set', () async {
+    final version = Version.parse('1.2.3');
+    final sdkConstraint = VersionConstraint.parse('>=3.6.0 <4.0.0');
+    final value = await parse(
+      {
+        'name': 'sample',
+        'version': version.toString(),
+        'publish_to': 'none',
+        'author': 'name@example.com',
+        'environment': {'sdk': sdkConstraint.toString()},
+        'description': 'description',
+        'homepage': 'homepage',
+        'documentation': 'documentation',
+        'repository': 'https://github.com/example/repo',
+        'issue_tracker': 'https://github.com/example/repo/issues',
+        'funding': [
+          'https://patreon.com/example',
+        ],
+        'topics': ['widget', 'button'],
+        'ignored_advisories': ['111', '222'],
+        'screenshots': [
+          {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+        ],
+        'workspace': [
+          'pkg1',
+          'pkg2',
+        ],
+        'resolution': 'workspace',
+        'executables': {
+          'my_script': 'bin/my_script.dart',
+          'my_script2': 'bin/my_script2.dart',
+        },
+      },
+      skipTryPub: true,
+    );
+    expect(value.name, 'sample');
+    expect(value.version, version);
+    expect(value.publishTo, 'none');
+    expect(value.description, 'description');
+    expect(value.homepage, 'homepage');
+    expect(value.author, 'name@example.com');
+    expect(value.authors, ['name@example.com']);
+    expect(value.environment, hasLength(1));
+    expect(value.environment, containsPair('sdk', sdkConstraint));
+    expect(value.documentation, 'documentation');
+    expect(value.dependencies, isEmpty);
+    expect(value.devDependencies, isEmpty);
+    expect(value.dependencyOverrides, isEmpty);
+    expect(value.repository, Uri.parse('https://github.com/example/repo'));
+    expect(
+      value.issueTracker,
+      Uri.parse('https://github.com/example/repo/issues'),
+    );
+    expect(value.funding, hasLength(1));
+    expect(value.funding!.single.toString(), 'https://patreon.com/example');
+    expect(value.topics, hasLength(2));
+    expect(value.topics!.first, 'widget');
+    expect(value.topics!.last, 'button');
+    expect(value.ignoredAdvisories, hasLength(2));
+    expect(value.ignoredAdvisories!.first, '111');
+    expect(value.ignoredAdvisories!.last, '222');
+    expect(value.screenshots, hasLength(1));
+    expect(value.screenshots!.first.description, 'my screenshot');
+    expect(value.screenshots!.first.path, 'path/to/screenshot');
+    expect(value.executables, hasLength(2));
+    expect(value.executables.keys, contains('my_script'));
+    expect(value.executables.keys, contains('my_script2'));
+    expect(value.executables['my_script'], 'bin/my_script.dart');
+    expect(value.executables['my_script2'], 'bin/my_script2.dart');
+    expect(value.workspace, hasLength(2));
+    expect(value.workspace!.first, 'pkg1');
+    expect(value.workspace!.last, 'pkg2');
+    expect(value.resolution, 'workspace');
+  });
+
+  test('environment values can be null', () async {
+    final value = await parse(
+      {
+        'name': 'sample',
+        'environment': {
+          'sdk': '>=2.12.0 <3.0.0',
+          'bob': null,
+        },
+      },
+      skipTryPub: true,
+    );
+    expect(value.name, 'sample');
+    expect(value.environment, hasLength(2));
+    expect(value.environment, containsPair('bob', isNull));
+  });
+
+  group('publish_to', () {
+    for (var entry in {
+      42: "Unsupported value for \"publish_to\". type 'int' is not a subtype of type 'String?'",
+      '##not a uri!': r'''
+line 3, column 16: Unsupported value for "publish_to". Must be an http or https URL.
+  ╷
+3 │  "publish_to": "##not a uri!"
+  │                ^^^^^^^^^^^^^^
+  ╵''',
+      '/cool/beans': r'''
+line 3, column 16: Unsupported value for "publish_to". Must be an http or https URL.
+  ╷
+3 │  "publish_to": "/cool/beans"
+  │                ^^^^^^^^^^^^^
+  ╵''',
+      'file:///Users/kevmoo/': r'''
+line 3, column 16: Unsupported value for "publish_to". Must be an http or https URL.
+  ╷
+3 │  "publish_to": "file:///Users/kevmoo/"
+  │                ^^^^^^^^^^^^^^^^^^^^^^^
+  ╵''',
+    }.entries) {
+      test('cannot be `${entry.key}`', () {
+        expectParseThrowsContaining(
+          {'name': 'sample', 'publish_to': entry.key},
+          entry.value,
+          skipTryPub: true,
+        );
+      });
+    }
+
+    for (var entry in {
+      null: null,
+      'http': 'http://example.com',
+      'https': 'https://example.com',
+      'none': 'none',
+    }.entries) {
+      test('can be ${entry.key}', () async {
+        final value = await parse({
+          ...defaultPubspec,
+          'publish_to': entry.value,
+        });
+        expect(value.publishTo, entry.value);
+      });
+    }
+  });
+
+  group('author, authors', () {
+    test('one author', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'author': 'name@example.com',
+      });
+      expect(value.author, 'name@example.com');
+      expect(value.authors, ['name@example.com']);
+    });
+
+    test('one author, via authors', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'authors': ['name@example.com'],
+      });
+      expect(value.author, 'name@example.com');
+      expect(value.authors, ['name@example.com']);
+    });
+
+    test('many authors', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'authors': ['name@example.com', 'name2@example.com'],
+      });
+      expect(value.author, isNull);
+      expect(value.authors, ['name@example.com', 'name2@example.com']);
+    });
+
+    test('author and authors', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'author': 'name@example.com',
+        'authors': ['name2@example.com'],
+      });
+      expect(value.author, isNull);
+      expect(value.authors, ['name@example.com', 'name2@example.com']);
+    });
+
+    test('duplicate author values', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'author': 'name@example.com',
+        'authors': ['name@example.com', 'name@example.com'],
+      });
+      expect(value.author, 'name@example.com');
+      expect(value.authors, ['name@example.com']);
+    });
+
+    test('flutter', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'flutter': {'key': 'value'},
+      });
+      expect(value.flutter, {'key': 'value'});
+    });
+  });
+
+  group('executables', () {
+    test('one executable', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'executables': {'my_script': 'bin/my_script.dart'},
+      });
+      expect(value.executables, hasLength(1));
+      expect(value.executables.keys, contains('my_script'));
+      expect(value.executables['my_script'], 'bin/my_script.dart');
+    });
+
+    test('many executables', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'executables': {
+          'my_script': 'bin/my_script.dart',
+          'my_script2': 'bin/my_script2.dart',
+        },
+      });
+      expect(value.executables, hasLength(2));
+      expect(value.executables.keys, contains('my_script'));
+      expect(value.executables.keys, contains('my_script2'));
+      expect(value.executables['my_script'], 'bin/my_script.dart');
+      expect(value.executables['my_script2'], 'bin/my_script2.dart');
+    });
+
+    test('invalid value', () async {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'executables': {
+            'script': 32,
+          },
+        },
+        'Unsupported value for "script". `32` is not a String.',
+        skipTryPub: true,
+      );
+    });
+
+    test('invalid executable - lenient', () async {
+      final value = await parse(
+        {
+          ...defaultPubspec,
+          'executables': 'Invalid value',
+        },
+        lenient: true,
+      );
+      expect(value.name, 'sample');
+      expect(value.executables, isEmpty);
+    });
+  });
+
+  group('invalid', () {
+    test('null', () {
+      expectParseThrows(
+        null,
+        r'''
+line 1, column 1: Not a map
+  ╷
+1 │ null
+  │ ^^^^
+  ╵''',
+      );
+    });
+    test('empty string', () {
+      expectParseThrows(
+        '',
+        r'''
+line 1, column 1: Not a map
+  ╷
+1 │ ""
+  │ ^^
+  ╵''',
+      );
+    });
+    test('array', () {
+      expectParseThrows(
+        [],
+        r'''
+line 1, column 1: Not a map
+  ╷
+1 │ []
+  │ ^^
+  ╵''',
+      );
+    });
+
+    test('missing name', () {
+      expectParseThrowsContaining(
+        {},
+        "Missing key \"name\". type 'Null' is not a subtype of type 'String'",
+      );
+    });
+
+    test('null name value', () {
+      expectParseThrowsContaining(
+        {'name': null},
+        "Unsupported value for \"name\". type 'Null' is not a subtype of type 'String'",
+      );
+    });
+
+    test('empty name value', () {
+      expectParseThrows(
+        {'name': ''},
+        r'''
+line 2, column 10: Unsupported value for "name". "name" cannot be empty.
+  ╷
+2 │  "name": ""
+  │          ^^
+  ╵''',
+      );
+    });
+
+    test('"dart" is an invalid environment key', () {
+      expectParseThrows(
+        {
+          'name': 'sample',
+          'environment': {'dart': 'cool'},
+        },
+        r'''
+line 4, column 3: Use "sdk" to for Dart SDK constraints.
+  ╷
+4 │   "dart": "cool"
+  │   ^^^^^^
+  ╵''',
+      );
+    });
+
+    test('environment values cannot be int', () {
+      expectParseThrows(
+        {
+          'name': 'sample',
+          'environment': {'sdk': 42},
+        },
+        r'''
+line 4, column 10: Unsupported value for "sdk". `42` is not a String.
+  ╷
+4 │     "sdk": 42
+  │ ┌──────────^
+5 │ │  }
+  │ └─^
+  ╵''',
+      );
+    });
+
+    test('version', () {
+      expectParseThrows(
+        {'name': 'sample', 'version': 'invalid'},
+        r'''
+line 3, column 13: Unsupported value for "version". Could not parse "invalid".
+  ╷
+3 │  "version": "invalid"
+  │             ^^^^^^^^^
+  ╵''',
+      );
+    });
+
+    test('invalid environment value', () {
+      expectParseThrows(
+        {
+          'name': 'sample',
+          'environment': {'sdk': 'silly'},
+        },
+        r'''
+line 4, column 10: Unsupported value for "sdk". Could not parse version "silly". Unknown text at "silly".
+  ╷
+4 │   "sdk": "silly"
+  │          ^^^^^^^
+  ╵''',
+      );
+    });
+
+    test('bad repository url', () {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'repository': {'x': 'y'},
+        },
+        "Unsupported value for \"repository\". type 'YamlMap' is not a subtype of type 'String'",
+        skipTryPub: true,
+      );
+    });
+
+    test('bad issue_tracker url', () {
+      expectParseThrowsContaining(
+        {
+          'name': 'sample',
+          'issue_tracker': {'x': 'y'},
+        },
+        "Unsupported value for \"issue_tracker\". type 'YamlMap' is not a subtype of type 'String'",
+        skipTryPub: true,
+      );
+    });
+  });
+
+  group('funding', () {
+    test('not a list', () {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'funding': 1,
+        },
+        "Unsupported value for \"funding\". type 'int' is not a subtype of type 'List<dynamic>?'",
+        skipTryPub: true,
+      );
+    });
+
+    test('not an uri', () {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'funding': [1],
+        },
+        "Unsupported value for \"funding\". type 'int' is not a subtype of type 'String'",
+        skipTryPub: true,
+      );
+    });
+
+    test('not an uri', () {
+      expectParseThrows(
+        {
+          ...defaultPubspec,
+          'funding': ['ht tps://example.com/'],
+        },
+        r'''
+line 6, column 13: Unsupported value for "funding". Illegal scheme character at offset 2.
+  ╷
+6 │    "funding": [
+  │ ┌─────────────^
+7 │ │   "ht tps://example.com/"
+8 │ └  ]
+  ╵''',
+        skipTryPub: true,
+      );
+    });
+  });
+  group('topics', () {
+    test('not a list', () {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'topics': 1,
+        },
+        "Unsupported value for \"topics\". type 'int' is not a subtype of type 'List<dynamic>?'",
+        skipTryPub: true,
+      );
+    });
+
+    test('not a string', () {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'topics': [1],
+        },
+        "Unsupported value for \"topics\". type 'int' is not a subtype of type 'String'",
+        skipTryPub: true,
+      );
+    });
+
+    test('invalid data - lenient', () async {
+      final value = await parse(
+        {
+          ...defaultPubspec,
+          'topics': [1],
+        },
+        skipTryPub: true,
+        lenient: true,
+      );
+      expect(value.name, 'sample');
+      expect(value.topics, isNull);
+    });
+  });
+
+  group('ignored_advisories', () {
+    test('not a list', () {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'ignored_advisories': 1,
+        },
+        "Unsupported value for \"ignored_advisories\". type 'int' is not a subtype of type 'List<dynamic>?'",
+        skipTryPub: true,
+      );
+    });
+
+    test('not a string', () {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'ignored_advisories': [1],
+        },
+        "Unsupported value for \"ignored_advisories\". type 'int' is not a subtype of type 'String'",
+        skipTryPub: true,
+      );
+    });
+
+    test('invalid data - lenient', () async {
+      final value = await parse(
+        {
+          ...defaultPubspec,
+          'ignored_advisories': [1],
+        },
+        skipTryPub: true,
+        lenient: true,
+      );
+      expect(value.name, 'sample');
+      expect(value.ignoredAdvisories, isNull);
+    });
+  });
+
+  group('screenshots', () {
+    test('one screenshot', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'screenshots': [
+          {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+        ],
+      });
+      expect(value.screenshots, hasLength(1));
+      expect(value.screenshots!.first.description, 'my screenshot');
+      expect(value.screenshots!.first.path, 'path/to/screenshot');
+    });
+
+    test('many screenshots', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'screenshots': [
+          {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+          {
+            'description': 'my second screenshot',
+            'path': 'path/to/screenshot2',
+          },
+        ],
+      });
+      expect(value.screenshots, hasLength(2));
+      expect(value.screenshots!.first.description, 'my screenshot');
+      expect(value.screenshots!.first.path, 'path/to/screenshot');
+      expect(value.screenshots!.last.description, 'my second screenshot');
+      expect(value.screenshots!.last.path, 'path/to/screenshot2');
+    });
+
+    test('one screenshot plus invalid entries', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'screenshots': [
+          42,
+          {
+            'description': 'my screenshot',
+            'path': 'path/to/screenshot',
+            'extraKey': 'not important',
+          },
+          'not a screenshot',
+        ],
+      });
+      expect(value.screenshots, hasLength(1));
+      expect(value.screenshots!.first.description, 'my screenshot');
+      expect(value.screenshots!.first.path, 'path/to/screenshot');
+    });
+
+    test('invalid entries', () async {
+      final value = await parse({
+        ...defaultPubspec,
+        'screenshots': [
+          42,
+          'not a screenshot',
+        ],
+      });
+      expect(value.screenshots, isEmpty);
+    });
+
+    test('missing key `dessription', () {
+      expectParseThrows(
+        {
+          ...defaultPubspec,
+          'screenshots': [
+            {'path': 'my/path'},
+          ],
+        },
+        r'''
+line 7, column 3: Missing key "description". Missing required key `description`
+  ╷
+7 │ ┌   {
+8 │ │    "path": "my/path"
+9 │ └   }
+  ╵''',
+        skipTryPub: true,
+      );
+    });
+
+    test('missing key `path`', () {
+      expectParseThrows(
+        {
+          ...defaultPubspec,
+          'screenshots': [
+            {'description': 'my screenshot'},
+          ],
+        },
+        r'''
+line 7, column 3: Missing key "path". Missing required key `path`
+  ╷
+7 │ ┌   {
+8 │ │    "description": "my screenshot"
+9 │ └   }
+  ╵''',
+        skipTryPub: true,
+      );
+    });
+
+    test('Value of description not a String`', () {
+      expectParseThrows(
+        {
+          ...defaultPubspec,
+          'screenshots': [
+            {'description': 42},
+          ],
+        },
+        r'''
+line 8, column 19: Unsupported value for "description". `42` is not a String
+  ╷
+8 │      "description": 42
+  │ ┌───────────────────^
+9 │ │   }
+  │ └──^
+  ╵''',
+        skipTryPub: true,
+      );
+    });
+
+    test('Value of path not a String`', () {
+      expectParseThrows(
+        {
+          ...defaultPubspec,
+          'screenshots': [
+            {
+              'description': '',
+              'path': 42,
+            },
+          ],
+        },
+        r'''
+line 9, column 12: Unsupported value for "path". `42` is not a String
+   ╷
+9  │      "path": 42
+   │ ┌────────────^
+10 │ │   }
+   │ └──^
+   ╵''',
+        skipTryPub: true,
+      );
+    });
+
+    test('invalid screenshot - lenient', () async {
+      final value = await parse(
+        {
+          ...defaultPubspec,
+          'screenshots': 'Invalid value',
+        },
+        lenient: true,
+      );
+      expect(value.name, 'sample');
+      expect(value.screenshots, isEmpty);
+    });
+  });
+
+  group('lenient', () {
+    test('null', () {
+      expectParseThrows(
+        null,
+        r'''
+line 1, column 1: Not a map
+  ╷
+1 │ null
+  │ ^^^^
+  ╵''',
+        lenient: true,
+      );
+    });
+
+    test('empty string', () {
+      expectParseThrows(
+        '',
+        r'''
+line 1, column 1: Not a map
+  ╷
+1 │ ""
+  │ ^^
+  ╵''',
+        lenient: true,
+      );
+    });
+
+    test('name cannot be empty', () {
+      expectParseThrowsContaining(
+        {},
+        "Missing key \"name\". type 'Null' is not a subtype of type 'String'",
+        lenient: true,
+      );
+    });
+
+    test('bad repository url', () async {
+      final value = await parse(
+        {
+          ...defaultPubspec,
+          'repository': {'x': 'y'},
+        },
+        lenient: true,
+      );
+      expect(value.name, 'sample');
+      expect(value.repository, isNull);
+    });
+
+    test('bad issue_tracker url', () async {
+      final value = await parse(
+        {
+          ...defaultPubspec,
+          'issue_tracker': {'x': 'y'},
+        },
+        lenient: true,
+      );
+      expect(value.name, 'sample');
+      expect(value.issueTracker, isNull);
+    });
+
+    test('multiple bad values', () async {
+      final value = await parse(
+        {
+          ...defaultPubspec,
+          'repository': {'x': 'y'},
+          'issue_tracker': {'x': 'y'},
+        },
+        lenient: true,
+      );
+      expect(value.name, 'sample');
+      expect(value.repository, isNull);
+      expect(value.issueTracker, isNull);
+    });
+
+    test('deep error throws with lenient', () {
+      expect(
+        () => parse(
+          {
+            'name': 'sample',
+            'dependencies': {
+              'foo': {
+                'git': {'url': 1},
+              },
+            },
+            'issue_tracker': {'x': 'y'},
+          },
+          skipTryPub: true,
+          lenient: true,
+        ),
+        throwsException,
+      );
+    });
+  });
+
+  group('workspaces', () {
+    test('workspace key must be a list', () {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'workspace': 42,
+        },
+        'Unsupported value for "workspace". type \'int\' is not a subtype of type \'List<dynamic>?\' in type cast',
+        skipTryPub: true,
+      );
+    });
+
+    test('workspace key must be a list of strings', () {
+      expectParseThrowsContaining(
+        {
+          ...defaultPubspec,
+          'workspace': [42],
+        },
+        'Unsupported value for "workspace". type \'int\' is not a subtype of type \'String\' in type cast',
+        skipTryPub: true,
+      );
+    });
+
+    test('resolution key must be a string', () {
+      expectParseThrowsContaining(
+        {
+          'name': 'sample',
+          'environment': {'sdk': '^3.6.0'},
+          'resolution': 42,
+        },
+        'Unsupported value for "resolution". type \'int\' is not a subtype of type \'String?\' in type cast',
+        skipTryPub: true,
+      );
+    });
+  });
+}
diff --git a/pkgs/pubspec_parse/test/pub_utils.dart b/pkgs/pubspec_parse/test/pub_utils.dart
new file mode 100644
index 0000000..a60aa2a
--- /dev/null
+++ b/pkgs/pubspec_parse/test/pub_utils.dart
@@ -0,0 +1,88 @@
+// Copyright (c) 2018, 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:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test_process/test_process.dart';
+
+Future<ProcResult> tryPub(String content) async {
+  await d.file('pubspec.yaml', content).create();
+
+  final proc = await TestProcess.start(
+    Platform.resolvedExecutable,
+    ['pub', 'get', '--offline'],
+    workingDirectory: d.sandbox,
+    // Don't pass current process VM options to child
+    environment: Map.from(Platform.environment)..remove('DART_VM_OPTIONS'),
+  );
+
+  final result = await ProcResult.fromTestProcess(proc);
+
+  printOnFailure(
+    [
+      '-----BEGIN pub output-----',
+      result.toString().trim(),
+      '-----END pub output-----',
+    ].join('\n'),
+  );
+
+  if (result.exitCode == 0) {
+    final lockContent =
+        File(p.join(d.sandbox, 'pubspec.lock')).readAsStringSync();
+
+    printOnFailure(
+      [
+        '-----BEGIN pubspec.lock-----',
+        lockContent.trim(),
+        '-----END pubspec.lock-----',
+      ].join('\n'),
+    );
+  }
+
+  return result;
+}
+
+class ProcResult {
+  final int exitCode;
+  final List<ProcLine> lines;
+
+  bool get cleanParse => exitCode == 0 || exitCode == 66 || exitCode == 69;
+
+  ProcResult(this.exitCode, this.lines);
+
+  static Future<ProcResult> fromTestProcess(TestProcess proc) async {
+    final items = <ProcLine>[];
+
+    final values = await Future.wait([
+      proc.exitCode,
+      proc.stdoutStream().forEach((line) => items.add(ProcLine(false, line))),
+      proc.stderrStream().forEach((line) => items.add(ProcLine(true, line))),
+    ]);
+
+    return ProcResult(values[0] as int, items);
+  }
+
+  @override
+  String toString() {
+    final buffer = StringBuffer('Exit code: $exitCode');
+    for (var line in lines) {
+      buffer.write('\n$line');
+    }
+    return buffer.toString();
+  }
+}
+
+class ProcLine {
+  final bool isError;
+  final String line;
+
+  ProcLine(this.isError, this.line);
+
+  @override
+  String toString() => '${isError ? 'err' : 'out'}  $line';
+}
diff --git a/pkgs/pubspec_parse/test/test_utils.dart b/pkgs/pubspec_parse/test/test_utils.dart
new file mode 100644
index 0000000..cc46522
--- /dev/null
+++ b/pkgs/pubspec_parse/test/test_utils.dart
@@ -0,0 +1,157 @@
+// Copyright (c) 2018, 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 'package:checked_yaml/checked_yaml.dart';
+import 'package:json_annotation/json_annotation.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'pub_utils.dart';
+
+const defaultPubspec = {
+  'name': 'sample',
+  'environment': {'sdk': '>=2.12.0 <3.0.0'},
+};
+
+String _encodeJson(Object? input) =>
+    const JsonEncoder.withIndent(' ').convert(input);
+
+Matcher _throwsParsedYamlException(String prettyValue) => throwsA(
+      const TypeMatcher<ParsedYamlException>().having(
+        (e) {
+          final message = e.formattedMessage;
+          printOnFailure("Actual error format:\nr'''\n$message'''");
+          _printDebugParsedYamlException(e);
+          return message;
+        },
+        'formattedMessage',
+        prettyValue,
+      ),
+    );
+
+void _printDebugParsedYamlException(ParsedYamlException e) {
+  var innerError = e.innerError;
+  StackTrace? innerStack;
+
+  if (innerError is CheckedFromJsonException) {
+    final cfje = innerError;
+
+    if (cfje.innerError != null) {
+      innerError = cfje.innerError;
+      innerStack = cfje.innerStack;
+    }
+  }
+
+  if (innerError != null) {
+    final items = [innerError];
+    if (innerStack != null) {
+      items.add(Trace.format(innerStack));
+    }
+
+    final content =
+        LineSplitter.split(items.join('\n')).map((e) => '  $e').join('\n');
+
+    printOnFailure('Inner error details:\n$content');
+  }
+}
+
+Future<Pubspec> parse(
+  Object? content, {
+  bool quietOnError = false,
+  bool skipTryPub = false,
+  bool lenient = false,
+}) async {
+  final encoded = _encodeJson(content);
+
+  ProcResult? pubResult;
+  if (!skipTryPub) {
+    // ignore: deprecated_member_use
+    pubResult = await tryPub(encoded);
+    expect(pubResult, isNotNull);
+  }
+
+  try {
+    final value = Pubspec.parse(encoded, lenient: lenient);
+
+    if (pubResult != null) {
+      addTearDown(() {
+        expect(
+          pubResult!.cleanParse,
+          isTrue,
+          reason:
+              'On success, parsing from the pub client should also succeed.',
+        );
+      });
+    }
+    return value;
+  } catch (e) {
+    if (pubResult != null) {
+      addTearDown(() {
+        expect(
+          pubResult!.cleanParse,
+          isFalse,
+          reason: 'On failure, parsing from the pub client should also fail.',
+        );
+      });
+    }
+    if (e is ParsedYamlException) {
+      if (!quietOnError) {
+        _printDebugParsedYamlException(e);
+      }
+    }
+    rethrow;
+  }
+}
+
+void expectParseThrows(
+  Object? content,
+  String expectedError, {
+  bool skipTryPub = false,
+  bool lenient = false,
+}) =>
+    expect(
+      () => parse(
+        content,
+        lenient: lenient,
+        quietOnError: true,
+        skipTryPub: skipTryPub,
+      ),
+      _throwsParsedYamlException(expectedError),
+    );
+
+void expectParseThrowsContaining(
+  Object? content,
+  String errorFragment, {
+  bool skipTryPub = false,
+  bool lenient = false,
+}) {
+  expect(
+    () => parse(
+      content,
+      lenient: lenient,
+      quietOnError: true,
+      skipTryPub: skipTryPub,
+    ),
+    _throwsParsedYamlExceptionContaining(errorFragment),
+  );
+}
+
+// ignore: prefer_expression_function_bodies
+Matcher _throwsParsedYamlExceptionContaining(String errorFragment) {
+  return throwsA(
+    const TypeMatcher<ParsedYamlException>().having(
+      (e) {
+        final message = e.formattedMessage;
+        printOnFailure("Actual error format:\nr'''\n$message'''");
+        _printDebugParsedYamlException(e);
+        return message;
+      },
+      'formattedMessage',
+      contains(errorFragment),
+    ),
+  );
+}
diff --git a/pkgs/source_maps/.gitignore b/pkgs/source_maps/.gitignore
new file mode 100644
index 0000000..f73b2f9
--- /dev/null
+++ b/pkgs/source_maps/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.packages
+.pub/
+pubspec.lock
diff --git a/pkgs/source_maps/CHANGELOG.md b/pkgs/source_maps/CHANGELOG.md
new file mode 100644
index 0000000..b06ac72
--- /dev/null
+++ b/pkgs/source_maps/CHANGELOG.md
@@ -0,0 +1,133 @@
+## 0.10.14-wip
+
+## 0.10.13
+
+* Require Dart 3.3
+* Move to `dart-lang/tools` monorepo.
+
+## 0.10.12
+
+* Add additional types at API boundaries.
+
+## 0.10.11
+
+* Populate the pubspec `repository` field.
+* Update the source map documentation link in the readme.
+
+## 0.10.10
+
+* Stable release for null safety.
+
+## 0.10.9
+
+* Fix a number of document comment issues.
+* Allow parsing source map files with a missing `names` field.
+
+## 0.10.8
+
+* Preserve source-map extensions in `SingleMapping`. Extensions are keys in the
+  json map that start with `"x_"`.
+
+## 0.10.7
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 0.10.6
+
+* Require version 2.0.0 of the Dart SDK.
+
+## 0.10.5
+
+* Add a `SingleMapping.files` field which provides access to `SourceFile`s
+  representing the `"sourcesContent"` fields in the source map.
+
+* Add an `includeSourceContents` flag to `SingleMapping.toJson()` which
+  indicates whether to include source file contents in the source map.
+
+## 0.10.4
+* Implement `highlight` in `SourceMapFileSpan`.
+* Require version `^1.3.0` of `source_span`.
+
+## 0.10.3
+ * Add `addMapping` and `containsMapping` members to `MappingBundle`.
+
+## 0.10.2
+ * Support for extended source map format.
+ * Polish `MappingBundle.spanFor` handling of URIs that have a suffix that
+   exactly match a source map in the MappingBundle.
+
+## 0.10.1+5
+ * Fix strong mode warning in test.
+
+## 0.10.1+4
+
+* Extend `MappingBundle.spanFor` to accept requests for output files that
+  don't have source maps.
+
+## 0.10.1+3
+
+* Add `MappingBundle` class that handles extended source map format that
+  supports source maps for multiple output files in a single mapper.
+  Extend `Mapping.spanFor` API to accept a uri parameter that is optional
+  for normal source maps but required for MappingBundle source maps.
+
+## 0.10.1+2
+
+* Fix more strong mode warnings.
+
+## 0.10.1+1
+
+* Fix all strong mode warnings.
+
+## 0.10.1
+
+* Add a `mapUrl` named argument to `parse` and `parseJson`. This argument is
+  used to resolve source URLs for source spans.
+
+## 0.10.0+2
+
+* Fix analyzer error (FileSpan has a new field since `source_span` 1.1.1)
+
+## 0.10.0+1
+
+* Remove an unnecessary warning printed when the "file" field is missing from a
+  Json formatted source map. This field is optional and its absence is not
+  unusual.
+
+## 0.10.0
+
+* Remove the `Span`, `Location` and `SourceFile` classes. Use the
+  corresponding `source_span` classes instead.
+
+## 0.9.4
+
+* Update `SpanFormatException` with `source` and `offset`.
+
+* All methods that take `Span`s, `Location`s, and `SourceFile`s as inputs now
+  also accept the corresponding `source_span` classes as well. Using the old
+  classes is now deprecated and will be unsupported in version 0.10.0.
+
+## 0.9.3
+
+* Support writing SingleMapping objects to source map version 3 format.
+* Support the `sourceRoot` field in the SingleMapping class.
+* Support updating the `targetUrl` field in the SingleMapping class.
+
+## 0.9.2+2
+
+* Fix a bug in `FixedSpan.getLocationMessage`.
+
+## 0.9.2+1
+
+* Minor readability improvements to `FixedSpan.getLocationMessage` and
+  `SpanException.toString`.
+
+## 0.9.2
+
+* Add `SpanException` and `SpanFormatException` classes.
+
+## 0.9.1
+
+* Support unmapped areas in source maps.
+
+* Increase the readability of location messages.
diff --git a/pkgs/source_maps/LICENSE b/pkgs/source_maps/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/pkgs/source_maps/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/source_maps/README.md b/pkgs/source_maps/README.md
new file mode 100644
index 0000000..cf80291
--- /dev/null
+++ b/pkgs/source_maps/README.md
@@ -0,0 +1,25 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/source_maps.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/source_maps.yaml)
+[![pub package](https://img.shields.io/pub/v/source_maps.svg)](https://pub.dev/packages/source_maps)
+[![package publisher](https://img.shields.io/pub/publisher/source_maps.svg)](https://pub.dev/packages/source_maps/publisher)
+
+This project implements a Dart pub package to work with source maps.
+
+## Docs and usage
+
+The implementation is based on the [source map version 3 spec][spec] which was
+originated from the [Closure Compiler][closure] and has been implemented in
+Chrome and Firefox.
+
+In this package we provide:
+
+  * Data types defining file locations and spans: these are not part of the
+    original source map specification. These data types are great for tracking
+    source locations on source maps, but they can also be used by tools to
+    reporting useful error messages that include on source locations.
+  * A builder that creates a source map programmatically and produces the encoded
+    source map format.
+  * A parser that reads the source map format and provides APIs to read the
+    mapping information.
+
+[closure]: https://github.com/google/closure-compiler/wiki/Source-Maps
+[spec]: https://docs.google.com/a/google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
diff --git a/pkgs/source_maps/analysis_options.yaml b/pkgs/source_maps/analysis_options.yaml
new file mode 100644
index 0000000..d978f81
--- /dev/null
+++ b/pkgs/source_maps/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/source_maps/lib/builder.dart b/pkgs/source_maps/lib/builder.dart
new file mode 100644
index 0000000..9043c63
--- /dev/null
+++ b/pkgs/source_maps/lib/builder.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains a builder object useful for creating source maps programatically.
+library;
+
+// TODO(sigmund): add a builder for multi-section mappings.
+
+import 'dart:convert';
+
+import 'package:source_span/source_span.dart';
+
+import 'parser.dart';
+import 'src/source_map_span.dart';
+
+/// Builds a source map given a set of mappings.
+class SourceMapBuilder {
+  final List<Entry> _entries = <Entry>[];
+
+  /// Adds an entry mapping the [targetOffset] to [source].
+  void addFromOffset(SourceLocation source, SourceFile targetFile,
+      int targetOffset, String identifier) {
+    ArgumentError.checkNotNull(targetFile, 'targetFile');
+    _entries.add(Entry(source, targetFile.location(targetOffset), identifier));
+  }
+
+  /// Adds an entry mapping [target] to [source].
+  ///
+  /// If [isIdentifier] is true or if [target] is a [SourceMapSpan] with
+  /// `isIdentifier` set to true, this entry is considered to represent an
+  /// identifier whose value will be stored in the source map. [isIdentifier]
+  /// takes precedence over [target]'s `isIdentifier` value.
+  void addSpan(SourceSpan source, SourceSpan target, {bool? isIdentifier}) {
+    isIdentifier ??= source is SourceMapSpan ? source.isIdentifier : false;
+
+    var name = isIdentifier ? source.text : null;
+    _entries.add(Entry(source.start, target.start, name));
+  }
+
+  /// Adds an entry mapping [target] to [source].
+  void addLocation(
+      SourceLocation source, SourceLocation target, String? identifier) {
+    _entries.add(Entry(source, target, identifier));
+  }
+
+  /// Encodes all mappings added to this builder as a json map.
+  Map<String, dynamic> build(String fileUrl) {
+    return SingleMapping.fromEntries(_entries, fileUrl).toJson();
+  }
+
+  /// Encodes all mappings added to this builder as a json string.
+  String toJson(String fileUrl) => jsonEncode(build(fileUrl));
+}
+
+/// An entry in the source map builder.
+class Entry implements Comparable<Entry> {
+  /// Span denoting the original location in the input source file
+  final SourceLocation source;
+
+  /// Span indicating the corresponding location in the target file.
+  final SourceLocation target;
+
+  /// An identifier name, when this location is the start of an identifier.
+  final String? identifierName;
+
+  /// Creates a new [Entry] mapping [target] to [source].
+  Entry(this.source, this.target, this.identifierName);
+
+  /// Implements [Comparable] to ensure that entries are ordered by their
+  /// location in the target file. We sort primarily by the target offset
+  /// because source map files are encoded by printing each mapping in order as
+  /// they appear in the target file.
+  @override
+  int compareTo(Entry other) {
+    var res = target.compareTo(other.target);
+    if (res != 0) return res;
+    res = source.sourceUrl
+        .toString()
+        .compareTo(other.source.sourceUrl.toString());
+    if (res != 0) return res;
+    return source.compareTo(other.source);
+  }
+}
diff --git a/pkgs/source_maps/lib/parser.dart b/pkgs/source_maps/lib/parser.dart
new file mode 100644
index 0000000..590dfc6
--- /dev/null
+++ b/pkgs/source_maps/lib/parser.dart
@@ -0,0 +1,718 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains the top-level function to parse source maps version 3.
+library;
+
+import 'dart:convert';
+
+import 'package:source_span/source_span.dart';
+
+import 'builder.dart' as builder;
+import 'src/source_map_span.dart';
+import 'src/utils.dart';
+import 'src/vlq.dart';
+
+/// Parses a source map directly from a json string.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+// TODO(sigmund): evaluate whether other maps should have the json parsed, or
+// the string represenation.
+// TODO(tjblasi): Ignore the first line of [jsonMap] if the JSON safety string
+// `)]}'` begins the string representation of the map.
+Mapping parse(String jsonMap,
+        {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) =>
+    parseJson(jsonDecode(jsonMap) as Map, otherMaps: otherMaps, mapUrl: mapUrl);
+
+/// Parses a source map or source map bundle directly from a json string.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseExtended(String jsonMap,
+        {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) =>
+    parseJsonExtended(jsonDecode(jsonMap),
+        otherMaps: otherMaps, mapUrl: mapUrl);
+
+/// Parses a source map or source map bundle.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseJsonExtended(/*List|Map*/ Object? json,
+    {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) {
+  if (json is List) {
+    return MappingBundle.fromJson(json, mapUrl: mapUrl);
+  }
+  return parseJson(json as Map);
+}
+
+/// Parses a source map.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseJson(Map map,
+    {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) {
+  if (map['version'] != 3) {
+    throw ArgumentError('unexpected source map version: ${map["version"]}. '
+        'Only version 3 is supported.');
+  }
+
+  if (map.containsKey('sections')) {
+    if (map.containsKey('mappings') ||
+        map.containsKey('sources') ||
+        map.containsKey('names')) {
+      throw const FormatException('map containing "sections" '
+          'cannot contain "mappings", "sources", or "names".');
+    }
+    return MultiSectionMapping.fromJson(map['sections'] as List, otherMaps,
+        mapUrl: mapUrl);
+  }
+  return SingleMapping.fromJson(map.cast<String, dynamic>(), mapUrl: mapUrl);
+}
+
+/// A mapping parsed out of a source map.
+abstract class Mapping {
+  /// Returns the span associated with [line] and [column].
+  ///
+  /// [uri] is the optional location of the output file to find the span for
+  /// to disambiguate cases where a mapping may have different mappings for
+  /// different output files.
+  SourceMapSpan? spanFor(int line, int column,
+      {Map<String, SourceFile>? files, String? uri});
+
+  /// Returns the span associated with [location].
+  SourceMapSpan? spanForLocation(SourceLocation location,
+      {Map<String, SourceFile>? files}) {
+    return spanFor(location.line, location.column,
+        uri: location.sourceUrl?.toString(), files: files);
+  }
+}
+
+/// A meta-level map containing sections.
+class MultiSectionMapping extends Mapping {
+  /// For each section, the start line offset.
+  final List<int> _lineStart = <int>[];
+
+  /// For each section, the start column offset.
+  final List<int> _columnStart = <int>[];
+
+  /// For each section, the actual source map information, which is not adjusted
+  /// for offsets.
+  final List<Mapping> _maps = <Mapping>[];
+
+  /// Creates a section mapping from json.
+  MultiSectionMapping.fromJson(List sections, Map<String, Map>? otherMaps,
+      {/*String|Uri*/ Object? mapUrl}) {
+    for (var section in sections.cast<Map>()) {
+      var offset = section['offset'] as Map?;
+      if (offset == null) throw const FormatException('section missing offset');
+
+      var line = offset['line'] as int?;
+      if (line == null) throw const FormatException('offset missing line');
+
+      var column = offset['column'] as int?;
+      if (column == null) throw const FormatException('offset missing column');
+
+      _lineStart.add(line);
+      _columnStart.add(column);
+
+      var url = section['url'] as String?;
+      var map = section['map'] as Map?;
+
+      if (url != null && map != null) {
+        throw const FormatException(
+            "section can't use both url and map entries");
+      } else if (url != null) {
+        var other = otherMaps?[url];
+        if (otherMaps == null || other == null) {
+          throw FormatException(
+              'section contains refers to $url, but no map was '
+              'given for it. Make sure a map is passed in "otherMaps"');
+        }
+        _maps.add(parseJson(other, otherMaps: otherMaps, mapUrl: url));
+      } else if (map != null) {
+        _maps.add(parseJson(map, otherMaps: otherMaps, mapUrl: mapUrl));
+      } else {
+        throw const FormatException('section missing url or map');
+      }
+    }
+    if (_lineStart.isEmpty) {
+      throw const FormatException('expected at least one section');
+    }
+  }
+
+  int _indexFor(int line, int column) {
+    for (var i = 0; i < _lineStart.length; i++) {
+      if (line < _lineStart[i]) return i - 1;
+      if (line == _lineStart[i] && column < _columnStart[i]) return i - 1;
+    }
+    return _lineStart.length - 1;
+  }
+
+  @override
+  SourceMapSpan? spanFor(int line, int column,
+      {Map<String, SourceFile>? files, String? uri}) {
+    // TODO(jacobr): perhaps verify that targetUrl matches the actual uri
+    // or at least ends in the same file name.
+    var index = _indexFor(line, column);
+    return _maps[index].spanFor(
+        line - _lineStart[index], column - _columnStart[index],
+        files: files);
+  }
+
+  @override
+  String toString() {
+    var buff = StringBuffer('$runtimeType : [');
+    for (var i = 0; i < _lineStart.length; i++) {
+      buff
+        ..write('(')
+        ..write(_lineStart[i])
+        ..write(',')
+        ..write(_columnStart[i])
+        ..write(':')
+        ..write(_maps[i])
+        ..write(')');
+    }
+    buff.write(']');
+    return buff.toString();
+  }
+}
+
+class MappingBundle extends Mapping {
+  final Map<String, SingleMapping> _mappings = {};
+
+  MappingBundle();
+
+  MappingBundle.fromJson(List json, {/*String|Uri*/ Object? mapUrl}) {
+    for (var map in json) {
+      addMapping(parseJson(map as Map, mapUrl: mapUrl) as SingleMapping);
+    }
+  }
+
+  void addMapping(SingleMapping mapping) {
+    // TODO(jacobr): verify that targetUrl is valid uri instead of a windows
+    // path.
+    // TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227
+    var targetUrl = ArgumentError.checkNotNull<String>(
+        mapping.targetUrl, 'mapping.targetUrl');
+    _mappings[targetUrl] = mapping;
+  }
+
+  /// Encodes the Mapping mappings as a json map.
+  List toJson() => _mappings.values.map((v) => v.toJson()).toList();
+
+  @override
+  String toString() {
+    var buff = StringBuffer();
+    for (var map in _mappings.values) {
+      buff.write(map.toString());
+    }
+    return buff.toString();
+  }
+
+  bool containsMapping(String url) => _mappings.containsKey(url);
+
+  @override
+  SourceMapSpan? spanFor(int line, int column,
+      {Map<String, SourceFile>? files, String? uri}) {
+    // TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227
+    uri = ArgumentError.checkNotNull<String>(uri, 'uri');
+
+    // Find the longest suffix of the uri that matches the sourcemap
+    // where the suffix starts after a path segment boundary.
+    // We consider ":" and "/" as path segment boundaries so that
+    // "package:" uris can be handled with minimal special casing. Having a
+    // few false positive path segment boundaries is not a significant issue
+    // as we prefer the longest matching prefix.
+    // Using package:path `path.split` to find path segment boundaries would
+    // not generate all of the path segment boundaries we want for "package:"
+    // urls as "package:package_name" would be one path segment when we want
+    // "package" and "package_name" to be sepearate path segments.
+
+    var onBoundary = true;
+    var separatorCodeUnits = ['/'.codeUnitAt(0), ':'.codeUnitAt(0)];
+    for (var i = 0; i < uri.length; ++i) {
+      if (onBoundary) {
+        var candidate = uri.substring(i);
+        var candidateMapping = _mappings[candidate];
+        if (candidateMapping != null) {
+          return candidateMapping.spanFor(line, column,
+              files: files, uri: candidate);
+        }
+      }
+      onBoundary = separatorCodeUnits.contains(uri.codeUnitAt(i));
+    }
+
+    // Note: when there is no source map for an uri, this behaves like an
+    // identity function, returning the requested location as the result.
+
+    // Create a mock offset for the output location. We compute it in terms
+    // of the input line and column to minimize the chances that two different
+    // line and column locations are mapped to the same offset.
+    var offset = line * 1000000 + column;
+    var location = SourceLocation(offset,
+        line: line, column: column, sourceUrl: Uri.parse(uri));
+    return SourceMapSpan(location, location, '');
+  }
+}
+
+/// A map containing direct source mappings.
+class SingleMapping extends Mapping {
+  /// Source urls used in the mapping, indexed by id.
+  final List<String> urls;
+
+  /// Source names used in the mapping, indexed by id.
+  final List<String> names;
+
+  /// The [SourceFile]s to which the entries in [lines] refer.
+  ///
+  /// This is in the same order as [urls]. If this was constructed using
+  /// [SingleMapping.fromEntries], this contains files from any [FileLocation]s
+  /// used to build the mapping. If it was parsed from JSON, it contains files
+  /// for any sources whose contents were provided via the `"sourcesContent"`
+  /// field.
+  ///
+  /// Files whose contents aren't available are `null`.
+  final List<SourceFile?> files;
+
+  /// Entries indicating the beginning of each span.
+  final List<TargetLineEntry> lines;
+
+  /// Url of the target file.
+  String? targetUrl;
+
+  /// Source root prepended to all entries in [urls].
+  String? sourceRoot;
+
+  final Uri? _mapUrl;
+
+  final Map<String, dynamic> extensions;
+
+  SingleMapping._(this.targetUrl, this.files, this.urls, this.names, this.lines)
+      : _mapUrl = null,
+        extensions = {};
+
+  factory SingleMapping.fromEntries(Iterable<builder.Entry> entries,
+      [String? fileUrl]) {
+    // The entries needs to be sorted by the target offsets.
+    var sourceEntries = entries.toList()..sort();
+    var lines = <TargetLineEntry>[];
+
+    // Indices associated with file urls that will be part of the source map. We
+    // rely on map order so that `urls.keys[urls[u]] == u`
+    var urls = <String, int>{};
+
+    // Indices associated with identifiers that will be part of the source map.
+    // We rely on map order so that `names.keys[names[n]] == n`
+    var names = <String, int>{};
+
+    /// The file for each URL, indexed by [urls]' values.
+    var files = <int, SourceFile>{};
+
+    int? lineNum;
+    late List<TargetEntry> targetEntries;
+    for (var sourceEntry in sourceEntries) {
+      if (lineNum == null || sourceEntry.target.line > lineNum) {
+        lineNum = sourceEntry.target.line;
+        targetEntries = <TargetEntry>[];
+        lines.add(TargetLineEntry(lineNum, targetEntries));
+      }
+
+      var sourceUrl = sourceEntry.source.sourceUrl;
+      var urlId = urls.putIfAbsent(
+          sourceUrl == null ? '' : sourceUrl.toString(), () => urls.length);
+
+      if (sourceEntry.source is FileLocation) {
+        files.putIfAbsent(
+            urlId, () => (sourceEntry.source as FileLocation).file);
+      }
+
+      var sourceEntryIdentifierName = sourceEntry.identifierName;
+      var srcNameId = sourceEntryIdentifierName == null
+          ? null
+          : names.putIfAbsent(sourceEntryIdentifierName, () => names.length);
+      targetEntries.add(TargetEntry(sourceEntry.target.column, urlId,
+          sourceEntry.source.line, sourceEntry.source.column, srcNameId));
+    }
+    return SingleMapping._(fileUrl, urls.values.map((i) => files[i]).toList(),
+        urls.keys.toList(), names.keys.toList(), lines);
+  }
+
+  SingleMapping.fromJson(Map<String, dynamic> map, {Object? mapUrl})
+      : targetUrl = map['file'] as String?,
+        urls = List<String>.from(map['sources'] as List),
+        names = List<String>.from((map['names'] as List?) ?? []),
+        files = List.filled((map['sources'] as List).length, null),
+        sourceRoot = map['sourceRoot'] as String?,
+        lines = <TargetLineEntry>[],
+        _mapUrl = mapUrl is String ? Uri.parse(mapUrl) : (mapUrl as Uri?),
+        extensions = {} {
+    var sourcesContent = map['sourcesContent'] == null
+        ? const <String?>[]
+        : List<String?>.from(map['sourcesContent'] as List);
+    for (var i = 0; i < urls.length && i < sourcesContent.length; i++) {
+      var source = sourcesContent[i];
+      if (source == null) continue;
+      files[i] = SourceFile.fromString(source, url: urls[i]);
+    }
+
+    var line = 0;
+    var column = 0;
+    var srcUrlId = 0;
+    var srcLine = 0;
+    var srcColumn = 0;
+    var srcNameId = 0;
+    var tokenizer = _MappingTokenizer(map['mappings'] as String);
+    var entries = <TargetEntry>[];
+
+    while (tokenizer.hasTokens) {
+      if (tokenizer.nextKind.isNewLine) {
+        if (entries.isNotEmpty) {
+          lines.add(TargetLineEntry(line, entries));
+          entries = <TargetEntry>[];
+        }
+        line++;
+        column = 0;
+        tokenizer._consumeNewLine();
+        continue;
+      }
+
+      // Decode the next entry, using the previous encountered values to
+      // decode the relative values.
+      //
+      // We expect 1, 4, or 5 values. If present, values are expected in the
+      // following order:
+      //   0: the starting column in the current line of the generated file
+      //   1: the id of the original source file
+      //   2: the starting line in the original source
+      //   3: the starting column in the original source
+      //   4: the id of the original symbol name
+      // The values are relative to the previous encountered values.
+      if (tokenizer.nextKind.isNewSegment) throw _segmentError(0, line);
+      column += tokenizer._consumeValue();
+      if (!tokenizer.nextKind.isValue) {
+        entries.add(TargetEntry(column));
+      } else {
+        srcUrlId += tokenizer._consumeValue();
+        if (srcUrlId >= urls.length) {
+          throw StateError(
+              'Invalid source url id. $targetUrl, $line, $srcUrlId');
+        }
+        if (!tokenizer.nextKind.isValue) throw _segmentError(2, line);
+        srcLine += tokenizer._consumeValue();
+        if (!tokenizer.nextKind.isValue) throw _segmentError(3, line);
+        srcColumn += tokenizer._consumeValue();
+        if (!tokenizer.nextKind.isValue) {
+          entries.add(TargetEntry(column, srcUrlId, srcLine, srcColumn));
+        } else {
+          srcNameId += tokenizer._consumeValue();
+          if (srcNameId >= names.length) {
+            throw StateError('Invalid name id: $targetUrl, $line, $srcNameId');
+          }
+          entries.add(
+              TargetEntry(column, srcUrlId, srcLine, srcColumn, srcNameId));
+        }
+      }
+      if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment();
+    }
+    if (entries.isNotEmpty) {
+      lines.add(TargetLineEntry(line, entries));
+    }
+
+    map.forEach((name, value) {
+      if (name.startsWith('x_')) extensions[name] = value;
+    });
+  }
+
+  /// Encodes the Mapping mappings as a json map.
+  ///
+  /// If [includeSourceContents] is `true`, this includes the source file
+  /// contents from [files] in the map if possible.
+  Map<String, dynamic> toJson({bool includeSourceContents = false}) {
+    var buff = StringBuffer();
+    var line = 0;
+    var column = 0;
+    var srcLine = 0;
+    var srcColumn = 0;
+    var srcUrlId = 0;
+    var srcNameId = 0;
+    var first = true;
+
+    for (var entry in lines) {
+      var nextLine = entry.line;
+      if (nextLine > line) {
+        for (var i = line; i < nextLine; ++i) {
+          buff.write(';');
+        }
+        line = nextLine;
+        column = 0;
+        first = true;
+      }
+
+      for (var segment in entry.entries) {
+        if (!first) buff.write(',');
+        first = false;
+        column = _append(buff, column, segment.column);
+
+        // Encoding can be just the column offset if there is no source
+        // information.
+        var newUrlId = segment.sourceUrlId;
+        if (newUrlId == null) continue;
+        srcUrlId = _append(buff, srcUrlId, newUrlId);
+        srcLine = _append(buff, srcLine, segment.sourceLine!);
+        srcColumn = _append(buff, srcColumn, segment.sourceColumn!);
+
+        if (segment.sourceNameId == null) continue;
+        srcNameId = _append(buff, srcNameId, segment.sourceNameId!);
+      }
+    }
+
+    var result = <String, dynamic>{
+      'version': 3,
+      'sourceRoot': sourceRoot ?? '',
+      'sources': urls,
+      'names': names,
+      'mappings': buff.toString(),
+    };
+    if (targetUrl != null) result['file'] = targetUrl!;
+
+    if (includeSourceContents) {
+      result['sourcesContent'] = files.map((file) => file?.getText(0)).toList();
+    }
+    extensions.forEach((name, value) => result[name] = value);
+
+    return result;
+  }
+
+  /// Appends to [buff] a VLQ encoding of [newValue] using the difference
+  /// between [oldValue] and [newValue]
+  static int _append(StringBuffer buff, int oldValue, int newValue) {
+    buff.writeAll(encodeVlq(newValue - oldValue));
+    return newValue;
+  }
+
+  StateError _segmentError(int seen, int line) =>
+      StateError('Invalid entry in sourcemap, expected 1, 4, or 5'
+          ' values, but got $seen.\ntargeturl: $targetUrl, line: $line');
+
+  /// Returns [TargetLineEntry] which includes the location in the target [line]
+  /// number. In particular, the resulting entry is the last entry whose line
+  /// number is lower or equal to [line].
+  TargetLineEntry? _findLine(int line) {
+    var index = binarySearch(lines, (e) => e.line > line);
+    return (index <= 0) ? null : lines[index - 1];
+  }
+
+  /// Returns [TargetEntry] which includes the location denoted by
+  /// [line], [column]. If [lineEntry] corresponds to [line], then this will be
+  /// the last entry whose column is lower or equal than [column]. If
+  /// [lineEntry] corresponds to a line prior to [line], then the result will be
+  /// the very last entry on that line.
+  TargetEntry? _findColumn(int line, int column, TargetLineEntry? lineEntry) {
+    if (lineEntry == null || lineEntry.entries.isEmpty) return null;
+    if (lineEntry.line != line) return lineEntry.entries.last;
+    var entries = lineEntry.entries;
+    var index = binarySearch(entries, (e) => e.column > column);
+    return (index <= 0) ? null : entries[index - 1];
+  }
+
+  @override
+  SourceMapSpan? spanFor(int line, int column,
+      {Map<String, SourceFile>? files, String? uri}) {
+    var entry = _findColumn(line, column, _findLine(line));
+    if (entry == null) return null;
+
+    var sourceUrlId = entry.sourceUrlId;
+    if (sourceUrlId == null) return null;
+
+    var url = urls[sourceUrlId];
+    if (sourceRoot != null) {
+      url = '$sourceRoot$url';
+    }
+
+    var sourceNameId = entry.sourceNameId;
+    var file = files?[url];
+    if (file != null) {
+      var start = file.getOffset(entry.sourceLine!, entry.sourceColumn);
+      if (sourceNameId != null) {
+        var text = names[sourceNameId];
+        return SourceMapFileSpan(file.span(start, start + text.length),
+            isIdentifier: true);
+      } else {
+        return SourceMapFileSpan(file.location(start).pointSpan());
+      }
+    } else {
+      var start = SourceLocation(0,
+          sourceUrl: _mapUrl?.resolve(url) ?? url,
+          line: entry.sourceLine,
+          column: entry.sourceColumn);
+
+      // Offset and other context is not available.
+      if (sourceNameId != null) {
+        return SourceMapSpan.identifier(start, names[sourceNameId]);
+      } else {
+        return SourceMapSpan(start, start, '');
+      }
+    }
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('$runtimeType : [')
+          ..write('targetUrl: ')
+          ..write(targetUrl)
+          ..write(', sourceRoot: ')
+          ..write(sourceRoot)
+          ..write(', urls: ')
+          ..write(urls)
+          ..write(', names: ')
+          ..write(names)
+          ..write(', lines: ')
+          ..write(lines)
+          ..write(']'))
+        .toString();
+  }
+
+  String get debugString {
+    var buff = StringBuffer();
+    for (var lineEntry in lines) {
+      var line = lineEntry.line;
+      for (var entry in lineEntry.entries) {
+        buff
+          ..write(targetUrl)
+          ..write(': ')
+          ..write(line)
+          ..write(':')
+          ..write(entry.column);
+        var sourceUrlId = entry.sourceUrlId;
+        if (sourceUrlId != null) {
+          buff
+            ..write('   -->   ')
+            ..write(sourceRoot)
+            ..write(urls[sourceUrlId])
+            ..write(': ')
+            ..write(entry.sourceLine)
+            ..write(':')
+            ..write(entry.sourceColumn);
+        }
+        var sourceNameId = entry.sourceNameId;
+        if (sourceNameId != null) {
+          buff
+            ..write(' (')
+            ..write(names[sourceNameId])
+            ..write(')');
+        }
+        buff.write('\n');
+      }
+    }
+    return buff.toString();
+  }
+}
+
+/// A line entry read from a source map.
+class TargetLineEntry {
+  final int line;
+  List<TargetEntry> entries;
+  TargetLineEntry(this.line, this.entries);
+
+  @override
+  String toString() => '$runtimeType: $line $entries';
+}
+
+/// A target segment entry read from a source map
+class TargetEntry {
+  final int column;
+  final int? sourceUrlId;
+  final int? sourceLine;
+  final int? sourceColumn;
+  final int? sourceNameId;
+
+  TargetEntry(this.column,
+      [this.sourceUrlId,
+      this.sourceLine,
+      this.sourceColumn,
+      this.sourceNameId]);
+
+  @override
+  String toString() => '$runtimeType: '
+      '($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)';
+}
+
+/// A character iterator over a string that can peek one character ahead.
+class _MappingTokenizer implements Iterator<String> {
+  final String _internal;
+  final int _length;
+  int index = -1;
+  _MappingTokenizer(String internal)
+      : _internal = internal,
+        _length = internal.length;
+
+  // Iterator API is used by decodeVlq to consume VLQ entries.
+  @override
+  bool moveNext() => ++index < _length;
+
+  @override
+  String get current => (index >= 0 && index < _length)
+      ? _internal[index]
+      : throw RangeError.index(index, _internal);
+
+  bool get hasTokens => index < _length - 1 && _length > 0;
+
+  _TokenKind get nextKind {
+    if (!hasTokens) return _TokenKind.eof;
+    var next = _internal[index + 1];
+    if (next == ';') return _TokenKind.line;
+    if (next == ',') return _TokenKind.segment;
+    return _TokenKind.value;
+  }
+
+  int _consumeValue() => decodeVlq(this);
+  void _consumeNewLine() {
+    ++index;
+  }
+
+  void _consumeNewSegment() {
+    ++index;
+  }
+
+  // Print the state of the iterator, with colors indicating the current
+  // position.
+  @override
+  String toString() {
+    var buff = StringBuffer();
+    for (var i = 0; i < index; i++) {
+      buff.write(_internal[i]);
+    }
+    buff.write('');
+    try {
+      buff.write(current);
+      // TODO: Determine whether this try / catch can be removed.
+      // ignore: avoid_catching_errors
+    } on RangeError catch (_) {}
+    buff.write('');
+    for (var i = index + 1; i < _internal.length; i++) {
+      buff.write(_internal[i]);
+    }
+    buff.write(' ($index)');
+    return buff.toString();
+  }
+}
+
+class _TokenKind {
+  static const _TokenKind line = _TokenKind(isNewLine: true);
+  static const _TokenKind segment = _TokenKind(isNewSegment: true);
+  static const _TokenKind eof = _TokenKind(isEof: true);
+  static const _TokenKind value = _TokenKind();
+  final bool isNewLine;
+  final bool isNewSegment;
+  final bool isEof;
+  bool get isValue => !isNewLine && !isNewSegment && !isEof;
+
+  const _TokenKind(
+      {this.isNewLine = false, this.isNewSegment = false, this.isEof = false});
+}
diff --git a/pkgs/source_maps/lib/printer.dart b/pkgs/source_maps/lib/printer.dart
new file mode 100644
index 0000000..32523d6
--- /dev/null
+++ b/pkgs/source_maps/lib/printer.dart
@@ -0,0 +1,262 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains a code printer that generates code by recording the source maps.
+library;
+
+import 'package:source_span/source_span.dart';
+
+import 'builder.dart';
+import 'src/source_map_span.dart';
+import 'src/utils.dart';
+
+/// A simple printer that keeps track of offset locations and records source
+/// maps locations.
+class Printer {
+  final String filename;
+  final StringBuffer _buff = StringBuffer();
+  final SourceMapBuilder _maps = SourceMapBuilder();
+  String get text => _buff.toString();
+  String get map => _maps.toJson(filename);
+
+  /// Current source location mapping.
+  SourceLocation? _loc;
+
+  /// Current line in the buffer;
+  int _line = 0;
+
+  /// Current column in the buffer.
+  int _column = 0;
+
+  Printer(this.filename);
+
+  /// Add [str] contents to the output, tracking new lines to track correct
+  /// positions for span locations. When [projectMarks] is true, this method
+  /// adds a source map location on each new line, projecting that every new
+  /// line in the target file (printed here) corresponds to a new line in the
+  /// source file.
+  void add(String str, {bool projectMarks = false}) {
+    var chars = str.runes.toList();
+    var length = chars.length;
+    for (var i = 0; i < length; i++) {
+      var c = chars[i];
+      if (c == lineFeed ||
+          (c == carriageReturn &&
+              (i + 1 == length || chars[i + 1] != lineFeed))) {
+        // Return not followed by line-feed is treated as a new line.
+        _line++;
+        _column = 0;
+        {
+          // **Warning**: Any calls to `mark` will change the value of `_loc`,
+          // so this local variable is no longer up to date after that point.
+          //
+          // This is why it has been put inside its own block to limit the
+          // scope in which it is available.
+          var loc = _loc;
+          if (projectMarks && loc != null) {
+            if (loc is FileLocation) {
+              var file = loc.file;
+              mark(file.location(file.getOffset(loc.line + 1)));
+            } else {
+              mark(SourceLocation(0,
+                  sourceUrl: loc.sourceUrl, line: loc.line + 1, column: 0));
+            }
+          }
+        }
+      } else {
+        _column++;
+      }
+    }
+    _buff.write(str);
+  }
+
+  /// Append a [total] number of spaces in the target file. Typically used for
+  /// formatting indentation.
+  void addSpaces(int total) {
+    for (var i = 0; i < total; i++) {
+      _buff.write(' ');
+    }
+    _column += total;
+  }
+
+  /// Marks that the current point in the target file corresponds to the [mark]
+  /// in the source file, which can be either a [SourceLocation] or a
+  /// [SourceSpan]. When the mark is a [SourceMapSpan] with `isIdentifier` set,
+  /// this also records the name of the identifier in the source map
+  /// information.
+  void mark(Object mark) {
+    late final SourceLocation loc;
+    String? identifier;
+    if (mark is SourceLocation) {
+      loc = mark;
+    } else if (mark is SourceSpan) {
+      loc = mark.start;
+      if (mark is SourceMapSpan && mark.isIdentifier) identifier = mark.text;
+    }
+    _maps.addLocation(loc,
+        SourceLocation(_buff.length, line: _line, column: _column), identifier);
+    _loc = loc;
+  }
+}
+
+/// A more advanced printer that keeps track of offset locations to record
+/// source maps, but additionally allows nesting of different kind of items,
+/// including [NestedPrinter]s, and it let's you automatically indent text.
+///
+/// This class is especially useful when doing code generation, where different
+/// pieces of the code are generated independently on separate printers, and are
+/// finally put together in the end.
+class NestedPrinter implements NestedItem {
+  /// Items recoded by this printer, which can be [String] literals,
+  /// [NestedItem]s, and source map information like [SourceLocation] and
+  /// [SourceSpan].
+  final List<Object> _items = [];
+
+  /// Internal buffer to merge consecutive strings added to this printer.
+  StringBuffer? _buff;
+
+  /// Current indentation, which can be updated from outside this class.
+  int indent;
+
+  /// [Printer] used during the last call to [build], if any.
+  Printer? printer;
+
+  /// Returns the text produced after calling [build].
+  String? get text => printer?.text;
+
+  /// Returns the source-map information produced after calling [build].
+  String? get map => printer?.map;
+
+  /// Item used to indicate that the following item is copied from the original
+  /// source code, and hence we should preserve source-maps on every new line.
+  static final _original = Object();
+
+  NestedPrinter([this.indent = 0]);
+
+  /// Adds [object] to this printer. [object] can be a [String],
+  /// [NestedPrinter], or anything implementing [NestedItem]. If [object] is a
+  /// [String], the value is appended directly, without doing any formatting
+  /// changes. If you wish to add a line of code with automatic indentation, use
+  /// [addLine] instead.  [NestedPrinter]s and [NestedItem]s are not processed
+  /// until [build] gets called later on. We ensure that [build] emits every
+  /// object in the order that they were added to this printer.
+  ///
+  /// The [location] and [span] parameters indicate the corresponding source map
+  /// location of [object] in the original input. Only one, [location] or
+  /// [span], should be provided at a time.
+  ///
+  /// Indicate [isOriginal] when [object] is copied directly from the user code.
+  /// Setting [isOriginal] will make this printer propagate source map locations
+  /// on every line-break.
+  void add(Object object,
+      {SourceLocation? location, SourceSpan? span, bool isOriginal = false}) {
+    if (object is! String || location != null || span != null || isOriginal) {
+      _flush();
+      assert(location == null || span == null);
+      if (location != null) _items.add(location);
+      if (span != null) _items.add(span);
+      if (isOriginal) _items.add(_original);
+    }
+
+    if (object is String) {
+      _appendString(object);
+    } else {
+      _items.add(object);
+    }
+  }
+
+  /// Append `2 * indent` spaces to this printer.
+  void insertIndent() => _indent(indent);
+
+  /// Add a [line], autoindenting to the current value of [indent]. Note,
+  /// indentation is not inferred from the contents added to this printer. If a
+  /// line starts or ends an indentation block, you need to also update [indent]
+  /// accordingly. Also, indentation is not adapted for nested printers. If
+  /// you add a [NestedPrinter] to this printer, its indentation is set
+  /// separately and will not include any the indentation set here.
+  ///
+  /// The [location] and [span] parameters indicate the corresponding source map
+  /// location of [line] in the original input. Only one, [location] or
+  /// [span], should be provided at a time.
+  void addLine(String? line, {SourceLocation? location, SourceSpan? span}) {
+    if (location != null || span != null) {
+      _flush();
+      assert(location == null || span == null);
+      if (location != null) _items.add(location);
+      if (span != null) _items.add(span);
+    }
+    if (line == null) return;
+    if (line != '') {
+      // We don't indent empty lines.
+      _indent(indent);
+      _appendString(line);
+    }
+    _appendString('\n');
+  }
+
+  /// Appends a string merging it with any previous strings, if possible.
+  void _appendString(String s) {
+    var buf = _buff ??= StringBuffer();
+    buf.write(s);
+  }
+
+  /// Adds all of the current [_buff] contents as a string item.
+  void _flush() {
+    if (_buff != null) {
+      _items.add(_buff.toString());
+      _buff = null;
+    }
+  }
+
+  void _indent(int indent) {
+    for (var i = 0; i < indent; i++) {
+      _appendString('  ');
+    }
+  }
+
+  /// Returns a string representation of all the contents appended to this
+  /// printer, including source map location tokens.
+  @override
+  String toString() {
+    _flush();
+    return (StringBuffer()..writeAll(_items)).toString();
+  }
+
+  /// Builds the output of this printer and source map information. After
+  /// calling this function, you can use [text] and [map] to retrieve the
+  /// geenrated code and source map information, respectively.
+  void build(String filename) {
+    writeTo(printer = Printer(filename));
+  }
+
+  /// Implements the [NestedItem] interface.
+  @override
+  void writeTo(Printer printer) {
+    _flush();
+    var propagate = false;
+    for (var item in _items) {
+      if (item is NestedItem) {
+        item.writeTo(printer);
+      } else if (item is String) {
+        printer.add(item, projectMarks: propagate);
+        propagate = false;
+      } else if (item is SourceLocation || item is SourceSpan) {
+        printer.mark(item);
+      } else if (item == _original) {
+        // we insert booleans when we are about to quote text that was copied
+        // from the original source. In such case, we will propagate marks on
+        // every new-line.
+        propagate = true;
+      } else {
+        throw UnsupportedError('Unknown item type: $item');
+      }
+    }
+  }
+}
+
+/// An item added to a [NestedPrinter].
+abstract class NestedItem {
+  /// Write the contents of this item into [printer].
+  void writeTo(Printer printer);
+}
diff --git a/pkgs/source_maps/lib/refactor.dart b/pkgs/source_maps/lib/refactor.dart
new file mode 100644
index 0000000..a518a0c
--- /dev/null
+++ b/pkgs/source_maps/lib/refactor.dart
@@ -0,0 +1,140 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Tools to help implement refactoring like transformations to Dart code.
+///
+/// [TextEditTransaction] supports making a series of changes to a text buffer.
+/// [guessIndent] helps to guess the appropriate indentiation for the new code.
+library;
+
+import 'package:source_span/source_span.dart';
+
+import 'printer.dart';
+import 'src/utils.dart';
+
+/// Editable text transaction.
+///
+/// Applies a series of edits using original location
+/// information, and composes them into the edited string.
+class TextEditTransaction {
+  final SourceFile? file;
+  final String original;
+  final _edits = <_TextEdit>[];
+
+  /// Creates a new transaction.
+  TextEditTransaction(this.original, this.file);
+
+  bool get hasEdits => _edits.isNotEmpty;
+
+  /// Edit the original text, replacing text on the range [begin] and [end]
+  /// with the [replacement]. [replacement] can be either a string or a
+  /// [NestedPrinter].
+  void edit(int begin, int end, Object replacement) {
+    _edits.add(_TextEdit(begin, end, replacement));
+  }
+
+  /// Create a source map [SourceLocation] for [offset], if [file] is not
+  /// `null`.
+  SourceLocation? _loc(int offset) => file?.location(offset);
+
+  /// Applies all pending [edit]s and returns a [NestedPrinter] containing the
+  /// rewritten string and source map information. [file]`.location` is given to
+  /// the underlying printer to indicate the name of the generated file that
+  /// will contains the source map information.
+  ///
+  /// Throws [UnsupportedError] if the edits were overlapping. If no edits were
+  /// made, the printer simply contains the original string.
+  NestedPrinter commit() {
+    var printer = NestedPrinter();
+    if (_edits.isEmpty) {
+      return printer..add(original, location: _loc(0), isOriginal: true);
+    }
+
+    // Sort edits by start location.
+    _edits.sort();
+
+    var consumed = 0;
+    for (var edit in _edits) {
+      if (consumed > edit.begin) {
+        var sb = StringBuffer();
+        sb
+          ..write(file?.location(edit.begin).toolString)
+          ..write(': overlapping edits. Insert at offset ')
+          ..write(edit.begin)
+          ..write(' but have consumed ')
+          ..write(consumed)
+          ..write(' input characters. List of edits:');
+        for (var e in _edits) {
+          sb
+            ..write('\n    ')
+            ..write(e);
+        }
+        throw UnsupportedError(sb.toString());
+      }
+
+      // Add characters from the original string between this edit and the last
+      // one, if any.
+      var betweenEdits = original.substring(consumed, edit.begin);
+      printer
+        ..add(betweenEdits, location: _loc(consumed), isOriginal: true)
+        ..add(edit.replace, location: _loc(edit.begin));
+      consumed = edit.end;
+    }
+
+    // Add any text from the end of the original string that was not replaced.
+    printer.add(original.substring(consumed),
+        location: _loc(consumed), isOriginal: true);
+    return printer;
+  }
+}
+
+class _TextEdit implements Comparable<_TextEdit> {
+  final int begin;
+  final int end;
+
+  /// The replacement used by the edit, can be a string or a [NestedPrinter].
+  final Object replace;
+
+  _TextEdit(this.begin, this.end, this.replace);
+
+  int get length => end - begin;
+
+  @override
+  String toString() => '(Edit @ $begin,$end: "$replace")';
+
+  @override
+  int compareTo(_TextEdit other) {
+    var diff = begin - other.begin;
+    if (diff != 0) return diff;
+    return end - other.end;
+  }
+}
+
+/// Returns all whitespace characters at the start of [charOffset]'s line.
+String guessIndent(String code, int charOffset) {
+  // Find the beginning of the line
+  var lineStart = 0;
+  for (var i = charOffset - 1; i >= 0; i--) {
+    var c = code.codeUnitAt(i);
+    if (c == lineFeed || c == carriageReturn) {
+      lineStart = i + 1;
+      break;
+    }
+  }
+
+  // Grab all the whitespace
+  var whitespaceEnd = code.length;
+  for (var i = lineStart; i < code.length; i++) {
+    var c = code.codeUnitAt(i);
+    if (c != _space && c != _tab) {
+      whitespaceEnd = i;
+      break;
+    }
+  }
+
+  return code.substring(lineStart, whitespaceEnd);
+}
+
+const int _tab = 9;
+const int _space = 32;
diff --git a/pkgs/source_maps/lib/source_maps.dart b/pkgs/source_maps/lib/source_maps.dart
new file mode 100644
index 0000000..244dee7
--- /dev/null
+++ b/pkgs/source_maps/lib/source_maps.dart
@@ -0,0 +1,38 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Library to create and parse source maps.
+///
+/// Create a source map using [SourceMapBuilder]. For example:
+///
+/// ```dart
+/// var json = (new SourceMapBuilder()
+///     ..add(inputSpan1, outputSpan1)
+///     ..add(inputSpan2, outputSpan2)
+///     ..add(inputSpan3, outputSpan3)
+///     .toJson(outputFile);
+/// ```
+///
+/// Use the source_span package's [SourceSpan] and [SourceFile] classes to
+/// specify span locations.
+///
+/// Parse a source map using [parse], and call `spanFor` on the returned mapping
+/// object. For example:
+///
+/// ```dart
+/// var mapping = parse(json);
+/// mapping.spanFor(outputSpan1.line, outputSpan1.column)
+/// ```
+library;
+
+import 'package:source_span/source_span.dart';
+
+import 'builder.dart';
+import 'parser.dart';
+
+export 'builder.dart';
+export 'parser.dart';
+export 'printer.dart';
+export 'refactor.dart';
+export 'src/source_map_span.dart';
diff --git a/pkgs/source_maps/lib/src/source_map_span.dart b/pkgs/source_maps/lib/src/source_map_span.dart
new file mode 100644
index 0000000..aad8a32
--- /dev/null
+++ b/pkgs/source_maps/lib/src/source_map_span.dart
@@ -0,0 +1,72 @@
+// 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:source_span/source_span.dart';
+
+/// A [SourceSpan] for spans coming from or being written to source maps.
+///
+/// These spans have an extra piece of metadata: whether or not they represent
+/// an identifier (see [isIdentifier]).
+class SourceMapSpan extends SourceSpanBase {
+  /// Whether this span represents an identifier.
+  ///
+  /// If this is `true`, [text] is the value of the identifier.
+  final bool isIdentifier;
+
+  SourceMapSpan(super.start, super.end, super.text,
+      {this.isIdentifier = false});
+
+  /// Creates a [SourceMapSpan] for an identifier with value [text] starting at
+  /// [start].
+  ///
+  /// The [end] location is determined by adding [text] to [start].
+  SourceMapSpan.identifier(SourceLocation start, String text)
+      : this(
+            start,
+            SourceLocation(start.offset + text.length,
+                sourceUrl: start.sourceUrl,
+                line: start.line,
+                column: start.column + text.length),
+            text,
+            isIdentifier: true);
+}
+
+/// A wrapper aruond a [FileSpan] that implements [SourceMapSpan].
+class SourceMapFileSpan implements SourceMapSpan, FileSpan {
+  final FileSpan _inner;
+  @override
+  final bool isIdentifier;
+
+  @override
+  SourceFile get file => _inner.file;
+  @override
+  FileLocation get start => _inner.start;
+  @override
+  FileLocation get end => _inner.end;
+  @override
+  String get text => _inner.text;
+  @override
+  String get context => _inner.context;
+  @override
+  Uri? get sourceUrl => _inner.sourceUrl;
+  @override
+  int get length => _inner.length;
+
+  SourceMapFileSpan(this._inner, {this.isIdentifier = false});
+
+  @override
+  int compareTo(SourceSpan other) => _inner.compareTo(other);
+  @override
+  String highlight({Object? color}) => _inner.highlight(color: color);
+  @override
+  SourceSpan union(SourceSpan other) => _inner.union(other);
+  @override
+  FileSpan expand(FileSpan other) => _inner.expand(other);
+  @override
+  String message(String message, {Object? color}) =>
+      _inner.message(message, color: color);
+  @override
+  String toString() =>
+      _inner.toString().replaceAll('FileSpan', 'SourceMapFileSpan');
+}
diff --git a/pkgs/source_maps/lib/src/utils.dart b/pkgs/source_maps/lib/src/utils.dart
new file mode 100644
index 0000000..ba04fbb
--- /dev/null
+++ b/pkgs/source_maps/lib/src/utils.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Utilities that shouldn't be in this package.
+library;
+
+/// Find the first entry in a sorted [list] that matches a monotonic predicate.
+/// Given a result `n`, that all items before `n` will not match, `n` matches,
+/// and all items after `n` match too. The result is -1 when there are no
+/// items, 0 when all items match, and list.length when none does.
+// TODO(sigmund): remove this function after dartbug.com/5624 is fixed.
+int binarySearch<T>(List<T> list, bool Function(T) matches) {
+  if (list.isEmpty) return -1;
+  if (matches(list.first)) return 0;
+  if (!matches(list.last)) return list.length;
+
+  var min = 0;
+  var max = list.length - 1;
+  while (min < max) {
+    var half = min + ((max - min) ~/ 2);
+    if (matches(list[half])) {
+      max = half;
+    } else {
+      min = half + 1;
+    }
+  }
+  return max;
+}
+
+const int lineFeed = 10;
+const int carriageReturn = 13;
diff --git a/pkgs/source_maps/lib/src/vlq.dart b/pkgs/source_maps/lib/src/vlq.dart
new file mode 100644
index 0000000..3b0562d
--- /dev/null
+++ b/pkgs/source_maps/lib/src/vlq.dart
@@ -0,0 +1,101 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Utilities to encode and decode VLQ values used in source maps.
+///
+/// Sourcemaps are encoded with variable length numbers as base64 encoded
+/// strings with the least significant digit coming first. Each base64 digit
+/// encodes a 5-bit value (0-31) and a continuation bit. Signed values can be
+/// represented by using the least significant bit of the value as the sign bit.
+///
+/// For more details see the source map [version 3 documentation](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?usp=sharing).
+library;
+
+import 'dart:math';
+
+const int vlqBaseShift = 5;
+
+const int vlqBaseMask = (1 << 5) - 1;
+
+const int vlqContinuationBit = 1 << 5;
+
+const int vlqContinuationMask = 1 << 5;
+
+const String base64Digits =
+    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+
+final Map<String, int> _digits = () {
+  var map = <String, int>{};
+  for (var i = 0; i < 64; i++) {
+    map[base64Digits[i]] = i;
+  }
+  return map;
+}();
+
+final int maxInt32 = (pow(2, 31) as int) - 1;
+final int minInt32 = -(pow(2, 31) as int);
+
+/// Creates the VLQ encoding of [value] as a sequence of characters
+Iterable<String> encodeVlq(int value) {
+  if (value < minInt32 || value > maxInt32) {
+    throw ArgumentError('expected 32 bit int, got: $value');
+  }
+  var res = <String>[];
+  var signBit = 0;
+  if (value < 0) {
+    signBit = 1;
+    value = -value;
+  }
+  value = (value << 1) | signBit;
+  do {
+    var digit = value & vlqBaseMask;
+    value >>= vlqBaseShift;
+    if (value > 0) {
+      digit |= vlqContinuationBit;
+    }
+    res.add(base64Digits[digit]);
+  } while (value > 0);
+  return res;
+}
+
+/// Decodes a value written as a sequence of VLQ characters. The first input
+/// character will be `chars.current` after calling `chars.moveNext` once. The
+/// iterator is advanced until a stop character is found (a character without
+/// the [vlqContinuationBit]).
+int decodeVlq(Iterator<String> chars) {
+  var result = 0;
+  var stop = false;
+  var shift = 0;
+  while (!stop) {
+    if (!chars.moveNext()) throw StateError('incomplete VLQ value');
+    var char = chars.current;
+    var digit = _digits[char];
+    if (digit == null) {
+      throw FormatException('invalid character in VLQ encoding: $char');
+    }
+    stop = (digit & vlqContinuationBit) == 0;
+    digit &= vlqBaseMask;
+    result += digit << shift;
+    shift += vlqBaseShift;
+  }
+
+  // Result uses the least significant bit as a sign bit. We convert it into a
+  // two-complement value. For example,
+  //   2 (10 binary) becomes 1
+  //   3 (11 binary) becomes -1
+  //   4 (100 binary) becomes 2
+  //   5 (101 binary) becomes -2
+  //   6 (110 binary) becomes 3
+  //   7 (111 binary) becomes -3
+  var negate = (result & 1) == 1;
+  result = result >> 1;
+  result = negate ? -result : result;
+
+  // TODO(sigmund): can we detect this earlier?
+  if (result < minInt32 || result > maxInt32) {
+    throw FormatException(
+        'expected an encoded 32 bit int, but we got: $result');
+  }
+  return result;
+}
diff --git a/pkgs/source_maps/pubspec.yaml b/pkgs/source_maps/pubspec.yaml
new file mode 100644
index 0000000..32cbf4f
--- /dev/null
+++ b/pkgs/source_maps/pubspec.yaml
@@ -0,0 +1,15 @@
+name: source_maps
+version: 0.10.14-wip
+description: A library to programmatically manipulate source map files.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/source_maps
+
+environment:
+  sdk: ^3.3.0
+
+dependencies:
+  source_span: ^1.8.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  term_glyph: ^1.2.0
+  test: ^1.16.0
diff --git a/pkgs/source_maps/test/builder_test.dart b/pkgs/source_maps/test/builder_test.dart
new file mode 100644
index 0000000..4f773e7
--- /dev/null
+++ b/pkgs/source_maps/test/builder_test.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:source_maps/source_maps.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+void main() {
+  test('builder - with span', () {
+    var map = (SourceMapBuilder()
+          ..addSpan(inputVar1, outputVar1)
+          ..addSpan(inputFunction, outputFunction)
+          ..addSpan(inputVar2, outputVar2)
+          ..addSpan(inputExpr, outputExpr))
+        .build(output.url.toString());
+    expect(map, equals(expectedMap));
+  });
+
+  test('builder - with location', () {
+    var str = (SourceMapBuilder()
+          ..addLocation(inputVar1.start, outputVar1.start, 'longVar1')
+          ..addLocation(inputFunction.start, outputFunction.start, 'longName')
+          ..addLocation(inputVar2.start, outputVar2.start, 'longVar2')
+          ..addLocation(inputExpr.start, outputExpr.start, null))
+        .toJson(output.url.toString());
+    expect(str, jsonEncode(expectedMap));
+  });
+}
diff --git a/pkgs/source_maps/test/common.dart b/pkgs/source_maps/test/common.dart
new file mode 100644
index 0000000..e225ff5
--- /dev/null
+++ b/pkgs/source_maps/test/common.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Common input/output used by builder, parser and end2end tests
+library;
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+/// Content of the source file
+const String inputContent = '''
+/** this is a comment. */
+int longVar1 = 3;
+
+// this is a comment too
+int longName(int longVar2) {
+  return longVar1 + longVar2;
+}
+''';
+final input = SourceFile.fromString(inputContent, url: 'input.dart');
+
+/// A span in the input file
+SourceMapSpan ispan(int start, int end, [bool isIdentifier = false]) =>
+    SourceMapFileSpan(input.span(start, end), isIdentifier: isIdentifier);
+
+SourceMapSpan inputVar1 = ispan(30, 38, true);
+SourceMapSpan inputFunction = ispan(74, 82, true);
+SourceMapSpan inputVar2 = ispan(87, 95, true);
+
+SourceMapSpan inputVar1NoSymbol = ispan(30, 38);
+SourceMapSpan inputFunctionNoSymbol = ispan(74, 82);
+SourceMapSpan inputVar2NoSymbol = ispan(87, 95);
+
+SourceMapSpan inputExpr = ispan(108, 127);
+
+/// Content of the target file
+const String outputContent = '''
+var x = 3;
+f(y) => x + y;
+''';
+final output = SourceFile.fromString(outputContent, url: 'output.dart');
+
+/// A span in the output file
+SourceMapSpan ospan(int start, int end, [bool isIdentifier = false]) =>
+    SourceMapFileSpan(output.span(start, end), isIdentifier: isIdentifier);
+
+SourceMapSpan outputVar1 = ospan(4, 5, true);
+SourceMapSpan outputFunction = ospan(11, 12, true);
+SourceMapSpan outputVar2 = ospan(13, 14, true);
+SourceMapSpan outputVar1NoSymbol = ospan(4, 5);
+SourceMapSpan outputFunctionNoSymbol = ospan(11, 12);
+SourceMapSpan outputVar2NoSymbol = ospan(13, 14);
+SourceMapSpan outputExpr = ospan(19, 24);
+
+/// Expected output mapping when recording the following four mappings:
+///      inputVar1       <=   outputVar1
+///      inputFunction   <=   outputFunction
+///      inputVar2       <=   outputVar2
+///      inputExpr       <=   outputExpr
+///
+/// This mapping is stored in the tests so we can independently test the builder
+/// and parser algorithms without relying entirely on end2end tests.
+const Map<String, dynamic> expectedMap = {
+  'version': 3,
+  'sourceRoot': '',
+  'sources': ['input.dart'],
+  'names': ['longVar1', 'longName', 'longVar2'],
+  'mappings': 'IACIA;AAGAC,EAAaC,MACR',
+  'file': 'output.dart'
+};
+
+void check(SourceSpan outputSpan, Mapping mapping, SourceMapSpan inputSpan,
+    bool realOffsets) {
+  var line = outputSpan.start.line;
+  var column = outputSpan.start.column;
+  var files = realOffsets ? {'input.dart': input} : null;
+  var span = mapping.spanFor(line, column, files: files)!;
+  var span2 = mapping.spanForLocation(outputSpan.start, files: files)!;
+
+  // Both mapping APIs are equivalent.
+  expect(span.start.offset, span2.start.offset);
+  expect(span.start.line, span2.start.line);
+  expect(span.start.column, span2.start.column);
+  expect(span.end.offset, span2.end.offset);
+  expect(span.end.line, span2.end.line);
+  expect(span.end.column, span2.end.column);
+
+  // Mapping matches our input location (modulo using real offsets)
+  expect(span.start.line, inputSpan.start.line);
+  expect(span.start.column, inputSpan.start.column);
+  expect(span.sourceUrl, inputSpan.sourceUrl);
+  expect(span.start.offset, realOffsets ? inputSpan.start.offset : 0);
+
+  // Mapping includes the identifier, if any
+  if (inputSpan.isIdentifier) {
+    expect(span.end.line, inputSpan.end.line);
+    expect(span.end.column, inputSpan.end.column);
+    expect(span.end.offset, span.start.offset + inputSpan.text.length);
+    if (realOffsets) expect(span.end.offset, inputSpan.end.offset);
+  } else {
+    expect(span.end.offset, span.start.offset);
+    expect(span.end.line, span.start.line);
+    expect(span.end.column, span.start.column);
+  }
+}
diff --git a/pkgs/source_maps/test/end2end_test.dart b/pkgs/source_maps/test/end2end_test.dart
new file mode 100644
index 0000000..84dd5ba
--- /dev/null
+++ b/pkgs/source_maps/test/end2end_test.dart
@@ -0,0 +1,160 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+void main() {
+  test('end-to-end setup', () {
+    expect(inputVar1.text, 'longVar1');
+    expect(inputFunction.text, 'longName');
+    expect(inputVar2.text, 'longVar2');
+    expect(inputVar1NoSymbol.text, 'longVar1');
+    expect(inputFunctionNoSymbol.text, 'longName');
+    expect(inputVar2NoSymbol.text, 'longVar2');
+    expect(inputExpr.text, 'longVar1 + longVar2');
+
+    expect(outputVar1.text, 'x');
+    expect(outputFunction.text, 'f');
+    expect(outputVar2.text, 'y');
+    expect(outputVar1NoSymbol.text, 'x');
+    expect(outputFunctionNoSymbol.text, 'f');
+    expect(outputVar2NoSymbol.text, 'y');
+    expect(outputExpr.text, 'x + y');
+  });
+
+  test('build + parse', () {
+    var map = (SourceMapBuilder()
+          ..addSpan(inputVar1, outputVar1)
+          ..addSpan(inputFunction, outputFunction)
+          ..addSpan(inputVar2, outputVar2)
+          ..addSpan(inputExpr, outputExpr))
+        .build(output.url.toString());
+    var mapping = parseJson(map);
+    check(outputVar1, mapping, inputVar1, false);
+    check(outputVar2, mapping, inputVar2, false);
+    check(outputFunction, mapping, inputFunction, false);
+    check(outputExpr, mapping, inputExpr, false);
+  });
+
+  test('build + parse - no symbols', () {
+    var map = (SourceMapBuilder()
+          ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol)
+          ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol)
+          ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol)
+          ..addSpan(inputExpr, outputExpr))
+        .build(output.url.toString());
+    var mapping = parseJson(map);
+    check(outputVar1NoSymbol, mapping, inputVar1NoSymbol, false);
+    check(outputVar2NoSymbol, mapping, inputVar2NoSymbol, false);
+    check(outputFunctionNoSymbol, mapping, inputFunctionNoSymbol, false);
+    check(outputExpr, mapping, inputExpr, false);
+  });
+
+  test('build + parse, repeated entries', () {
+    var map = (SourceMapBuilder()
+          ..addSpan(inputVar1, outputVar1)
+          ..addSpan(inputVar1, outputVar1)
+          ..addSpan(inputFunction, outputFunction)
+          ..addSpan(inputFunction, outputFunction)
+          ..addSpan(inputVar2, outputVar2)
+          ..addSpan(inputVar2, outputVar2)
+          ..addSpan(inputExpr, outputExpr)
+          ..addSpan(inputExpr, outputExpr))
+        .build(output.url.toString());
+    var mapping = parseJson(map);
+    check(outputVar1, mapping, inputVar1, false);
+    check(outputVar2, mapping, inputVar2, false);
+    check(outputFunction, mapping, inputFunction, false);
+    check(outputExpr, mapping, inputExpr, false);
+  });
+
+  test('build + parse - no symbols, repeated entries', () {
+    var map = (SourceMapBuilder()
+          ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol)
+          ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol)
+          ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol)
+          ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol)
+          ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol)
+          ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol)
+          ..addSpan(inputExpr, outputExpr))
+        .build(output.url.toString());
+    var mapping = parseJson(map);
+    check(outputVar1NoSymbol, mapping, inputVar1NoSymbol, false);
+    check(outputVar2NoSymbol, mapping, inputVar2NoSymbol, false);
+    check(outputFunctionNoSymbol, mapping, inputFunctionNoSymbol, false);
+    check(outputExpr, mapping, inputExpr, false);
+  });
+
+  test('build + parse with file', () {
+    var json = (SourceMapBuilder()
+          ..addSpan(inputVar1, outputVar1)
+          ..addSpan(inputFunction, outputFunction)
+          ..addSpan(inputVar2, outputVar2)
+          ..addSpan(inputExpr, outputExpr))
+        .toJson(output.url.toString());
+    var mapping = parse(json);
+    check(outputVar1, mapping, inputVar1, true);
+    check(outputVar2, mapping, inputVar2, true);
+    check(outputFunction, mapping, inputFunction, true);
+    check(outputExpr, mapping, inputExpr, true);
+  });
+
+  test('printer projecting marks + parse', () {
+    var out = inputContent.replaceAll('long', '_s');
+    var file = SourceFile.fromString(out, url: 'output2.dart');
+    var printer = Printer('output2.dart');
+    printer.mark(ispan(0, 0));
+
+    var segments = inputContent.split('long');
+    expect(segments.length, 6);
+    printer.add(segments[0], projectMarks: true);
+    printer.mark(inputVar1);
+    printer.add('_s');
+    printer.add(segments[1], projectMarks: true);
+    printer.mark(inputFunction);
+    printer.add('_s');
+    printer.add(segments[2], projectMarks: true);
+    printer.mark(inputVar2);
+    printer.add('_s');
+    printer.add(segments[3], projectMarks: true);
+    printer.mark(inputExpr);
+    printer.add('_s');
+    printer.add(segments[4], projectMarks: true);
+    printer.add('_s');
+    printer.add(segments[5], projectMarks: true);
+
+    expect(printer.text, out);
+
+    var mapping = parse(printer.map);
+    void checkHelper(SourceMapSpan inputSpan, int adjustment) {
+      var start = inputSpan.start.offset - adjustment;
+      var end = (inputSpan.end.offset - adjustment) - 2;
+      var span = SourceMapFileSpan(file.span(start, end),
+          isIdentifier: inputSpan.isIdentifier);
+      check(span, mapping, inputSpan, true);
+    }
+
+    checkHelper(inputVar1, 0);
+    checkHelper(inputFunction, 2);
+    checkHelper(inputVar2, 4);
+    checkHelper(inputExpr, 6);
+
+    // We projected correctly lines that have no mappings
+    check(file.span(66, 66), mapping, ispan(45, 45), true);
+    check(file.span(63, 64), mapping, ispan(45, 45), true);
+    check(file.span(68, 68), mapping, ispan(70, 70), true);
+    check(file.span(71, 71), mapping, ispan(70, 70), true);
+
+    // Start of the last line
+    var oOffset = out.length - 2;
+    var iOffset = inputContent.length - 2;
+    check(file.span(oOffset, oOffset), mapping, ispan(iOffset, iOffset), true);
+    check(file.span(oOffset + 1, oOffset + 1), mapping, ispan(iOffset, iOffset),
+        true);
+  });
+}
diff --git a/pkgs/source_maps/test/parser_test.dart b/pkgs/source_maps/test/parser_test.dart
new file mode 100644
index 0000000..6cfe928
--- /dev/null
+++ b/pkgs/source_maps/test/parser_test.dart
@@ -0,0 +1,431 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: inference_failure_on_collection_literal
+// ignore_for_file: inference_failure_on_instance_creation
+
+import 'dart:convert';
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+const Map<String, dynamic> _mapWithNoSourceLocation = {
+  'version': 3,
+  'sourceRoot': '',
+  'sources': ['input.dart'],
+  'names': [],
+  'mappings': 'A',
+  'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocation = {
+  'version': 3,
+  'sourceRoot': '',
+  'sources': ['input.dart'],
+  'names': [],
+  'mappings': 'AAAA',
+  'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndMissingNames = {
+  'version': 3,
+  'sourceRoot': '',
+  'sources': ['input.dart'],
+  'mappings': 'AAAA',
+  'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName = {
+  'version': 3,
+  'sourceRoot': '',
+  'sources': ['input.dart'],
+  'names': ['var'],
+  'mappings': 'AAAAA',
+  'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName1 = {
+  'version': 3,
+  'sourceRoot': 'pkg/',
+  'sources': ['input1.dart'],
+  'names': ['var1'],
+  'mappings': 'AAAAA',
+  'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName2 = {
+  'version': 3,
+  'sourceRoot': 'pkg/',
+  'sources': ['input2.dart'],
+  'names': ['var2'],
+  'mappings': 'AAAAA',
+  'file': 'output2.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName3 = {
+  'version': 3,
+  'sourceRoot': 'pkg/',
+  'sources': ['input3.dart'],
+  'names': ['var3'],
+  'mappings': 'AAAAA',
+  'file': '3/output.dart'
+};
+
+const _sourceMapBundle = [
+  _mapWithSourceLocationAndName1,
+  _mapWithSourceLocationAndName2,
+  _mapWithSourceLocationAndName3,
+];
+
+void main() {
+  test('parse', () {
+    var mapping = parseJson(expectedMap);
+    check(outputVar1, mapping, inputVar1, false);
+    check(outputVar2, mapping, inputVar2, false);
+    check(outputFunction, mapping, inputFunction, false);
+    check(outputExpr, mapping, inputExpr, false);
+  });
+
+  test('parse + json', () {
+    var mapping = parse(jsonEncode(expectedMap));
+    check(outputVar1, mapping, inputVar1, false);
+    check(outputVar2, mapping, inputVar2, false);
+    check(outputFunction, mapping, inputFunction, false);
+    check(outputExpr, mapping, inputExpr, false);
+  });
+
+  test('parse with file', () {
+    var mapping = parseJson(expectedMap);
+    check(outputVar1, mapping, inputVar1, true);
+    check(outputVar2, mapping, inputVar2, true);
+    check(outputFunction, mapping, inputFunction, true);
+    check(outputExpr, mapping, inputExpr, true);
+  });
+
+  test('parse with no source location', () {
+    var map = parse(jsonEncode(_mapWithNoSourceLocation)) as SingleMapping;
+    expect(map.lines.length, 1);
+    expect(map.lines.first.entries.length, 1);
+    var entry = map.lines.first.entries.first;
+
+    expect(entry.column, 0);
+    expect(entry.sourceUrlId, null);
+    expect(entry.sourceColumn, null);
+    expect(entry.sourceLine, null);
+    expect(entry.sourceNameId, null);
+  });
+
+  test('parse with source location and no name', () {
+    var map = parse(jsonEncode(_mapWithSourceLocation)) as SingleMapping;
+    expect(map.lines.length, 1);
+    expect(map.lines.first.entries.length, 1);
+    var entry = map.lines.first.entries.first;
+
+    expect(entry.column, 0);
+    expect(entry.sourceUrlId, 0);
+    expect(entry.sourceColumn, 0);
+    expect(entry.sourceLine, 0);
+    expect(entry.sourceNameId, null);
+  });
+
+  test('parse with source location and missing names entry', () {
+    var map = parse(jsonEncode(_mapWithSourceLocationAndMissingNames))
+        as SingleMapping;
+    expect(map.lines.length, 1);
+    expect(map.lines.first.entries.length, 1);
+    var entry = map.lines.first.entries.first;
+
+    expect(entry.column, 0);
+    expect(entry.sourceUrlId, 0);
+    expect(entry.sourceColumn, 0);
+    expect(entry.sourceLine, 0);
+    expect(entry.sourceNameId, null);
+  });
+
+  test('parse with source location and name', () {
+    var map = parse(jsonEncode(_mapWithSourceLocationAndName)) as SingleMapping;
+    expect(map.lines.length, 1);
+    expect(map.lines.first.entries.length, 1);
+    var entry = map.lines.first.entries.first;
+
+    expect(entry.sourceUrlId, 0);
+    expect(entry.sourceUrlId, 0);
+    expect(entry.sourceColumn, 0);
+    expect(entry.sourceLine, 0);
+    expect(entry.sourceNameId, 0);
+  });
+
+  test('parse with source root', () {
+    var inputMap = Map.from(_mapWithSourceLocation);
+    inputMap['sourceRoot'] = '/pkg/';
+    var mapping = parseJson(inputMap) as SingleMapping;
+    expect(mapping.spanFor(0, 0)?.sourceUrl, Uri.parse('/pkg/input.dart'));
+    expect(
+        mapping
+            .spanForLocation(
+                SourceLocation(0, sourceUrl: Uri.parse('ignored.dart')))
+            ?.sourceUrl,
+        Uri.parse('/pkg/input.dart'));
+
+    var newSourceRoot = '/new/';
+
+    mapping.sourceRoot = newSourceRoot;
+    inputMap['sourceRoot'] = newSourceRoot;
+
+    expect(mapping.toJson(), equals(inputMap));
+  });
+
+  test('parse with map URL', () {
+    var inputMap = Map.from(_mapWithSourceLocation);
+    inputMap['sourceRoot'] = 'pkg/';
+    var mapping = parseJson(inputMap, mapUrl: 'file:///path/to/map');
+    expect(mapping.spanFor(0, 0)?.sourceUrl,
+        Uri.parse('file:///path/to/pkg/input.dart'));
+  });
+
+  group('parse with bundle', () {
+    var mapping =
+        parseJsonExtended(_sourceMapBundle, mapUrl: 'file:///path/to/map');
+
+    test('simple', () {
+      expect(
+          mapping
+              .spanForLocation(SourceLocation(0,
+                  sourceUrl: Uri.file('/path/to/output.dart')))
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input1.dart'));
+      expect(
+          mapping
+              .spanForLocation(SourceLocation(0,
+                  sourceUrl: Uri.file('/path/to/output2.dart')))
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input2.dart'));
+      expect(
+          mapping
+              .spanForLocation(SourceLocation(0,
+                  sourceUrl: Uri.file('/path/to/3/output.dart')))
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input3.dart'));
+
+      expect(
+          mapping.spanFor(0, 0, uri: 'file:///path/to/output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input1.dart'));
+      expect(
+          mapping.spanFor(0, 0, uri: 'file:///path/to/output2.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input2.dart'));
+      expect(
+          mapping
+              .spanFor(0, 0, uri: 'file:///path/to/3/output.dart')
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input3.dart'));
+    });
+
+    test('package uris', () {
+      expect(
+          mapping
+              .spanForLocation(SourceLocation(0,
+                  sourceUrl: Uri.parse('package:1/output.dart')))
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input1.dart'));
+      expect(
+          mapping
+              .spanForLocation(SourceLocation(0,
+                  sourceUrl: Uri.parse('package:2/output2.dart')))
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input2.dart'));
+      expect(
+          mapping
+              .spanForLocation(SourceLocation(0,
+                  sourceUrl: Uri.parse('package:3/output.dart')))
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input3.dart'));
+
+      expect(mapping.spanFor(0, 0, uri: 'package:1/output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input1.dart'));
+      expect(mapping.spanFor(0, 0, uri: 'package:2/output2.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input2.dart'));
+      expect(mapping.spanFor(0, 0, uri: 'package:3/output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input3.dart'));
+    });
+
+    test('unmapped path', () {
+      var span = mapping.spanFor(0, 0, uri: 'unmapped_output.dart')!;
+      expect(span.sourceUrl, Uri.parse('unmapped_output.dart'));
+      expect(span.start.line, equals(0));
+      expect(span.start.column, equals(0));
+
+      span = mapping.spanFor(10, 5, uri: 'unmapped_output.dart')!;
+      expect(span.sourceUrl, Uri.parse('unmapped_output.dart'));
+      expect(span.start.line, equals(10));
+      expect(span.start.column, equals(5));
+    });
+
+    test('missing path', () {
+      expect(() => mapping.spanFor(0, 0), throwsA(anything));
+    });
+
+    test('incomplete paths', () {
+      expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input1.dart'));
+      expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input2.dart'));
+      expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input3.dart'));
+    });
+
+    test('parseExtended', () {
+      var mapping = parseExtended(jsonEncode(_sourceMapBundle),
+          mapUrl: 'file:///path/to/map');
+
+      expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input1.dart'));
+      expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input2.dart'));
+      expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input3.dart'));
+    });
+
+    test('build bundle incrementally', () {
+      var mapping = MappingBundle();
+
+      mapping.addMapping(parseJson(_mapWithSourceLocationAndName1,
+          mapUrl: 'file:///path/to/map') as SingleMapping);
+      expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input1.dart'));
+
+      expect(mapping.containsMapping('output2.dart'), isFalse);
+      mapping.addMapping(parseJson(_mapWithSourceLocationAndName2,
+          mapUrl: 'file:///path/to/map') as SingleMapping);
+      expect(mapping.containsMapping('output2.dart'), isTrue);
+      expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input2.dart'));
+
+      expect(mapping.containsMapping('3/output.dart'), isFalse);
+      mapping.addMapping(parseJson(_mapWithSourceLocationAndName3,
+          mapUrl: 'file:///path/to/map') as SingleMapping);
+      expect(mapping.containsMapping('3/output.dart'), isTrue);
+      expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input3.dart'));
+    });
+
+    // Test that the source map can handle cases where the uri passed in is
+    // not from the expected host but it is still unambiguous which source
+    // map should be used.
+    test('different paths', () {
+      expect(
+          mapping
+              .spanForLocation(SourceLocation(0,
+                  sourceUrl: Uri.parse('http://localhost/output.dart')))
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input1.dart'));
+      expect(
+          mapping
+              .spanForLocation(SourceLocation(0,
+                  sourceUrl: Uri.parse('http://localhost/output2.dart')))
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input2.dart'));
+      expect(
+          mapping
+              .spanForLocation(SourceLocation(0,
+                  sourceUrl: Uri.parse('http://localhost/3/output.dart')))
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input3.dart'));
+
+      expect(
+          mapping.spanFor(0, 0, uri: 'http://localhost/output.dart')?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input1.dart'));
+      expect(
+          mapping
+              .spanFor(0, 0, uri: 'http://localhost/output2.dart')
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input2.dart'));
+      expect(
+          mapping
+              .spanFor(0, 0, uri: 'http://localhost/3/output.dart')
+              ?.sourceUrl,
+          Uri.parse('file:///path/to/pkg/input3.dart'));
+    });
+  });
+
+  test('parse and re-emit', () {
+    for (var expected in [
+      expectedMap,
+      _mapWithNoSourceLocation,
+      _mapWithSourceLocation,
+      _mapWithSourceLocationAndName
+    ]) {
+      var mapping = parseJson(expected) as SingleMapping;
+      expect(mapping.toJson(), equals(expected));
+
+      mapping = parseJsonExtended(expected) as SingleMapping;
+      expect(mapping.toJson(), equals(expected));
+    }
+
+    var mapping = parseJsonExtended(_sourceMapBundle) as MappingBundle;
+    expect(mapping.toJson(), equals(_sourceMapBundle));
+  });
+
+  test('parse extensions', () {
+    var map = Map.from(expectedMap);
+    map['x_foo'] = 'a';
+    map['x_bar'] = [3];
+    var mapping = parseJson(map) as SingleMapping;
+    expect(mapping.toJson(), equals(map));
+    expect(mapping.extensions['x_foo'], equals('a'));
+    expect((mapping.extensions['x_bar'] as List).first, equals(3));
+  });
+
+  group('source files', () {
+    group('from fromEntries()', () {
+      test('are null for non-FileLocations', () {
+        var mapping = SingleMapping.fromEntries([
+          Entry(SourceLocation(10, line: 1, column: 8), outputVar1.start, null)
+        ]);
+        expect(mapping.files, equals([null]));
+      });
+
+      test("use a file location's file", () {
+        var mapping = SingleMapping.fromEntries(
+            [Entry(inputVar1.start, outputVar1.start, null)]);
+        expect(mapping.files, equals([input]));
+      });
+    });
+
+    group('from parse()', () {
+      group('are null', () {
+        test('with no sourcesContent field', () {
+          var mapping = parseJson(expectedMap) as SingleMapping;
+          expect(mapping.files, equals([null]));
+        });
+
+        test('with null sourcesContent values', () {
+          var map = Map.from(expectedMap);
+          map['sourcesContent'] = [null];
+          var mapping = parseJson(map) as SingleMapping;
+          expect(mapping.files, equals([null]));
+        });
+
+        test('with a too-short sourcesContent', () {
+          var map = Map.from(expectedMap);
+          map['sourcesContent'] = [];
+          var mapping = parseJson(map) as SingleMapping;
+          expect(mapping.files, equals([null]));
+        });
+      });
+
+      test('are parsed from sourcesContent', () {
+        var map = Map.from(expectedMap);
+        map['sourcesContent'] = ['hello, world!'];
+        var mapping = parseJson(map) as SingleMapping;
+
+        var file = mapping.files[0]!;
+        expect(file.url, equals(Uri.parse('input.dart')));
+        expect(file.getText(0), equals('hello, world!'));
+      });
+    });
+  });
+}
diff --git a/pkgs/source_maps/test/printer_test.dart b/pkgs/source_maps/test/printer_test.dart
new file mode 100644
index 0000000..89265e3
--- /dev/null
+++ b/pkgs/source_maps/test/printer_test.dart
@@ -0,0 +1,126 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+void main() {
+  test('printer', () {
+    var printer = Printer('output.dart');
+    printer
+      ..add('var ')
+      ..mark(inputVar1)
+      ..add('x = 3;\n')
+      ..mark(inputFunction)
+      ..add('f(')
+      ..mark(inputVar2)
+      ..add('y) => ')
+      ..mark(inputExpr)
+      ..add('x + y;\n');
+    expect(printer.text, outputContent);
+    expect(printer.map, jsonEncode(expectedMap));
+  });
+
+  test('printer projecting marks', () {
+    var out = inputContent.replaceAll('long', '_s');
+    var printer = Printer('output2.dart');
+
+    var segments = inputContent.split('long');
+    expect(segments.length, 6);
+    printer
+      ..mark(ispan(0, 0))
+      ..add(segments[0], projectMarks: true)
+      ..mark(inputVar1)
+      ..add('_s')
+      ..add(segments[1], projectMarks: true)
+      ..mark(inputFunction)
+      ..add('_s')
+      ..add(segments[2], projectMarks: true)
+      ..mark(inputVar2)
+      ..add('_s')
+      ..add(segments[3], projectMarks: true)
+      ..mark(inputExpr)
+      ..add('_s')
+      ..add(segments[4], projectMarks: true)
+      ..add('_s')
+      ..add(segments[5], projectMarks: true);
+
+    expect(printer.text, out);
+    // 8 new lines in the source map:
+    expect(printer.map.split(';').length, 8);
+
+    SourceMapSpan asFixed(SourceMapSpan s) =>
+        SourceMapSpan(s.start, s.end, s.text, isIdentifier: s.isIdentifier);
+
+    // The result is the same if we use fixed positions
+    var printer2 = Printer('output2.dart');
+    printer2
+      ..mark(SourceLocation(0, sourceUrl: 'input.dart').pointSpan())
+      ..add(segments[0], projectMarks: true)
+      ..mark(asFixed(inputVar1))
+      ..add('_s')
+      ..add(segments[1], projectMarks: true)
+      ..mark(asFixed(inputFunction))
+      ..add('_s')
+      ..add(segments[2], projectMarks: true)
+      ..mark(asFixed(inputVar2))
+      ..add('_s')
+      ..add(segments[3], projectMarks: true)
+      ..mark(asFixed(inputExpr))
+      ..add('_s')
+      ..add(segments[4], projectMarks: true)
+      ..add('_s')
+      ..add(segments[5], projectMarks: true);
+
+    expect(printer2.text, out);
+    expect(printer2.map, printer.map);
+  });
+
+  group('nested printer', () {
+    test('simple use', () {
+      var printer = NestedPrinter();
+      printer
+        ..add('var ')
+        ..add('x = 3;\n', span: inputVar1)
+        ..add('f(', span: inputFunction)
+        ..add('y) => ', span: inputVar2)
+        ..add('x + y;\n', span: inputExpr)
+        ..build('output.dart');
+      expect(printer.text, outputContent);
+      expect(printer.map, jsonEncode(expectedMap));
+    });
+
+    test('nested use', () {
+      var printer = NestedPrinter();
+      printer
+        ..add('var ')
+        ..add(NestedPrinter()..add('x = 3;\n', span: inputVar1))
+        ..add('f(', span: inputFunction)
+        ..add(NestedPrinter()..add('y) => ', span: inputVar2))
+        ..add('x + y;\n', span: inputExpr)
+        ..build('output.dart');
+      expect(printer.text, outputContent);
+      expect(printer.map, jsonEncode(expectedMap));
+    });
+
+    test('add indentation', () {
+      var out = inputContent.replaceAll('long', '_s');
+      var lines = inputContent.trim().split('\n');
+      expect(lines.length, 7);
+      var printer = NestedPrinter();
+      for (var i = 0; i < lines.length; i++) {
+        if (i == 5) printer.indent++;
+        printer.addLine(lines[i].replaceAll('long', '_s').trim());
+        if (i == 5) printer.indent--;
+      }
+      printer.build('output.dart');
+      expect(printer.text, out);
+    });
+  });
+}
diff --git a/pkgs/source_maps/test/refactor_test.dart b/pkgs/source_maps/test/refactor_test.dart
new file mode 100644
index 0000000..5bc3818
--- /dev/null
+++ b/pkgs/source_maps/test/refactor_test.dart
@@ -0,0 +1,199 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_maps/parser.dart' show Mapping, parse;
+import 'package:source_maps/refactor.dart';
+import 'package:source_span/source_span.dart';
+import 'package:term_glyph/term_glyph.dart' as term_glyph;
+import 'package:test/test.dart';
+
+void main() {
+  setUpAll(() {
+    term_glyph.ascii = true;
+  });
+
+  group('conflict detection', () {
+    var original = '0123456789abcdefghij';
+    var file = SourceFile.fromString(original);
+
+    test('no conflict, in order', () {
+      var txn = TextEditTransaction(original, file);
+      txn.edit(2, 4, '.');
+      txn.edit(5, 5, '|');
+      txn.edit(6, 6, '-');
+      txn.edit(6, 7, '_');
+      expect((txn.commit()..build('')).text, '01.4|5-_789abcdefghij');
+    });
+
+    test('no conflict, out of order', () {
+      var txn = TextEditTransaction(original, file);
+      txn.edit(2, 4, '.');
+      txn.edit(5, 5, '|');
+
+      // Regresion test for issue #404: there is no conflict/overlap for edits
+      // that don't remove any of the original code.
+      txn.edit(6, 7, '_');
+      txn.edit(6, 6, '-');
+      expect((txn.commit()..build('')).text, '01.4|5-_789abcdefghij');
+    });
+
+    test('conflict', () {
+      var txn = TextEditTransaction(original, file);
+      txn.edit(2, 4, '.');
+      txn.edit(3, 3, '-');
+      expect(
+          () => txn.commit(),
+          throwsA(
+              predicate((e) => e.toString().contains('overlapping edits'))));
+    });
+  });
+
+  test('generated source maps', () {
+    var original =
+        '0123456789\n0*23456789\n01*3456789\nabcdefghij\nabcd*fghij\n';
+    var file = SourceFile.fromString(original);
+    var txn = TextEditTransaction(original, file);
+    txn.edit(27, 29, '__\n    ');
+    txn.edit(34, 35, '___');
+    var printer = (txn.commit()..build(''));
+    var output = printer.text;
+    var map = parse(printer.map!);
+    expect(output,
+        '0123456789\n0*23456789\n01*34__\n    789\na___cdefghij\nabcd*fghij\n');
+
+    // Line 1 and 2 are unmodified: mapping any column returns the beginning
+    // of the corresponding line:
+    expect(
+        _span(1, 1, map, file),
+        'line 1, column 1: \n'
+        '  ,\n'
+        '1 | 0123456789\n'
+        '  | ^\n'
+        "  '");
+    expect(
+        _span(1, 5, map, file),
+        'line 1, column 1: \n'
+        '  ,\n'
+        '1 | 0123456789\n'
+        '  | ^\n'
+        "  '");
+    expect(
+        _span(2, 1, map, file),
+        'line 2, column 1: \n'
+        '  ,\n'
+        '2 | 0*23456789\n'
+        '  | ^\n'
+        "  '");
+    expect(
+        _span(2, 8, map, file),
+        'line 2, column 1: \n'
+        '  ,\n'
+        '2 | 0*23456789\n'
+        '  | ^\n'
+        "  '");
+
+    // Line 3 is modified part way: mappings before the edits have the right
+    // mapping, after the edits the mapping is null.
+    expect(
+        _span(3, 1, map, file),
+        'line 3, column 1: \n'
+        '  ,\n'
+        '3 | 01*3456789\n'
+        '  | ^\n'
+        "  '");
+    expect(
+        _span(3, 5, map, file),
+        'line 3, column 1: \n'
+        '  ,\n'
+        '3 | 01*3456789\n'
+        '  | ^\n'
+        "  '");
+
+    // Start of edits map to beginning of the edit secion:
+    expect(
+        _span(3, 6, map, file),
+        'line 3, column 6: \n'
+        '  ,\n'
+        '3 | 01*3456789\n'
+        '  |      ^\n'
+        "  '");
+    expect(
+        _span(3, 7, map, file),
+        'line 3, column 6: \n'
+        '  ,\n'
+        '3 | 01*3456789\n'
+        '  |      ^\n'
+        "  '");
+
+    // Lines added have no mapping (they should inherit the last mapping),
+    // but the end of the edit region continues were we left off:
+    expect(_span(4, 1, map, file), isNull);
+    expect(
+        _span(4, 5, map, file),
+        'line 3, column 8: \n'
+        '  ,\n'
+        '3 | 01*3456789\n'
+        '  |        ^\n'
+        "  '");
+
+    // Subsequent lines are still mapped correctly:
+    // a (in a___cd...)
+    expect(
+        _span(5, 1, map, file),
+        'line 4, column 1: \n'
+        '  ,\n'
+        '4 | abcdefghij\n'
+        '  | ^\n'
+        "  '");
+    // _ (in a___cd...)
+    expect(
+        _span(5, 2, map, file),
+        'line 4, column 2: \n'
+        '  ,\n'
+        '4 | abcdefghij\n'
+        '  |  ^\n'
+        "  '");
+    // _ (in a___cd...)
+    expect(
+        _span(5, 3, map, file),
+        'line 4, column 2: \n'
+        '  ,\n'
+        '4 | abcdefghij\n'
+        '  |  ^\n'
+        "  '");
+    // _ (in a___cd...)
+    expect(
+        _span(5, 4, map, file),
+        'line 4, column 2: \n'
+        '  ,\n'
+        '4 | abcdefghij\n'
+        '  |  ^\n'
+        "  '");
+    // c (in a___cd...)
+    expect(
+        _span(5, 5, map, file),
+        'line 4, column 3: \n'
+        '  ,\n'
+        '4 | abcdefghij\n'
+        '  |   ^\n'
+        "  '");
+    expect(
+        _span(6, 1, map, file),
+        'line 5, column 1: \n'
+        '  ,\n'
+        '5 | abcd*fghij\n'
+        '  | ^\n'
+        "  '");
+    expect(
+        _span(6, 8, map, file),
+        'line 5, column 1: \n'
+        '  ,\n'
+        '5 | abcd*fghij\n'
+        '  | ^\n'
+        "  '");
+  });
+}
+
+String? _span(int line, int column, Mapping map, SourceFile file) =>
+    map.spanFor(line - 1, column - 1, files: {'': file})?.message('').trim();
diff --git a/pkgs/source_maps/test/utils_test.dart b/pkgs/source_maps/test/utils_test.dart
new file mode 100644
index 0000000..2516d1e
--- /dev/null
+++ b/pkgs/source_maps/test/utils_test.dart
@@ -0,0 +1,53 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Tests for the binary search utility algorithm.
+library;
+
+import 'package:source_maps/src/utils.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('binary search', () {
+    test('empty', () {
+      expect(binarySearch([], (x) => true), -1);
+    });
+
+    test('single element', () {
+      expect(binarySearch([1], (x) => true), 0);
+      expect(binarySearch([1], (x) => false), 1);
+    });
+
+    test('no matches', () {
+      var list = [1, 2, 3, 4, 5, 6, 7];
+      expect(binarySearch(list, (x) => false), list.length);
+    });
+
+    test('all match', () {
+      var list = [1, 2, 3, 4, 5, 6, 7];
+      expect(binarySearch(list, (x) => true), 0);
+    });
+
+    test('compare with linear search', () {
+      for (var size = 0; size < 100; size++) {
+        var list = <int>[];
+        for (var i = 0; i < size; i++) {
+          list.add(i);
+        }
+        for (var pos = 0; pos <= size; pos++) {
+          expect(binarySearch(list, (x) => x >= pos),
+              _linearSearch(list, (x) => x >= pos));
+        }
+      }
+    });
+  });
+}
+
+int _linearSearch<T>(List<T> list, bool Function(T) predicate) {
+  if (list.isEmpty) return -1;
+  for (var i = 0; i < list.length; i++) {
+    if (predicate(list[i])) return i;
+  }
+  return list.length;
+}
diff --git a/pkgs/source_maps/test/vlq_test.dart b/pkgs/source_maps/test/vlq_test.dart
new file mode 100644
index 0000000..4568cff
--- /dev/null
+++ b/pkgs/source_maps/test/vlq_test.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math';
+
+import 'package:source_maps/src/vlq.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('encode and decode - simple values', () {
+    expect(encodeVlq(1).join(''), 'C');
+    expect(encodeVlq(2).join(''), 'E');
+    expect(encodeVlq(3).join(''), 'G');
+    expect(encodeVlq(100).join(''), 'oG');
+    expect(decodeVlq('C'.split('').iterator), 1);
+    expect(decodeVlq('E'.split('').iterator), 2);
+    expect(decodeVlq('G'.split('').iterator), 3);
+    expect(decodeVlq('oG'.split('').iterator), 100);
+  });
+
+  test('encode and decode', () {
+    for (var i = -10000; i < 10000; i++) {
+      _checkEncodeDecode(i);
+    }
+  });
+
+  test('only 32-bit ints allowed', () {
+    var maxInt = (pow(2, 31) as int) - 1;
+    var minInt = -(pow(2, 31) as int);
+    _checkEncodeDecode(maxInt - 1);
+    _checkEncodeDecode(minInt + 1);
+    _checkEncodeDecode(maxInt);
+    _checkEncodeDecode(minInt);
+
+    expect(encodeVlq(minInt).join(''), 'hgggggE');
+    expect(decodeVlq('hgggggE'.split('').iterator), minInt);
+
+    expect(() => encodeVlq(maxInt + 1), throwsA(anything));
+    expect(() => encodeVlq(maxInt + 2), throwsA(anything));
+    expect(() => encodeVlq(minInt - 1), throwsA(anything));
+    expect(() => encodeVlq(minInt - 2), throwsA(anything));
+
+    // if we allowed more than 32 bits, these would be the expected encodings
+    // for the large numbers above.
+    expect(() => decodeVlq('ggggggE'.split('').iterator), throwsA(anything));
+    expect(() => decodeVlq('igggggE'.split('').iterator), throwsA(anything));
+    expect(() => decodeVlq('jgggggE'.split('').iterator), throwsA(anything));
+    expect(() => decodeVlq('lgggggE'.split('').iterator), throwsA(anything));
+  },
+      // This test uses integers so large they overflow in JS.
+      testOn: 'dart-vm');
+}
+
+void _checkEncodeDecode(int value) {
+  var encoded = encodeVlq(value);
+  expect(decodeVlq(encoded.iterator), value);
+  expect(decodeVlq(encoded.join('').split('').iterator), value);
+}
diff --git a/pkgs/source_span/.gitignore b/pkgs/source_span/.gitignore
new file mode 100644
index 0000000..ab3cb76
--- /dev/null
+++ b/pkgs/source_span/.gitignore
@@ -0,0 +1,16 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.dart_tool/
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pkgs/source_span/CHANGELOG.md b/pkgs/source_span/CHANGELOG.md
new file mode 100644
index 0000000..b8319d7
--- /dev/null
+++ b/pkgs/source_span/CHANGELOG.md
@@ -0,0 +1,240 @@
+## 1.10.1
+
+* Require Dart 3.1
+* Move to `dart-lang/tools` monorepo.
+
+## 1.10.0
+
+* Add a `SourceFile.codeUnits` property.
+* Require Dart 2.18
+* Add an API usage example in `example/`.
+
+## 1.9.1
+
+* Properly handle multi-line labels for multi-span highlights.
+
+* Populate the pubspec `repository` field.
+
+## 1.9.0
+
+* Add `SourceSpanWithContextExtension.subspan` that returns a
+  `SourceSpanWithContext` rather than a plain `SourceSpan`.
+
+## 1.8.2
+
+* Fix a bug where highlighting multiple spans with `null` URLs could cause an
+  assertion error. Now when multiple spans are passed with `null` URLs, they're
+  highlighted as though they all come from different source files.
+
+## 1.8.1
+
+* Fix a bug where the URL header for the highlights with multiple files would
+  get omitted only one span has a non-null URI.
+
+## 1.8.0
+
+* Stable release for null safety.
+
+## 1.7.0
+
+* Add a `SourceSpan.subspan()` extension method which returns a slice of an
+  existing source span.
+
+## 1.6.0
+
+* Add support for highlighting multiple source spans at once, providing more
+  context for span-based messages. This is exposed through the new APIs
+  `SourceSpan.highlightMultiple()` and `SourceSpan.messageMultiple()` (both
+  extension methods), `MultiSourceSpanException`, and
+  `MultiSourceSpanFormatException`.
+
+## 1.5.6
+
+* Fix padding around line numbers that are powers of 10 in
+  `FileSpan.highlight()`.
+
+## 1.5.5
+
+* Fix a bug where `FileSpan.highlight()` would crash for spans that covered a
+  trailing newline and a single additional empty line.
+
+## 1.5.4
+
+* `FileSpan.highlight()` now properly highlights point spans at the beginning of
+  lines.
+
+## 1.5.3
+
+* Fix an edge case where `FileSpan.highlight()` would put the highlight
+  indicator in the wrong position when highlighting a point span after the end
+  of a file.
+
+## 1.5.2
+
+* `SourceFile.span()` now goes to the end of the file by default, rather than
+  ending one character before the end of the file. This matches the documented
+  behavior.
+
+* `FileSpan.context` now includes the full line on which the span appears for
+  empty spans at the beginning and end of lines.
+
+* Fix an edge case where `FileSpan.highlight()` could crash when highlighting a
+  span that ended with an empty line.
+
+## 1.5.1
+
+* Produce better source span highlights for multi-line spans that cover the
+  entire last line of the span, including the newline.
+
+* Produce better source span highlights for spans that contain Windows-style
+  newlines.
+
+## 1.5.0
+
+* Improve the output of `SourceSpan.highlight()` and `SourceSpan.message()`:
+
+  * They now include line numbers.
+  * They will now print every line of a multiline span.
+  * They will now use Unicode box-drawing characters by default (this can be
+    controlled using [`term_glyph.ascii`][]).
+
+[`term_glyph.ascii`]: https://pub.dartlang.org/documentation/term_glyph/latest/term_glyph/ascii.html
+
+## 1.4.1
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.4.0
+
+* The `new SourceFile()` constructor is deprecated. This constructed a source
+  file from a string's runes, rather than its code units, which runs counter to
+  the way Dart handles strings otherwise. The `new StringFile.fromString()`
+  constructor (see below) should be used instead.
+
+* The `new SourceFile.fromString()` constructor was added. This works like `new
+  SourceFile()`, except it uses code units rather than runes.
+
+* The current behavior when characters larger than `0xFFFF` are passed to `new
+  SourceFile.decoded()` is now considered deprecated.
+
+## 1.3.1
+
+* Properly highlight spans for lines that include tabs with
+  `SourceSpan.highlight()` and `SourceSpan.message()`.
+
+## 1.3.0
+
+* Add `SourceSpan.highlight()`, which returns just the highlighted text that
+  would be included in `SourceSpan.message()`.
+
+## 1.2.4
+
+* Fix a new strong mode error.
+
+## 1.2.3
+
+* Fix a bug where a point span at the end of a file without a trailing newline
+  would be printed incorrectly.
+
+## 1.2.2
+
+* Allow `SourceSpanException.message`, `SourceSpanFormatException.source`, and
+  `SourceSpanWithContext.context` to be overridden in strong mode.
+
+## 1.2.1
+
+* Fix the declared type of `FileSpan.start` and `FileSpan.end`. In 1.2.0 these
+  were mistakenly changed from `FileLocation` to `SourceLocation`.
+
+## 1.2.0
+
+* **Deprecated:** Extending `SourceLocation` directly is deprecated. Instead,
+  extend the new `SourceLocationBase` class or mix in the new
+  `SourceLocationMixin` mixin.
+
+* Dramatically improve the performance of `FileLocation`.
+
+## 1.1.6
+
+* Optimize `getLine()` in `SourceFile` when repeatedly called.
+
+## 1.1.5
+
+* Fixed another case in which `FileSpan.union` could throw an exception for
+  external implementations of `FileSpan`.
+
+## 1.1.4
+
+* Eliminated dart2js warning about overriding `==`, but not `hashCode`.
+
+## 1.1.3
+
+* `FileSpan.compareTo`, `FileSpan.==`, `FileSpan.union`, and `FileSpan.expand`
+  no longer throw exceptions for external implementations of `FileSpan`.
+
+* `FileSpan.hashCode` now fully agrees with `FileSpan.==`.
+
+## 1.1.2
+
+* Fixed validation in `SourceSpanWithContext` to allow multiple occurrences of
+  `text` within `context`.
+
+## 1.1.1
+
+* Fixed `FileSpan`'s context to include the full span text, not just the first
+  line of it.
+
+## 1.1.0
+
+* Added `SourceSpanWithContext`: a span that also includes the full line of text
+  that contains the span.
+
+## 1.0.3
+
+* Cleanup equality operator to accept any Object rather than just a
+  `SourceLocation`.
+
+## 1.0.2
+
+* Avoid unintentionally allocating extra objects for internal `FileSpan`
+  operations.
+
+* Ensure that `SourceSpan.operator==` works on arbitrary `Object`s.
+
+## 1.0.1
+
+* Use a more compact internal representation for `FileSpan`.
+
+## 1.0.0
+
+This package was extracted from the
+[`source_maps`](https://pub.dev/packages/source_maps) package, but the
+API has many differences. Among them:
+
+* `Span` has been renamed to `SourceSpan` and `Location` has been renamed to
+  `SourceLocation` to clarify their purpose and maintain consistency with the
+  package name. Likewise, `SpanException` is now `SourceSpanException` and
+  `SpanFormatException` is not `SourceSpanFormatException`.
+
+* `FixedSpan` and `FixedLocation` have been rolled into the `Span` and
+  `Location` classes, respectively.
+
+* `SourceFile` is more aggressive about validating its arguments. Out-of-bounds
+  lines, columns, and offsets will now throw errors rather than be silently
+  clamped.
+
+* `SourceSpan.sourceUrl`, `SourceLocation.sourceUrl`, and `SourceFile.url` now
+  return `Uri` objects rather than `String`s. The constructors allow either
+  `String`s or `Uri`s.
+
+* `Span.getLocationMessage` and `SourceFile.getLocationMessage` are now
+  `SourceSpan.message` and `SourceFile.message`, respectively. Rather than
+  taking both a `useColor` and a `color` parameter, they now take a single
+  `color` parameter that controls both whether and which color is used.
+
+* `Span.isIdentifier` has been removed. This property doesn't make sense outside
+  of a source map context.
+
+* `SourceFileSegment` has been removed. This class wasn't widely used and was
+  inconsistent in its choice of which parameters were considered relative and
+  which absolute.
diff --git a/pkgs/source_span/LICENSE b/pkgs/source_span/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/source_span/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/source_span/README.md b/pkgs/source_span/README.md
new file mode 100644
index 0000000..b4ce25f
--- /dev/null
+++ b/pkgs/source_span/README.md
@@ -0,0 +1,21 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/source_span.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/source_span.yaml)
+[![pub package](https://img.shields.io/pub/v/source_span.svg)](https://pub.dev/packages/source_span)
+[![package publisher](https://img.shields.io/pub/publisher/source_span.svg)](https://pub.dev/packages/source_span/publisher)
+
+## About this package
+
+`source_span` is a library for tracking locations in source code. It's designed
+to provide a standard representation for source code locations and spans so that
+disparate packages can easily pass them among one another, and to make it easy
+to generate human-friendly messages associated with a given piece of code.
+
+The most commonly-used class is the package's namesake, `SourceSpan`. It
+represents a span of characters in some source file, and is often attached to an
+object that has been parsed to indicate where it was parsed from. It provides
+access to the text of the span via `SourceSpan.text` and can be used to produce
+human-friendly messages using `SourceSpan.message()`.
+
+When parsing code from a file, `SourceFile` is useful. Not only does it provide
+an efficient means of computing line and column numbers, `SourceFile.span()`
+returns special `FileSpan`s that are able to provide more context for their
+error messages.
diff --git a/pkgs/source_span/analysis_options.yaml b/pkgs/source_span/analysis_options.yaml
new file mode 100644
index 0000000..d2ebdbf
--- /dev/null
+++ b/pkgs/source_span/analysis_options.yaml
@@ -0,0 +1,32 @@
+# 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
+  - prefer_const_declarations
+  - prefer_expression_function_bodies
+  - prefer_final_locals
+  - unnecessary_await_in_return
+  - unnecessary_raw_strings
+  - use_if_null_to_convert_nulls_to_bools
+  - use_raw_strings
+  - use_string_buffers
diff --git a/pkgs/source_span/example/main.dart b/pkgs/source_span/example/main.dart
new file mode 100644
index 0000000..e296765
--- /dev/null
+++ b/pkgs/source_span/example/main.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2023, 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:source_span/source_span.dart';
+
+void main(List<String> args) {
+  final file = File('README.md');
+  final contents = file.readAsStringSync();
+
+  final sourceFile = SourceFile.fromString(contents, url: file.uri);
+  final spans = _parseFile(contents, sourceFile);
+
+  for (var span in spans.take(30)) {
+    print('[${span.start.line + 1}:${span.start.column + 1}] ${span.text}');
+  }
+}
+
+Iterable<SourceSpan> _parseFile(String contents, SourceFile sourceFile) sync* {
+  var wordStart = 0;
+  var inWhiteSpace = true;
+
+  for (var i = 0; i < contents.length; i++) {
+    final codeUnit = contents.codeUnitAt(i);
+
+    if (codeUnit == _eol || codeUnit == _space) {
+      if (!inWhiteSpace) {
+        inWhiteSpace = true;
+
+        // emit a word
+        yield sourceFile.span(wordStart, i);
+      }
+    } else {
+      if (inWhiteSpace) {
+        inWhiteSpace = false;
+
+        wordStart = i;
+      }
+    }
+  }
+
+  if (!inWhiteSpace) {
+    // emit a word
+    yield sourceFile.span(wordStart, contents.length);
+  }
+}
+
+const int _eol = 10;
+const int _space = 32;
diff --git a/pkgs/source_span/lib/source_span.dart b/pkgs/source_span/lib/source_span.dart
new file mode 100644
index 0000000..534a3a7
--- /dev/null
+++ b/pkgs/source_span/lib/source_span.dart
@@ -0,0 +1,11 @@
+// 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/file.dart';
+export 'src/location.dart';
+export 'src/location_mixin.dart';
+export 'src/span.dart';
+export 'src/span_exception.dart';
+export 'src/span_mixin.dart';
+export 'src/span_with_context.dart';
diff --git a/pkgs/source_span/lib/src/charcode.dart b/pkgs/source_span/lib/src/charcode.dart
new file mode 100644
index 0000000..5182638
--- /dev/null
+++ b/pkgs/source_span/lib/src/charcode.dart
@@ -0,0 +1,15 @@
+// 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.
+
+/// "Carriage return" control character.
+const int $cr = 0x0D;
+
+/// "Line feed" control character.
+const int $lf = 0x0A;
+
+/// Space character.
+const int $space = 0x20;
+
+/// "Horizontal Tab" control character, common name.
+const int $tab = 0x09;
diff --git a/pkgs/source_span/lib/src/colors.dart b/pkgs/source_span/lib/src/colors.dart
new file mode 100644
index 0000000..b48d468
--- /dev/null
+++ b/pkgs/source_span/lib/src/colors.dart
@@ -0,0 +1,12 @@
+// 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.
+
+// Color constants used for generating messages.
+const String red = '\u001b[31m';
+
+const String yellow = '\u001b[33m';
+
+const String blue = '\u001b[34m';
+
+const String none = '\u001b[0m';
diff --git a/pkgs/source_span/lib/src/file.dart b/pkgs/source_span/lib/src/file.dart
new file mode 100644
index 0000000..74c9234
--- /dev/null
+++ b/pkgs/source_span/lib/src/file.dart
@@ -0,0 +1,454 @@
+// 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 'dart:typed_data';
+
+import 'location.dart';
+import 'location_mixin.dart';
+import 'span.dart';
+import 'span_mixin.dart';
+import 'span_with_context.dart';
+
+// Constants to determine end-of-lines.
+const int _lf = 10;
+const int _cr = 13;
+
+/// A class representing a source file.
+///
+/// This doesn't necessarily have to correspond to a file on disk, just a chunk
+/// of text usually with a URL associated with it.
+class SourceFile {
+  /// The URL where the source file is located.
+  ///
+  /// This may be null, indicating that the URL is unknown or unavailable.
+  final Uri? url;
+
+  /// An array of offsets for each line beginning in the file.
+  ///
+  /// Each offset refers to the first character *after* the newline. If the
+  /// source file has a trailing newline, the final offset won't actually be in
+  /// the file.
+  final _lineStarts = <int>[0];
+
+  /// The code units of the characters in the file.
+  ///
+  /// If this was constructed with the deprecated `SourceFile()` constructor,
+  /// this will instead contain the code _points_ of the characters in the file
+  /// (so characters above 2^16 are represented as individual integers rather
+  /// than surrogate pairs).
+  List<int> get codeUnits => _decodedChars;
+
+  /// The code units of the characters in this file.
+  final Uint32List _decodedChars;
+
+  /// The length of the file in characters.
+  int get length => _decodedChars.length;
+
+  /// The number of lines in the file.
+  int get lines => _lineStarts.length;
+
+  /// The line that the offset fell on the last time [getLine] was called.
+  ///
+  /// In many cases, sequential calls to getLine() are for nearby, usually
+  /// increasing offsets. In that case, we can find the line for an offset
+  /// quickly by first checking to see if the offset is on the same line as the
+  /// previous result.
+  int? _cachedLine;
+
+  /// This constructor is deprecated.
+  ///
+  /// Use [SourceFile.fromString] instead.
+  @Deprecated('Will be removed in 2.0.0')
+  SourceFile(String text, {Object? url}) : this.decoded(text.runes, url: url);
+
+  /// Creates a new source file from [text].
+  ///
+  /// [url] may be either a [String], a [Uri], or `null`.
+  SourceFile.fromString(String text, {Object? url})
+      : this.decoded(text.codeUnits, url: url);
+
+  /// Creates a new source file from a list of decoded code units.
+  ///
+  /// [url] may be either a [String], a [Uri], or `null`.
+  ///
+  /// Currently, if [decodedChars] contains characters larger than `0xFFFF`,
+  /// they'll be treated as single characters rather than being split into
+  /// surrogate pairs. **This behavior is deprecated**. For
+  /// forwards-compatibility, callers should only pass in characters less than
+  /// or equal to `0xFFFF`.
+  SourceFile.decoded(Iterable<int> decodedChars, {Object? url})
+      : url = url is String ? Uri.parse(url) : url as Uri?,
+        _decodedChars = Uint32List.fromList(decodedChars.toList()) {
+    for (var i = 0; i < _decodedChars.length; i++) {
+      var c = _decodedChars[i];
+      if (c == _cr) {
+        // Return not followed by newline is treated as a newline
+        final j = i + 1;
+        if (j >= _decodedChars.length || _decodedChars[j] != _lf) c = _lf;
+      }
+      if (c == _lf) _lineStarts.add(i + 1);
+    }
+  }
+
+  /// Returns a span from [start] to [end] (exclusive).
+  ///
+  /// If [end] isn't passed, it defaults to the end of the file.
+  FileSpan span(int start, [int? end]) {
+    end ??= length;
+    return _FileSpan(this, start, end);
+  }
+
+  /// Returns a location at [offset].
+  FileLocation location(int offset) => FileLocation._(this, offset);
+
+  /// Gets the 0-based line corresponding to [offset].
+  int getLine(int offset) {
+    if (offset < 0) {
+      throw RangeError('Offset may not be negative, was $offset.');
+    } else if (offset > length) {
+      throw RangeError('Offset $offset must not be greater than the number '
+          'of characters in the file, $length.');
+    }
+
+    if (offset < _lineStarts.first) return -1;
+    if (offset >= _lineStarts.last) return _lineStarts.length - 1;
+
+    if (_isNearCachedLine(offset)) return _cachedLine!;
+
+    _cachedLine = _binarySearch(offset) - 1;
+    return _cachedLine!;
+  }
+
+  /// Returns `true` if [offset] is near [_cachedLine].
+  ///
+  /// Checks on [_cachedLine] and the next line. If it's on the next line, it
+  /// updates [_cachedLine] to point to that.
+  bool _isNearCachedLine(int offset) {
+    if (_cachedLine == null) return false;
+    final cachedLine = _cachedLine!;
+
+    // See if it's before the cached line.
+    if (offset < _lineStarts[cachedLine]) return false;
+
+    // See if it's on the cached line.
+    if (cachedLine >= _lineStarts.length - 1 ||
+        offset < _lineStarts[cachedLine + 1]) {
+      return true;
+    }
+
+    // See if it's on the next line.
+    if (cachedLine >= _lineStarts.length - 2 ||
+        offset < _lineStarts[cachedLine + 2]) {
+      _cachedLine = cachedLine + 1;
+      return true;
+    }
+
+    return false;
+  }
+
+  /// Binary search through [_lineStarts] to find the line containing [offset].
+  ///
+  /// Returns the index of the line in [_lineStarts].
+  int _binarySearch(int offset) {
+    var min = 0;
+    var max = _lineStarts.length - 1;
+    while (min < max) {
+      final half = min + ((max - min) ~/ 2);
+      if (_lineStarts[half] > offset) {
+        max = half;
+      } else {
+        min = half + 1;
+      }
+    }
+
+    return max;
+  }
+
+  /// Gets the 0-based column corresponding to [offset].
+  ///
+  /// If [line] is passed, it's assumed to be the line containing [offset] and
+  /// is used to more efficiently compute the column.
+  int getColumn(int offset, {int? line}) {
+    if (offset < 0) {
+      throw RangeError('Offset may not be negative, was $offset.');
+    } else if (offset > length) {
+      throw RangeError('Offset $offset must be not be greater than the '
+          'number of characters in the file, $length.');
+    }
+
+    if (line == null) {
+      line = getLine(offset);
+    } else if (line < 0) {
+      throw RangeError('Line may not be negative, was $line.');
+    } else if (line >= lines) {
+      throw RangeError('Line $line must be less than the number of '
+          'lines in the file, $lines.');
+    }
+
+    final lineStart = _lineStarts[line];
+    if (lineStart > offset) {
+      throw RangeError('Line $line comes after offset $offset.');
+    }
+
+    return offset - lineStart;
+  }
+
+  /// Gets the offset for a [line] and [column].
+  ///
+  /// [column] defaults to 0.
+  int getOffset(int line, [int? column]) {
+    column ??= 0;
+
+    if (line < 0) {
+      throw RangeError('Line may not be negative, was $line.');
+    } else if (line >= lines) {
+      throw RangeError('Line $line must be less than the number of '
+          'lines in the file, $lines.');
+    } else if (column < 0) {
+      throw RangeError('Column may not be negative, was $column.');
+    }
+
+    final result = _lineStarts[line] + column;
+    if (result > length ||
+        (line + 1 < lines && result >= _lineStarts[line + 1])) {
+      throw RangeError("Line $line doesn't have $column columns.");
+    }
+
+    return result;
+  }
+
+  /// Returns the text of the file from [start] to [end] (exclusive).
+  ///
+  /// If [end] isn't passed, it defaults to the end of the file.
+  String getText(int start, [int? end]) =>
+      String.fromCharCodes(_decodedChars.sublist(start, end));
+}
+
+/// A [SourceLocation] within a [SourceFile].
+///
+/// Unlike the base [SourceLocation], [FileLocation] lazily computes its line
+/// and column values based on its offset and the contents of [file].
+///
+/// A [FileLocation] can be created using [SourceFile.location].
+class FileLocation extends SourceLocationMixin implements SourceLocation {
+  /// The [file] that `this` belongs to.
+  final SourceFile file;
+
+  @override
+  final int offset;
+
+  @override
+  Uri? get sourceUrl => file.url;
+
+  @override
+  int get line => file.getLine(offset);
+
+  @override
+  int get column => file.getColumn(offset);
+
+  FileLocation._(this.file, this.offset) {
+    if (offset < 0) {
+      throw RangeError('Offset may not be negative, was $offset.');
+    } else if (offset > file.length) {
+      throw RangeError('Offset $offset must not be greater than the number '
+          'of characters in the file, ${file.length}.');
+    }
+  }
+
+  @override
+  FileSpan pointSpan() => _FileSpan(file, offset, offset);
+}
+
+/// A [SourceSpan] within a [SourceFile].
+///
+/// Unlike the base [SourceSpan], [FileSpan] lazily computes its line and column
+/// values based on its offset and the contents of [file]. [SourceSpan.message]
+/// is also able to provide more context then [SourceSpan.message], and
+/// [SourceSpan.union] will return a [FileSpan] if possible.
+///
+/// A [FileSpan] can be created using [SourceFile.span].
+abstract class FileSpan implements SourceSpanWithContext {
+  /// The [file] that `this` belongs to.
+  SourceFile get file;
+
+  @override
+  FileLocation get start;
+
+  @override
+  FileLocation get end;
+
+  /// Returns a new span that covers both `this` and [other].
+  ///
+  /// Unlike [union], [other] may be disjoint from `this`. If it is, the text
+  /// between the two will be covered by the returned span.
+  FileSpan expand(FileSpan other);
+}
+
+/// The implementation of [FileSpan].
+///
+/// This is split into a separate class so that `is _FileSpan` checks can be run
+/// to make certain operations more efficient. If we used `is FileSpan`, that
+/// would break if external classes implemented the interface.
+class _FileSpan extends SourceSpanMixin implements FileSpan {
+  @override
+  final SourceFile file;
+
+  /// The offset of the beginning of the span.
+  ///
+  /// [start] is lazily generated from this to avoid allocating unnecessary
+  /// objects.
+  final int _start;
+
+  /// The offset of the end of the span.
+  ///
+  /// [end] is lazily generated from this to avoid allocating unnecessary
+  /// objects.
+  final int _end;
+
+  @override
+  Uri? get sourceUrl => file.url;
+
+  @override
+  int get length => _end - _start;
+
+  @override
+  FileLocation get start => FileLocation._(file, _start);
+
+  @override
+  FileLocation get end => FileLocation._(file, _end);
+
+  @override
+  String get text => file.getText(_start, _end);
+
+  @override
+  String get context {
+    final endLine = file.getLine(_end);
+    final endColumn = file.getColumn(_end);
+
+    int? endOffset;
+    if (endColumn == 0 && endLine != 0) {
+      // If [end] is at the very beginning of the line, the span covers the
+      // previous newline, so we only want to include the previous line in the
+      // context...
+
+      if (length == 0) {
+        // ...unless this is a point span, in which case we want to include the
+        // next line (or the empty string if this is the end of the file).
+        return endLine == file.lines - 1
+            ? ''
+            : file.getText(
+                file.getOffset(endLine), file.getOffset(endLine + 1));
+      }
+
+      endOffset = _end;
+    } else if (endLine == file.lines - 1) {
+      // If the span covers the last line of the file, the context should go all
+      // the way to the end of the file.
+      endOffset = file.length;
+    } else {
+      // Otherwise, the context should cover the full line on which [end]
+      // appears.
+      endOffset = file.getOffset(endLine + 1);
+    }
+
+    return file.getText(file.getOffset(file.getLine(_start)), endOffset);
+  }
+
+  _FileSpan(this.file, this._start, this._end) {
+    if (_end < _start) {
+      throw ArgumentError('End $_end must come after start $_start.');
+    } else if (_end > file.length) {
+      throw RangeError('End $_end must not be greater than the number '
+          'of characters in the file, ${file.length}.');
+    } else if (_start < 0) {
+      throw RangeError('Start may not be negative, was $_start.');
+    }
+  }
+
+  @override
+  int compareTo(SourceSpan other) {
+    if (other is! _FileSpan) return super.compareTo(other);
+
+    final result = _start.compareTo(other._start);
+    return result == 0 ? _end.compareTo(other._end) : result;
+  }
+
+  @override
+  SourceSpan union(SourceSpan other) {
+    if (other is! FileSpan) return super.union(other);
+
+    final span = expand(other);
+
+    if (other is _FileSpan) {
+      if (_start > other._end || other._start > _end) {
+        throw ArgumentError('Spans $this and $other are disjoint.');
+      }
+    } else {
+      if (_start > other.end.offset || other.start.offset > _end) {
+        throw ArgumentError('Spans $this and $other are disjoint.');
+      }
+    }
+
+    return span;
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other is! FileSpan) return super == other;
+    if (other is! _FileSpan) {
+      return super == other && sourceUrl == other.sourceUrl;
+    }
+
+    return _start == other._start &&
+        _end == other._end &&
+        sourceUrl == other.sourceUrl;
+  }
+
+  @override
+  int get hashCode => Object.hash(_start, _end, sourceUrl);
+
+  /// Returns a new span that covers both `this` and [other].
+  ///
+  /// Unlike [union], [other] may be disjoint from `this`. If it is, the text
+  /// between the two will be covered by the returned span.
+  @override
+  FileSpan expand(FileSpan other) {
+    if (sourceUrl != other.sourceUrl) {
+      throw ArgumentError('Source URLs "$sourceUrl" and '
+          " \"${other.sourceUrl}\" don't match.");
+    }
+
+    if (other is _FileSpan) {
+      final start = math.min(_start, other._start);
+      final end = math.max(_end, other._end);
+      return _FileSpan(file, start, end);
+    } else {
+      final start = math.min(_start, other.start.offset);
+      final end = math.max(_end, other.end.offset);
+      return _FileSpan(file, start, end);
+    }
+  }
+
+  /// See `SourceSpanExtension.subspan`.
+  FileSpan subspan(int start, [int? end]) {
+    RangeError.checkValidRange(start, end, length);
+    if (start == 0 && (end == null || end == length)) return this;
+    return file.span(_start + start, end == null ? _end : _start + end);
+  }
+}
+
+// TODO(#52): Move these to instance methods in the next breaking release.
+/// Extension methods on the [FileSpan] API.
+extension FileSpanExtension on FileSpan {
+  /// See `SourceSpanExtension.subspan`.
+  FileSpan subspan(int start, [int? end]) {
+    RangeError.checkValidRange(start, end, length);
+    if (start == 0 && (end == null || end == length)) return this;
+
+    final startOffset = this.start.offset;
+    return file.span(
+        startOffset + start, end == null ? this.end.offset : startOffset + end);
+  }
+}
diff --git a/pkgs/source_span/lib/src/highlighter.dart b/pkgs/source_span/lib/src/highlighter.dart
new file mode 100644
index 0000000..19e04d0
--- /dev/null
+++ b/pkgs/source_span/lib/src/highlighter.dart
@@ -0,0 +1,727 @@
+// Copyright (c) 2018, 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:path/path.dart' as p;
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+import 'charcode.dart';
+import 'colors.dart' as colors;
+import 'location.dart';
+import 'span.dart';
+import 'span_with_context.dart';
+import 'utils.dart';
+
+/// A class for writing a chunk of text with a particular span highlighted.
+class Highlighter {
+  /// The lines to display, including context around the highlighted spans.
+  final List<_Line> _lines;
+
+  /// The color to highlight the primary [_Highlight] within its context, or
+  /// `null` if it should not be colored.
+  final String? _primaryColor;
+
+  /// The color to highlight the secondary [_Highlight]s within their context,
+  /// or `null` if they should not be colored.
+  final String? _secondaryColor;
+
+  /// The number of characters before the bar in the sidebar.
+  final int _paddingBeforeSidebar;
+
+  /// The maximum number of multiline spans that cover any part of a single
+  /// line in [_lines].
+  final int _maxMultilineSpans;
+
+  /// Whether [_lines] includes lines from multiple different files.
+  final bool _multipleFiles;
+
+  /// The buffer to which to write the result.
+  final _buffer = StringBuffer();
+
+  /// The number of spaces to render for hard tabs that appear in `_span.text`.
+  ///
+  /// We don't want to render raw tabs, because they'll mess up our character
+  /// alignment.
+  static const _spacesPerTab = 4;
+
+  /// Creates a [Highlighter] that will return a string highlighting [span]
+  /// within the text of its file when [highlight] is called.
+  ///
+  /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+  /// it indicates an [ANSI terminal color escape][] that should be used to
+  /// highlight [span]'s text (for example, `"\u001b[31m"` will color red). If
+  /// it's `true`, it indicates that the text should be highlighted using the
+  /// default color. If it's `false` or `null`, it indicates that no color
+  /// should be used.
+  ///
+  /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+  Highlighter(SourceSpan span, {Object? color})
+      : this._(_collateLines([_Highlight(span, primary: true)]), () {
+          if (color == true) return colors.red;
+          if (color == false) return null;
+          return color as String?;
+        }(), null);
+
+  /// Creates a [Highlighter] that will return a string highlighting
+  /// [primarySpan] as well as all the spans in [secondarySpans] within the text
+  /// of their file when [highlight] is called.
+  ///
+  /// Each span has an associated label that will be written alongside it. For
+  /// [primarySpan] this message is [primaryLabel], and for [secondarySpans] the
+  /// labels are the map values.
+  ///
+  /// If [color] is `true`, this will use [ANSI terminal color escapes][] to
+  /// highlight the text. The [primarySpan] will be highlighted with
+  /// [primaryColor] (which defaults to red), and the [secondarySpans] will be
+  /// highlighted with [secondaryColor] (which defaults to blue). These
+  /// arguments are ignored if [color] is `false`.
+  ///
+  /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+  Highlighter.multiple(SourceSpan primarySpan, String primaryLabel,
+      Map<SourceSpan, String> secondarySpans,
+      {bool color = false, String? primaryColor, String? secondaryColor})
+      : this._(
+            _collateLines([
+              _Highlight(primarySpan, label: primaryLabel, primary: true),
+              for (var entry in secondarySpans.entries)
+                _Highlight(entry.key, label: entry.value)
+            ]),
+            color ? (primaryColor ?? colors.red) : null,
+            color ? (secondaryColor ?? colors.blue) : null);
+
+  Highlighter._(this._lines, this._primaryColor, this._secondaryColor)
+      : _paddingBeforeSidebar = 1 +
+            math.max<int>(
+              // In a purely mathematical world, floor(log10(n)) would give the
+              // number of digits in n, but floating point errors render that
+              // unreliable in practice.
+              (_lines.last.number + 1).toString().length,
+              // If [_lines] aren't contiguous, we'll write "..." in place of a
+              // line number.
+              _contiguous(_lines) ? 0 : 3,
+            ),
+        _maxMultilineSpans = _lines
+            .map((line) => line.highlights
+                .where((highlight) => isMultiline(highlight.span))
+                .length)
+            .reduce(math.max),
+        _multipleFiles = !isAllTheSame(_lines.map((line) => line.url));
+
+  /// Returns whether [lines] contains any adjacent lines from the same source
+  /// file that aren't adjacent in the original file.
+  static bool _contiguous(List<_Line> lines) {
+    for (var i = 0; i < lines.length - 1; i++) {
+      final thisLine = lines[i];
+      final nextLine = lines[i + 1];
+      if (thisLine.number + 1 != nextLine.number &&
+          thisLine.url == nextLine.url) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /// Collect all the source lines from the contexts of all spans in
+  /// [highlights], and associates them with the highlights that cover them.
+  static List<_Line> _collateLines(List<_Highlight> highlights) {
+    // Assign spans without URLs opaque Objects as keys. Each such Object will
+    // be different, but they can then be used later on to determine which lines
+    // came from the same span even if they'd all otherwise have `null` URLs.
+    final highlightsByUrl = groupBy<_Highlight, Object>(
+        highlights, (highlight) => highlight.span.sourceUrl ?? Object());
+    for (var list in highlightsByUrl.values) {
+      list.sort((highlight1, highlight2) =>
+          highlight1.span.compareTo(highlight2.span));
+    }
+
+    return highlightsByUrl.entries.expand((entry) {
+      final url = entry.key;
+      final highlightsForFile = entry.value;
+
+      // First, create a list of all the lines in the current file that we have
+      // context for along with their line numbers.
+      final lines = <_Line>[];
+      for (var highlight in highlightsForFile) {
+        final context = highlight.span.context;
+        // If [highlight.span.context] contains lines prior to the one
+        // [highlight.span.text] appears on, write those first.
+        final lineStart = findLineStart(
+            context, highlight.span.text, highlight.span.start.column)!;
+
+        final linesBeforeSpan =
+            '\n'.allMatches(context.substring(0, lineStart)).length;
+
+        var lineNumber = highlight.span.start.line - linesBeforeSpan;
+        for (var line in context.split('\n')) {
+          // Only add a line if it hasn't already been added for a previous span
+          if (lines.isEmpty || lineNumber > lines.last.number) {
+            lines.add(_Line(line, lineNumber, url));
+          }
+          lineNumber++;
+        }
+      }
+
+      // Next, associate each line with each highlights that covers it.
+      final activeHighlights = <_Highlight>[];
+      var highlightIndex = 0;
+      for (var line in lines) {
+        activeHighlights
+            .removeWhere((highlight) => highlight.span.end.line < line.number);
+
+        final oldHighlightLength = activeHighlights.length;
+        for (var highlight in highlightsForFile.skip(highlightIndex)) {
+          if (highlight.span.start.line > line.number) break;
+          activeHighlights.add(highlight);
+        }
+        highlightIndex += activeHighlights.length - oldHighlightLength;
+
+        line.highlights.addAll(activeHighlights);
+      }
+
+      return lines;
+    }).toList();
+  }
+
+  /// Returns the highlighted span text.
+  ///
+  /// This method should only be called once.
+  String highlight() {
+    _writeFileStart(_lines.first.url);
+
+    // Each index of this list represents a column after the sidebar that could
+    // contain a line indicating an active highlight. If it's `null`, that
+    // column is empty; if it contains a highlight, it should be drawn for that
+    // column.
+    final highlightsByColumn =
+        List<_Highlight?>.filled(_maxMultilineSpans, null);
+
+    for (var i = 0; i < _lines.length; i++) {
+      final line = _lines[i];
+      if (i > 0) {
+        final lastLine = _lines[i - 1];
+        if (lastLine.url != line.url) {
+          _writeSidebar(end: glyph.upEnd);
+          _buffer.writeln();
+          _writeFileStart(line.url);
+        } else if (lastLine.number + 1 != line.number) {
+          _writeSidebar(text: '...');
+          _buffer.writeln();
+        }
+      }
+
+      // If a highlight covers the entire first line other than initial
+      // whitespace, don't bother pointing out exactly where it begins. Iterate
+      // in reverse so that longer highlights (which are sorted after shorter
+      // highlights) appear further out, leading to fewer crossed lines.
+      for (var highlight in line.highlights.reversed) {
+        if (isMultiline(highlight.span) &&
+            highlight.span.start.line == line.number &&
+            _isOnlyWhitespace(
+                line.text.substring(0, highlight.span.start.column))) {
+          replaceFirstNull(highlightsByColumn, highlight);
+        }
+      }
+
+      _writeSidebar(line: line.number);
+      _buffer.write(' ');
+      _writeMultilineHighlights(line, highlightsByColumn);
+      if (highlightsByColumn.isNotEmpty) _buffer.write(' ');
+
+      final primaryIdx =
+          line.highlights.indexWhere((highlight) => highlight.isPrimary);
+      final primary = primaryIdx == -1 ? null : line.highlights[primaryIdx];
+
+      if (primary != null) {
+        _writeHighlightedText(
+            line.text,
+            primary.span.start.line == line.number
+                ? primary.span.start.column
+                : 0,
+            primary.span.end.line == line.number
+                ? primary.span.end.column
+                : line.text.length,
+            color: _primaryColor);
+      } else {
+        _writeText(line.text);
+      }
+      _buffer.writeln();
+
+      // Always write the primary span's indicator first so that it's right next
+      // to the highlighted text.
+      if (primary != null) _writeIndicator(line, primary, highlightsByColumn);
+      for (var highlight in line.highlights) {
+        if (highlight.isPrimary) continue;
+        _writeIndicator(line, highlight, highlightsByColumn);
+      }
+    }
+
+    _writeSidebar(end: glyph.upEnd);
+    return _buffer.toString();
+  }
+
+  /// Writes the beginning of the file highlight for the file with the given
+  /// [url] (or opaque object if it comes from a span with a null URL).
+  void _writeFileStart(Object url) {
+    if (!_multipleFiles || url is! Uri) {
+      _writeSidebar(end: glyph.downEnd);
+    } else {
+      _writeSidebar(end: glyph.topLeftCorner);
+      _colorize(() => _buffer.write('${glyph.horizontalLine * 2}>'),
+          color: colors.blue);
+      _buffer.write(' ${p.prettyUri(url)}');
+    }
+    _buffer.writeln();
+  }
+
+  /// Writes the post-sidebar highlight bars for [line] according to
+  /// [highlightsByColumn].
+  ///
+  /// If [current] is passed, it's the highlight for which an indicator is being
+  /// written. If it appears in [highlightsByColumn], a horizontal line is
+  /// written from its column to the rightmost column.
+  void _writeMultilineHighlights(
+      _Line line, List<_Highlight?> highlightsByColumn,
+      {_Highlight? current}) {
+    // Whether we've written a sidebar indicator for opening a new span on this
+    // line, and which color should be used for that indicator's rightward line.
+    var openedOnThisLine = false;
+    String? openedOnThisLineColor;
+
+    final currentColor = current == null
+        ? null
+        : current.isPrimary
+            ? _primaryColor
+            : _secondaryColor;
+    var foundCurrent = false;
+    for (var highlight in highlightsByColumn) {
+      final startLine = highlight?.span.start.line;
+      final endLine = highlight?.span.end.line;
+      if (current != null && highlight == current) {
+        foundCurrent = true;
+        assert(startLine == line.number || endLine == line.number);
+        _colorize(() {
+          _buffer.write(startLine == line.number
+              ? glyph.topLeftCorner
+              : glyph.bottomLeftCorner);
+        }, color: currentColor);
+      } else if (foundCurrent) {
+        _colorize(() {
+          _buffer.write(highlight == null ? glyph.horizontalLine : glyph.cross);
+        }, color: currentColor);
+      } else if (highlight == null) {
+        if (openedOnThisLine) {
+          _colorize(() => _buffer.write(glyph.horizontalLine),
+              color: openedOnThisLineColor);
+        } else {
+          _buffer.write(' ');
+        }
+      } else {
+        _colorize(() {
+          final vertical = openedOnThisLine ? glyph.cross : glyph.verticalLine;
+          if (current != null) {
+            _buffer.write(vertical);
+          } else if (startLine == line.number) {
+            _colorize(() {
+              _buffer
+                  .write(glyph.glyphOrAscii(openedOnThisLine ? '┬' : '┌', '/'));
+            }, color: openedOnThisLineColor);
+            openedOnThisLine = true;
+            openedOnThisLineColor ??=
+                highlight.isPrimary ? _primaryColor : _secondaryColor;
+          } else if (endLine == line.number &&
+              highlight.span.end.column == line.text.length) {
+            _buffer.write(highlight.label == null
+                ? glyph.glyphOrAscii('└', r'\')
+                : vertical);
+          } else {
+            _colorize(() {
+              _buffer.write(vertical);
+            }, color: openedOnThisLineColor);
+          }
+        }, color: highlight.isPrimary ? _primaryColor : _secondaryColor);
+      }
+    }
+  }
+
+  // Writes [text], with text between [startColumn] and [endColumn] colorized in
+  // the same way as [_colorize].
+  void _writeHighlightedText(String text, int startColumn, int endColumn,
+      {required String? color}) {
+    _writeText(text.substring(0, startColumn));
+    _colorize(() => _writeText(text.substring(startColumn, endColumn)),
+        color: color);
+    _writeText(text.substring(endColumn, text.length));
+  }
+
+  /// Writes an indicator for where [highlight] starts, ends, or both below
+  /// [line].
+  ///
+  /// This may either add or remove [highlight] from [highlightsByColumn].
+  void _writeIndicator(
+      _Line line, _Highlight highlight, List<_Highlight?> highlightsByColumn) {
+    final color = highlight.isPrimary ? _primaryColor : _secondaryColor;
+    if (!isMultiline(highlight.span)) {
+      _writeSidebar();
+      _buffer.write(' ');
+      _writeMultilineHighlights(line, highlightsByColumn, current: highlight);
+      if (highlightsByColumn.isNotEmpty) _buffer.write(' ');
+
+      final underlineLength = _colorize(() {
+        final start = _buffer.length;
+        _writeUnderline(line, highlight.span,
+            highlight.isPrimary ? '^' : glyph.horizontalLineBold);
+        return _buffer.length - start;
+      }, color: color);
+      _writeLabel(highlight, highlightsByColumn, underlineLength);
+    } else if (highlight.span.start.line == line.number) {
+      if (highlightsByColumn.contains(highlight)) return;
+      replaceFirstNull(highlightsByColumn, highlight);
+
+      _writeSidebar();
+      _buffer.write(' ');
+      _writeMultilineHighlights(line, highlightsByColumn, current: highlight);
+      _colorize(() => _writeArrow(line, highlight.span.start.column),
+          color: color);
+      _buffer.writeln();
+    } else if (highlight.span.end.line == line.number) {
+      final coversWholeLine = highlight.span.end.column == line.text.length;
+      if (coversWholeLine && highlight.label == null) {
+        replaceWithNull(highlightsByColumn, highlight);
+        return;
+      }
+
+      _writeSidebar();
+      _buffer.write(' ');
+      _writeMultilineHighlights(line, highlightsByColumn, current: highlight);
+
+      final underlineLength = _colorize(() {
+        final start = _buffer.length;
+        if (coversWholeLine) {
+          _buffer.write(glyph.horizontalLine * 3);
+        } else {
+          _writeArrow(line, math.max(highlight.span.end.column - 1, 0),
+              beginning: false);
+        }
+        return _buffer.length - start;
+      }, color: color);
+      _writeLabel(highlight, highlightsByColumn, underlineLength);
+      replaceWithNull(highlightsByColumn, highlight);
+    }
+  }
+
+  /// Underlines the portion of [line] covered by [span] with repeated instances
+  /// of [character].
+  void _writeUnderline(_Line line, SourceSpan span, String character) {
+    assert(!isMultiline(span));
+    assert(line.text.contains(span.text),
+        '"${line.text}" should contain "${span.text}"');
+
+    var startColumn = span.start.column;
+    var endColumn = span.end.column;
+
+    // Adjust the start and end columns to account for any tabs that were
+    // converted to spaces.
+    final tabsBefore = _countTabs(line.text.substring(0, startColumn));
+    final tabsInside = _countTabs(line.text.substring(startColumn, endColumn));
+    startColumn += tabsBefore * (_spacesPerTab - 1);
+    endColumn += (tabsBefore + tabsInside) * (_spacesPerTab - 1);
+
+    _buffer
+      ..write(' ' * startColumn)
+      ..write(character * math.max(endColumn - startColumn, 1));
+  }
+
+  /// Write an arrow pointing to column [column] in [line].
+  ///
+  /// If the arrow points to a tab character, this will point to the beginning
+  /// of the tab if [beginning] is `true` and the end if it's `false`.
+  void _writeArrow(_Line line, int column, {bool beginning = true}) {
+    final tabs =
+        _countTabs(line.text.substring(0, column + (beginning ? 0 : 1)));
+    _buffer
+      ..write(glyph.horizontalLine * (1 + column + tabs * (_spacesPerTab - 1)))
+      ..write('^');
+  }
+
+  /// Writes [highlight]'s label.
+  ///
+  /// The `_buffer` is assumed to be written to the point where the first line
+  /// of `highlight.label` can be written after a space, but this takes care of
+  /// writing indentation and highlight columns for later lines.
+  ///
+  /// The [highlightsByColumn] are used to write ongoing highlight lines if the
+  /// label is more than one line long.
+  ///
+  /// The [underlineLength] is the length of the line written between the
+  /// highlights and the beginning of the first label.
+  void _writeLabel(_Highlight highlight, List<_Highlight?> highlightsByColumn,
+      int underlineLength) {
+    final label = highlight.label;
+    if (label == null) {
+      _buffer.writeln();
+      return;
+    }
+
+    final lines = label.split('\n');
+    final color = highlight.isPrimary ? _primaryColor : _secondaryColor;
+    _colorize(() => _buffer.write(' ${lines.first}'), color: color);
+    _buffer.writeln();
+
+    for (var text in lines.skip(1)) {
+      _writeSidebar();
+      _buffer.write(' ');
+      for (var columnHighlight in highlightsByColumn) {
+        if (columnHighlight == null || columnHighlight == highlight) {
+          _buffer.write(' ');
+        } else {
+          _buffer.write(glyph.verticalLine);
+        }
+      }
+
+      _buffer.write(' ' * underlineLength);
+      _colorize(() => _buffer.write(' $text'), color: color);
+      _buffer.writeln();
+    }
+  }
+
+  /// Writes a snippet from the source text, converting hard tab characters into
+  /// plain indentation.
+  void _writeText(String text) {
+    for (var char in text.codeUnits) {
+      if (char == $tab) {
+        _buffer.write(' ' * _spacesPerTab);
+      } else {
+        _buffer.writeCharCode(char);
+      }
+    }
+  }
+
+  // Writes a sidebar to [buffer] that includes [line] as the line number if
+  // given and writes [end] at the end (defaults to [glyphs.verticalLine]).
+  //
+  // If [text] is given, it's used in place of the line number. It can't be
+  // passed at the same time as [line].
+  void _writeSidebar({int? line, String? text, String? end}) {
+    assert(line == null || text == null);
+
+    // Add 1 to line to convert from computer-friendly 0-indexed line numbers to
+    // human-friendly 1-indexed line numbers.
+    if (line != null) text = (line + 1).toString();
+    _colorize(() {
+      _buffer
+        ..write((text ?? '').padRight(_paddingBeforeSidebar))
+        ..write(end ?? glyph.verticalLine);
+    }, color: colors.blue);
+  }
+
+  /// Returns the number of hard tabs in [text].
+  int _countTabs(String text) {
+    var count = 0;
+    for (var char in text.codeUnits) {
+      if (char == $tab) count++;
+    }
+    return count;
+  }
+
+  /// Returns whether [text] contains only space or tab characters.
+  bool _isOnlyWhitespace(String text) {
+    for (var char in text.codeUnits) {
+      if (char != $space && char != $tab) return false;
+    }
+    return true;
+  }
+
+  /// Colors all text written to [_buffer] during [callback], if colorization is
+  /// enabled and [color] is not `null`.
+  T _colorize<T>(T Function() callback, {required String? color}) {
+    if (_primaryColor != null && color != null) _buffer.write(color);
+    final result = callback();
+    if (_primaryColor != null && color != null) _buffer.write(colors.none);
+    return result;
+  }
+}
+
+/// Information about how to highlight a single section of a source file.
+class _Highlight {
+  /// The section of the source file to highlight.
+  ///
+  /// This is normalized to make it easier for [Highlighter] to work with.
+  final SourceSpanWithContext span;
+
+  /// Whether this is the primary span in the highlight.
+  ///
+  /// The primary span is highlighted with a different character and colored
+  /// differently than non-primary spans.
+  final bool isPrimary;
+
+  /// The label to include inline when highlighting [span].
+  ///
+  /// This helps distinguish clarify what each highlight means when multiple are
+  /// used in the same message.
+  final String? label;
+
+  _Highlight(SourceSpan span, {String? label, bool primary = false})
+      : span = (() {
+          var newSpan = _normalizeContext(span);
+          newSpan = _normalizeNewlines(newSpan);
+          newSpan = _normalizeTrailingNewline(newSpan);
+          return _normalizeEndOfLine(newSpan);
+        })(),
+        isPrimary = primary,
+        label = label?.replaceAll('\r\n', '\n');
+
+  /// Normalizes [span] to ensure that it's a [SourceSpanWithContext] whose
+  /// context actually contains its text at the expected column.
+  ///
+  /// If it's not already a [SourceSpanWithContext], adjust the start and end
+  /// locations' line and column fields so that the highlighter can assume they
+  /// match up with the context.
+  static SourceSpanWithContext _normalizeContext(SourceSpan span) =>
+      span is SourceSpanWithContext &&
+              findLineStart(span.context, span.text, span.start.column) != null
+          ? span
+          : SourceSpanWithContext(
+              SourceLocation(span.start.offset,
+                  sourceUrl: span.sourceUrl, line: 0, column: 0),
+              SourceLocation(span.end.offset,
+                  sourceUrl: span.sourceUrl,
+                  line: countCodeUnits(span.text, $lf),
+                  column: _lastLineLength(span.text)),
+              span.text,
+              span.text);
+
+  /// Normalizes [span] to replace Windows-style newlines with Unix-style
+  /// newlines.
+  static SourceSpanWithContext _normalizeNewlines(SourceSpanWithContext span) {
+    final text = span.text;
+    if (!text.contains('\r\n')) return span;
+
+    var endOffset = span.end.offset;
+    for (var i = 0; i < text.length - 1; i++) {
+      if (text.codeUnitAt(i) == $cr && text.codeUnitAt(i + 1) == $lf) {
+        endOffset--;
+      }
+    }
+
+    return SourceSpanWithContext(
+        span.start,
+        SourceLocation(endOffset,
+            sourceUrl: span.sourceUrl,
+            line: span.end.line,
+            column: span.end.column),
+        text.replaceAll('\r\n', '\n'),
+        span.context.replaceAll('\r\n', '\n'));
+  }
+
+  /// Normalizes [span] to remove a trailing newline from `span.context`.
+  ///
+  /// If necessary, also adjust `span.end` so that it doesn't point past where
+  /// the trailing newline used to be.
+  static SourceSpanWithContext _normalizeTrailingNewline(
+      SourceSpanWithContext span) {
+    if (!span.context.endsWith('\n')) return span;
+
+    // If there's a full blank line on the end of [span.context], it's probably
+    // significant, so we shouldn't trim it.
+    if (span.text.endsWith('\n\n')) return span;
+
+    final context = span.context.substring(0, span.context.length - 1);
+    var text = span.text;
+    var start = span.start;
+    var end = span.end;
+    if (span.text.endsWith('\n') && _isTextAtEndOfContext(span)) {
+      text = span.text.substring(0, span.text.length - 1);
+      if (text.isEmpty) {
+        end = start;
+      } else {
+        end = SourceLocation(span.end.offset - 1,
+            sourceUrl: span.sourceUrl,
+            line: span.end.line - 1,
+            column: _lastLineLength(context));
+        start = span.start.offset == span.end.offset ? end : span.start;
+      }
+    }
+    return SourceSpanWithContext(start, end, text, context);
+  }
+
+  /// Normalizes [span] so that the end location is at the end of a line rather
+  /// than at the beginning of the next line.
+  static SourceSpanWithContext _normalizeEndOfLine(SourceSpanWithContext span) {
+    if (span.end.column != 0) return span;
+    if (span.end.line == span.start.line) return span;
+
+    final text = span.text.substring(0, span.text.length - 1);
+
+    return SourceSpanWithContext(
+        span.start,
+        SourceLocation(span.end.offset - 1,
+            sourceUrl: span.sourceUrl,
+            line: span.end.line - 1,
+            column: text.length - text.lastIndexOf('\n') - 1),
+        text,
+        // If the context also ends with a newline, it's possible that we don't
+        // have the full context for that line, so we shouldn't print it at all.
+        span.context.endsWith('\n')
+            ? span.context.substring(0, span.context.length - 1)
+            : span.context);
+  }
+
+  /// Returns the length of the last line in [text], whether or not it ends in a
+  /// newline.
+  static int _lastLineLength(String text) {
+    if (text.isEmpty) {
+      return 0;
+    } else if (text.codeUnitAt(text.length - 1) == $lf) {
+      return text.length == 1
+          ? 0
+          : text.length - text.lastIndexOf('\n', text.length - 2) - 1;
+    } else {
+      return text.length - text.lastIndexOf('\n') - 1;
+    }
+  }
+
+  /// Returns whether [span]'s text runs all the way to the end of its context.
+  static bool _isTextAtEndOfContext(SourceSpanWithContext span) =>
+      findLineStart(span.context, span.text, span.start.column)! +
+          span.start.column +
+          span.length ==
+      span.context.length;
+
+  @override
+  String toString() {
+    final buffer = StringBuffer();
+    if (isPrimary) buffer.write('primary ');
+    buffer.write('${span.start.line}:${span.start.column}-'
+        '${span.end.line}:${span.end.column}');
+    if (label != null) buffer.write(' ($label)');
+    return buffer.toString();
+  }
+}
+
+/// A single line of the source file being highlighted.
+class _Line {
+  /// The text of the line, not including the trailing newline.
+  final String text;
+
+  /// The 0-based line number in the source file.
+  final int number;
+
+  /// The URL of the source file in which this line appears.
+  ///
+  /// For lines created from spans without an explicit URL, this is an opaque
+  /// object that differs between lines that come from different spans.
+  final Object url;
+
+  /// All highlights that cover any portion of this line, in source span order.
+  ///
+  /// This is populated after the initial line is created.
+  final highlights = <_Highlight>[];
+
+  _Line(this.text, this.number, this.url);
+
+  @override
+  String toString() => '$number: "$text" (${highlights.join(', ')})';
+}
diff --git a/pkgs/source_span/lib/src/location.dart b/pkgs/source_span/lib/src/location.dart
new file mode 100644
index 0000000..8f22d7b
--- /dev/null
+++ b/pkgs/source_span/lib/src/location.dart
@@ -0,0 +1,102 @@
+// 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 'span.dart';
+
+// TODO(nweiz): Use SourceLocationMixin once we decide to cut a release with
+// breaking changes. See SourceLocationMixin for details.
+
+/// A class that describes a single location within a source file.
+///
+/// This class should not be extended. Instead, [SourceLocationBase] should be
+/// extended instead.
+class SourceLocation implements Comparable<SourceLocation> {
+  /// URL of the source containing this location.
+  ///
+  /// This may be null, indicating that the source URL is unknown or
+  /// unavailable.
+  final Uri? sourceUrl;
+
+  /// The 0-based offset of this location in the source.
+  final int offset;
+
+  /// The 0-based line of this location in the source.
+  final int line;
+
+  /// The 0-based column of this location in the source
+  final int column;
+
+  /// Returns a representation of this location in the `source:line:column`
+  /// format used by text editors.
+  ///
+  /// This prints 1-based lines and columns.
+  String get toolString {
+    final source = sourceUrl ?? 'unknown source';
+    return '$source:${line + 1}:${column + 1}';
+  }
+
+  /// Creates a new location indicating [offset] within [sourceUrl].
+  ///
+  /// [line] and [column] default to assuming the source is a single line. This
+  /// means that [line] defaults to 0 and [column] defaults to [offset].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  SourceLocation(this.offset, {Object? sourceUrl, int? line, int? column})
+      : sourceUrl =
+            sourceUrl is String ? Uri.parse(sourceUrl) : sourceUrl as Uri?,
+        line = line ?? 0,
+        column = column ?? offset {
+    if (offset < 0) {
+      throw RangeError('Offset may not be negative, was $offset.');
+    } else if (line != null && line < 0) {
+      throw RangeError('Line may not be negative, was $line.');
+    } else if (column != null && column < 0) {
+      throw RangeError('Column may not be negative, was $column.');
+    }
+  }
+
+  /// Returns the distance in characters between `this` and [other].
+  ///
+  /// This always returns a non-negative value.
+  int distance(SourceLocation other) {
+    if (sourceUrl != other.sourceUrl) {
+      throw ArgumentError('Source URLs "$sourceUrl" and '
+          "\"${other.sourceUrl}\" don't match.");
+    }
+    return (offset - other.offset).abs();
+  }
+
+  /// Returns a span that covers only a single point: this location.
+  SourceSpan pointSpan() => SourceSpan(this, this, '');
+
+  /// Compares two locations.
+  ///
+  /// [other] must have the same source URL as `this`.
+  @override
+  int compareTo(SourceLocation other) {
+    if (sourceUrl != other.sourceUrl) {
+      throw ArgumentError('Source URLs "$sourceUrl" and '
+          "\"${other.sourceUrl}\" don't match.");
+    }
+    return offset - other.offset;
+  }
+
+  @override
+  bool operator ==(Object other) =>
+      other is SourceLocation &&
+      sourceUrl == other.sourceUrl &&
+      offset == other.offset;
+
+  @override
+  int get hashCode => (sourceUrl?.hashCode ?? 0) + offset;
+
+  @override
+  String toString() => '<$runtimeType: $offset $toolString>';
+}
+
+/// A base class for source locations with [offset], [line], and [column] known
+/// at construction time.
+class SourceLocationBase extends SourceLocation {
+  SourceLocationBase(super.offset, {super.sourceUrl, super.line, super.column});
+}
diff --git a/pkgs/source_span/lib/src/location_mixin.dart b/pkgs/source_span/lib/src/location_mixin.dart
new file mode 100644
index 0000000..a44f5e2
--- /dev/null
+++ b/pkgs/source_span/lib/src/location_mixin.dart
@@ -0,0 +1,55 @@
+// 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 'location.dart';
+import 'span.dart';
+
+// Note: this class duplicates a lot of functionality of [SourceLocation]. This
+// is because in order for SourceLocation to use SourceLocationMixin,
+// SourceLocationMixin couldn't implement SourceLocation. In SourceSpan we
+// handle this by making the class itself non-extensible, but that would be a
+// breaking change for SourceLocation. So until we want to endure the pain of
+// cutting a release with breaking changes, we duplicate the code here.
+
+/// A mixin for easily implementing [SourceLocation].
+abstract class SourceLocationMixin implements SourceLocation {
+  @override
+  String get toolString {
+    final source = sourceUrl ?? 'unknown source';
+    return '$source:${line + 1}:${column + 1}';
+  }
+
+  @override
+  int distance(SourceLocation other) {
+    if (sourceUrl != other.sourceUrl) {
+      throw ArgumentError('Source URLs "$sourceUrl" and '
+          "\"${other.sourceUrl}\" don't match.");
+    }
+    return (offset - other.offset).abs();
+  }
+
+  @override
+  SourceSpan pointSpan() => SourceSpan(this, this, '');
+
+  @override
+  int compareTo(SourceLocation other) {
+    if (sourceUrl != other.sourceUrl) {
+      throw ArgumentError('Source URLs "$sourceUrl" and '
+          "\"${other.sourceUrl}\" don't match.");
+    }
+    return offset - other.offset;
+  }
+
+  @override
+  bool operator ==(Object other) =>
+      other is SourceLocation &&
+      sourceUrl == other.sourceUrl &&
+      offset == other.offset;
+
+  @override
+  int get hashCode => (sourceUrl?.hashCode ?? 0) + offset;
+
+  @override
+  String toString() => '<$runtimeType: $offset $toolString>';
+}
diff --git a/pkgs/source_span/lib/src/span.dart b/pkgs/source_span/lib/src/span.dart
new file mode 100644
index 0000000..941dedc
--- /dev/null
+++ b/pkgs/source_span/lib/src/span.dart
@@ -0,0 +1,193 @@
+// 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:path/path.dart' as p;
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+import 'file.dart';
+import 'highlighter.dart';
+import 'location.dart';
+import 'span_mixin.dart';
+import 'span_with_context.dart';
+import 'utils.dart';
+
+/// A class that describes a segment of source text.
+abstract class SourceSpan implements Comparable<SourceSpan> {
+  /// The start location of this span.
+  SourceLocation get start;
+
+  /// The end location of this span, exclusive.
+  SourceLocation get end;
+
+  /// The source text for this span.
+  String get text;
+
+  /// The URL of the source (typically a file) of this span.
+  ///
+  /// This may be null, indicating that the source URL is unknown or
+  /// unavailable.
+  Uri? get sourceUrl;
+
+  /// The length of this span, in characters.
+  int get length;
+
+  /// Creates a new span from [start] to [end] (exclusive) containing [text].
+  ///
+  /// [start] and [end] must have the same source URL and [start] must come
+  /// before [end]. [text] must have a number of characters equal to the
+  /// distance between [start] and [end].
+  factory SourceSpan(SourceLocation start, SourceLocation end, String text) =>
+      SourceSpanBase(start, end, text);
+
+  /// Creates a new span that's the union of `this` and [other].
+  ///
+  /// The two spans must have the same source URL and may not be disjoint.
+  /// [text] is computed by combining `this.text` and `other.text`.
+  SourceSpan union(SourceSpan other);
+
+  /// Compares two spans.
+  ///
+  /// [other] must have the same source URL as `this`. This orders spans by
+  /// [start] then [length].
+  @override
+  int compareTo(SourceSpan other);
+
+  /// Formats [message] in a human-friendly way associated with this span.
+  ///
+  /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+  /// it indicates an [ANSI terminal color escape][] that should
+  /// be used to highlight the span's text (for example, `"\u001b[31m"` will
+  /// color red). If it's `true`, it indicates that the text should be
+  /// highlighted using the default color. If it's `false` or `null`, it
+  /// indicates that the text shouldn't be highlighted.
+  ///
+  /// This uses the full range of Unicode characters to highlight the source
+  /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
+  /// characters if it's `true`.
+  ///
+  /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+  String message(String message, {Object? color});
+
+  /// Prints the text associated with this span in a user-friendly way.
+  ///
+  /// This is identical to [message], except that it doesn't print the file
+  /// name, line number, column number, or message. If [length] is 0 and this
+  /// isn't a [SourceSpanWithContext], returns an empty string.
+  ///
+  /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+  /// it indicates an [ANSI terminal color escape][] that should
+  /// be used to highlight the span's text (for example, `"\u001b[31m"` will
+  /// color red). If it's `true`, it indicates that the text should be
+  /// highlighted using the default color. If it's `false` or `null`, it
+  /// indicates that the text shouldn't be highlighted.
+  ///
+  /// This uses the full range of Unicode characters to highlight the source
+  /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
+  /// characters if it's `true`.
+  ///
+  /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+  String highlight({Object? color});
+}
+
+/// A base class for source spans with [start], [end], and [text] known at
+/// construction time.
+class SourceSpanBase extends SourceSpanMixin {
+  @override
+  final SourceLocation start;
+  @override
+  final SourceLocation end;
+  @override
+  final String text;
+
+  SourceSpanBase(this.start, this.end, this.text) {
+    if (end.sourceUrl != start.sourceUrl) {
+      throw ArgumentError('Source URLs "${start.sourceUrl}" and '
+          " \"${end.sourceUrl}\" don't match.");
+    } else if (end.offset < start.offset) {
+      throw ArgumentError('End $end must come after start $start.');
+    } else if (text.length != start.distance(end)) {
+      throw ArgumentError('Text "$text" must be ${start.distance(end)} '
+          'characters long.');
+    }
+  }
+}
+
+// TODO(#52): Move these to instance methods in the next breaking release.
+/// Extension methods on the base [SourceSpan] API.
+extension SourceSpanExtension on SourceSpan {
+  /// Like [SourceSpan.message], but also highlights [secondarySpans] to provide
+  /// the user with additional context.
+  ///
+  /// Each span takes a label ([label] for this span, and the values of the
+  /// [secondarySpans] map for the secondary spans) that's used to indicate to
+  /// the user what that particular span represents.
+  ///
+  /// If [color] is `true`, [ANSI terminal color escapes][] are used to color
+  /// the resulting string. By default this span is colored red and the
+  /// secondary spans are colored blue, but that can be customized by passing
+  /// ANSI escape strings to [primaryColor] or [secondaryColor].
+  ///
+  /// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+  ///
+  /// Each span in [secondarySpans] must refer to the same document as this
+  /// span. Throws an [ArgumentError] if any secondary span has a different
+  /// source URL than this span.
+  ///
+  /// Note that while this will work with plain [SourceSpan]s, it will produce
+  /// much more useful output with [SourceSpanWithContext]s (including
+  /// [FileSpan]s).
+  String messageMultiple(
+      String message, String label, Map<SourceSpan, String> secondarySpans,
+      {bool color = false, String? primaryColor, String? secondaryColor}) {
+    final buffer = StringBuffer()
+      ..write('line ${start.line + 1}, column ${start.column + 1}');
+    if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}');
+    buffer
+      ..writeln(': $message')
+      ..write(highlightMultiple(label, secondarySpans,
+          color: color,
+          primaryColor: primaryColor,
+          secondaryColor: secondaryColor));
+    return buffer.toString();
+  }
+
+  /// Like [SourceSpan.highlight], but also highlights [secondarySpans] to
+  /// provide the user with additional context.
+  ///
+  /// Each span takes a label ([label] for this span, and the values of the
+  /// [secondarySpans] map for the secondary spans) that's used to indicate to
+  /// the user what that particular span represents.
+  ///
+  /// If [color] is `true`, [ANSI terminal color escapes][] are used to color
+  /// the resulting string. By default this span is colored red and the
+  /// secondary spans are colored blue, but that can be customized by passing
+  /// ANSI escape strings to [primaryColor] or [secondaryColor].
+  ///
+  /// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+  ///
+  /// Each span in [secondarySpans] must refer to the same document as this
+  /// span. Throws an [ArgumentError] if any secondary span has a different
+  /// source URL than this span.
+  ///
+  /// Note that while this will work with plain [SourceSpan]s, it will produce
+  /// much more useful output with [SourceSpanWithContext]s (including
+  /// [FileSpan]s).
+  String highlightMultiple(String label, Map<SourceSpan, String> secondarySpans,
+          {bool color = false, String? primaryColor, String? secondaryColor}) =>
+      Highlighter.multiple(this, label, secondarySpans,
+              color: color,
+              primaryColor: primaryColor,
+              secondaryColor: secondaryColor)
+          .highlight();
+
+  /// Returns a span from [start] code units (inclusive) to [end] code units
+  /// (exclusive) after the beginning of this span.
+  SourceSpan subspan(int start, [int? end]) {
+    RangeError.checkValidRange(start, end, length);
+    if (start == 0 && (end == null || end == length)) return this;
+
+    final locations = subspanLocations(this, start, end);
+    return SourceSpan(locations[0], locations[1], text.substring(start, end));
+  }
+}
diff --git a/pkgs/source_span/lib/src/span_exception.dart b/pkgs/source_span/lib/src/span_exception.dart
new file mode 100644
index 0000000..90ad690
--- /dev/null
+++ b/pkgs/source_span/lib/src/span_exception.dart
@@ -0,0 +1,114 @@
+// 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 'span.dart';
+
+/// A class for exceptions that have source span information attached.
+class SourceSpanException implements Exception {
+  // This is a getter so that subclasses can override it.
+  /// A message describing the exception.
+  String get message => _message;
+  final String _message;
+
+  // This is a getter so that subclasses can override it.
+  /// The span associated with this exception.
+  ///
+  /// This may be `null` if the source location can't be determined.
+  SourceSpan? get span => _span;
+  final SourceSpan? _span;
+
+  SourceSpanException(this._message, this._span);
+
+  /// Returns a string representation of `this`.
+  ///
+  /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+  /// it indicates an ANSI terminal color escape that should be used to
+  /// highlight the span's text. If it's `true`, it indicates that the text
+  /// should be highlighted using the default color. If it's `false` or `null`,
+  /// it indicates that the text shouldn't be highlighted.
+  @override
+  String toString({Object? color}) {
+    if (span == null) return message;
+    return 'Error on ${span!.message(message, color: color)}';
+  }
+}
+
+/// A [SourceSpanException] that's also a [FormatException].
+class SourceSpanFormatException extends SourceSpanException
+    implements FormatException {
+  @override
+  final dynamic source;
+
+  @override
+  int? get offset => span?.start.offset;
+
+  SourceSpanFormatException(super.message, super.span, [this.source]);
+}
+
+/// A [SourceSpanException] that also highlights some secondary spans to provide
+/// the user with extra context.
+///
+/// Each span has a label ([primaryLabel] for the primary, and the values of the
+/// [secondarySpans] map for the secondary spans) that's used to indicate to the
+/// user what that particular span represents.
+class MultiSourceSpanException extends SourceSpanException {
+  /// A label to attach to [span] that provides additional information and helps
+  /// distinguish it from [secondarySpans].
+  final String primaryLabel;
+
+  /// A map whose keys are secondary spans that should be highlighted.
+  ///
+  /// Each span's value is a label to attach to that span that provides
+  /// additional information and helps distinguish it from [secondarySpans].
+  final Map<SourceSpan, String> secondarySpans;
+
+  MultiSourceSpanException(super.message, super.span, this.primaryLabel,
+      Map<SourceSpan, String> secondarySpans)
+      : secondarySpans = Map.unmodifiable(secondarySpans);
+
+  /// Returns a string representation of `this`.
+  ///
+  /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+  /// it indicates an ANSI terminal color escape that should be used to
+  /// highlight the primary span's text. If it's `true`, it indicates that the
+  /// text should be highlighted using the default color. If it's `false` or
+  /// `null`, it indicates that the text shouldn't be highlighted.
+  ///
+  /// If [color] is `true` or a string, [secondaryColor] is used to highlight
+  /// [secondarySpans].
+  @override
+  String toString({Object? color, String? secondaryColor}) {
+    if (span == null) return message;
+
+    var useColor = false;
+    String? primaryColor;
+    if (color is String) {
+      useColor = true;
+      primaryColor = color;
+    } else if (color == true) {
+      useColor = true;
+    }
+
+    final formatted = span!.messageMultiple(
+        message, primaryLabel, secondarySpans,
+        color: useColor,
+        primaryColor: primaryColor,
+        secondaryColor: secondaryColor);
+    return 'Error on $formatted';
+  }
+}
+
+/// A [MultiSourceSpanException] that's also a [FormatException].
+class MultiSourceSpanFormatException extends MultiSourceSpanException
+    implements FormatException {
+  @override
+  final dynamic source;
+
+  @override
+  int? get offset => span?.start.offset;
+
+  MultiSourceSpanFormatException(
+      super.message, super.span, super.primaryLabel, super.secondarySpans,
+      [this.source]);
+}
diff --git a/pkgs/source_span/lib/src/span_mixin.dart b/pkgs/source_span/lib/src/span_mixin.dart
new file mode 100644
index 0000000..29b6119
--- /dev/null
+++ b/pkgs/source_span/lib/src/span_mixin.dart
@@ -0,0 +1,84 @@
+// 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:path/path.dart' as p;
+
+import 'highlighter.dart';
+import 'span.dart';
+import 'span_with_context.dart';
+import 'utils.dart';
+
+/// A mixin for easily implementing [SourceSpan].
+///
+/// This implements the [SourceSpan] methods in terms of [start], [end], and
+/// [text]. This assumes that [start] and [end] have the same source URL, that
+/// [start] comes before [end], and that [text] has a number of characters equal
+/// to the distance between [start] and [end].
+abstract class SourceSpanMixin implements SourceSpan {
+  @override
+  Uri? get sourceUrl => start.sourceUrl;
+
+  @override
+  int get length => end.offset - start.offset;
+
+  @override
+  int compareTo(SourceSpan other) {
+    final result = start.compareTo(other.start);
+    return result == 0 ? end.compareTo(other.end) : result;
+  }
+
+  @override
+  SourceSpan union(SourceSpan other) {
+    if (sourceUrl != other.sourceUrl) {
+      throw ArgumentError('Source URLs "$sourceUrl" and '
+          " \"${other.sourceUrl}\" don't match.");
+    }
+
+    final start = min(this.start, other.start);
+    final end = max(this.end, other.end);
+    final beginSpan = start == this.start ? this : other;
+    final endSpan = end == this.end ? this : other;
+
+    if (beginSpan.end.compareTo(endSpan.start) < 0) {
+      throw ArgumentError('Spans $this and $other are disjoint.');
+    }
+
+    final text = beginSpan.text +
+        endSpan.text.substring(beginSpan.end.distance(endSpan.start));
+    return SourceSpan(start, end, text);
+  }
+
+  @override
+  String message(String message, {Object? color}) {
+    final buffer = StringBuffer()
+      ..write('line ${start.line + 1}, column ${start.column + 1}');
+    if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}');
+    buffer.write(': $message');
+
+    final highlight = this.highlight(color: color);
+    if (highlight.isNotEmpty) {
+      buffer
+        ..writeln()
+        ..write(highlight);
+    }
+
+    return buffer.toString();
+  }
+
+  @override
+  String highlight({Object? color}) {
+    if (this is! SourceSpanWithContext && length == 0) return '';
+    return Highlighter(this, color: color).highlight();
+  }
+
+  @override
+  bool operator ==(Object other) =>
+      other is SourceSpan && start == other.start && end == other.end;
+
+  @override
+  int get hashCode => Object.hash(start, end);
+
+  @override
+  String toString() => '<$runtimeType: from $start to $end "$text">';
+}
diff --git a/pkgs/source_span/lib/src/span_with_context.dart b/pkgs/source_span/lib/src/span_with_context.dart
new file mode 100644
index 0000000..776c789
--- /dev/null
+++ b/pkgs/source_span/lib/src/span_with_context.dart
@@ -0,0 +1,51 @@
+// 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 'location.dart';
+import 'span.dart';
+import 'utils.dart';
+
+/// A class that describes a segment of source text with additional context.
+class SourceSpanWithContext extends SourceSpanBase {
+  // This is a getter so that subclasses can override it.
+  /// Text around the span, which includes the line containing this span.
+  String get context => _context;
+  final String _context;
+
+  /// Creates a new span from [start] to [end] (exclusive) containing [text], in
+  /// the given [context].
+  ///
+  /// [start] and [end] must have the same source URL and [start] must come
+  /// before [end]. [text] must have a number of characters equal to the
+  /// distance between [start] and [end]. [context] must contain [text], and
+  /// [text] should start at `start.column` from the beginning of a line in
+  /// [context].
+  SourceSpanWithContext(
+      SourceLocation start, SourceLocation end, String text, this._context)
+      : super(start, end, text) {
+    if (!context.contains(text)) {
+      throw ArgumentError('The context line "$context" must contain "$text".');
+    }
+
+    if (findLineStart(context, text, start.column) == null) {
+      throw ArgumentError('The span text "$text" must start at '
+          'column ${start.column + 1} in a line within "$context".');
+    }
+  }
+}
+
+// TODO(#52): Move these to instance methods in the next breaking release.
+/// Extension methods on the base [SourceSpan] API.
+extension SourceSpanWithContextExtension on SourceSpanWithContext {
+  /// Returns a span from [start] code units (inclusive) to [end] code units
+  /// (exclusive) after the beginning of this span.
+  SourceSpanWithContext subspan(int start, [int? end]) {
+    RangeError.checkValidRange(start, end, length);
+    if (start == 0 && (end == null || end == length)) return this;
+
+    final locations = subspanLocations(this, start, end);
+    return SourceSpanWithContext(
+        locations[0], locations[1], text.substring(start, end), context);
+  }
+}
diff --git a/pkgs/source_span/lib/src/utils.dart b/pkgs/source_span/lib/src/utils.dart
new file mode 100644
index 0000000..aba14ec
--- /dev/null
+++ b/pkgs/source_span/lib/src/utils.dart
@@ -0,0 +1,145 @@
+// 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 'charcode.dart';
+import 'location.dart';
+import 'span.dart';
+import 'span_with_context.dart';
+
+/// Returns the minimum of [obj1] and [obj2] according to
+/// [Comparable.compareTo].
+T min<T extends Comparable<T>>(T obj1, T obj2) =>
+    obj1.compareTo(obj2) > 0 ? obj2 : obj1;
+
+/// Returns the maximum of [obj1] and [obj2] according to
+/// [Comparable.compareTo].
+T max<T extends Comparable<T>>(T obj1, T obj2) =>
+    obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+
+/// Returns whether all elements of [iter] are the same value, according to
+/// `==`.
+bool isAllTheSame(Iterable<Object?> iter) {
+  if (iter.isEmpty) return true;
+  final firstValue = iter.first;
+  for (var value in iter.skip(1)) {
+    if (value != firstValue) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/// Returns whether [span] covers multiple lines.
+bool isMultiline(SourceSpan span) => span.start.line != span.end.line;
+
+/// Sets the first `null` element of [list] to [element].
+void replaceFirstNull<E>(List<E?> list, E element) {
+  final index = list.indexOf(null);
+  if (index < 0) throw ArgumentError('$list contains no null elements.');
+  list[index] = element;
+}
+
+/// Sets the element of [list] that currently contains [element] to `null`.
+void replaceWithNull<E>(List<E?> list, E element) {
+  final index = list.indexOf(element);
+  if (index < 0) {
+    throw ArgumentError('$list contains no elements matching $element.');
+  }
+
+  list[index] = null;
+}
+
+/// Returns the number of instances of [codeUnit] in [string].
+int countCodeUnits(String string, int codeUnit) {
+  var count = 0;
+  for (var codeUnitToCheck in string.codeUnits) {
+    if (codeUnitToCheck == codeUnit) count++;
+  }
+  return count;
+}
+
+/// Finds a line in [context] containing [text] at the specified [column].
+///
+/// Returns the index in [context] where that line begins, or null if none
+/// exists.
+int? findLineStart(String context, String text, int column) {
+  // If the text is empty, we just want to find the first line that has at least
+  // [column] characters.
+  if (text.isEmpty) {
+    var beginningOfLine = 0;
+    while (true) {
+      final index = context.indexOf('\n', beginningOfLine);
+      if (index == -1) {
+        return context.length - beginningOfLine >= column
+            ? beginningOfLine
+            : null;
+      }
+
+      if (index - beginningOfLine >= column) return beginningOfLine;
+      beginningOfLine = index + 1;
+    }
+  }
+
+  var index = context.indexOf(text);
+  while (index != -1) {
+    // Start looking before [index] in case [text] starts with a newline.
+    final lineStart = index == 0 ? 0 : context.lastIndexOf('\n', index - 1) + 1;
+    final textColumn = index - lineStart;
+    if (column == textColumn) return lineStart;
+    index = context.indexOf(text, index + 1);
+  }
+  // ignore: avoid_returning_null
+  return null;
+}
+
+/// Returns a two-element list containing the start and end locations of the
+/// span from [start] code units (inclusive) to [end] code units (exclusive)
+/// after the beginning of [span].
+///
+/// This is factored out so it can be shared between
+/// [SourceSpanExtension.subspan] and [SourceSpanWithContextExtension.subspan].
+List<SourceLocation> subspanLocations(SourceSpan span, int start, [int? end]) {
+  final text = span.text;
+  final startLocation = span.start;
+  var line = startLocation.line;
+  var column = startLocation.column;
+
+  // Adjust [line] and [column] as necessary if the character at [i] in [text]
+  // is a newline.
+  void consumeCodePoint(int i) {
+    final codeUnit = text.codeUnitAt(i);
+    if (codeUnit == $lf ||
+        // A carriage return counts as a newline, but only if it's not
+        // followed by a line feed.
+        (codeUnit == $cr &&
+            (i + 1 == text.length || text.codeUnitAt(i + 1) != $lf))) {
+      line += 1;
+      column = 0;
+    } else {
+      column += 1;
+    }
+  }
+
+  for (var i = 0; i < start; i++) {
+    consumeCodePoint(i);
+  }
+
+  final newStartLocation = SourceLocation(startLocation.offset + start,
+      sourceUrl: span.sourceUrl, line: line, column: column);
+
+  SourceLocation newEndLocation;
+  if (end == null || end == span.length) {
+    newEndLocation = span.end;
+  } else if (end == start) {
+    newEndLocation = newStartLocation;
+  } else {
+    for (var i = start; i < end; i++) {
+      consumeCodePoint(i);
+    }
+    newEndLocation = SourceLocation(startLocation.offset + end,
+        sourceUrl: span.sourceUrl, line: line, column: column);
+  }
+
+  return [newStartLocation, newEndLocation];
+}
diff --git a/pkgs/source_span/pubspec.yaml b/pkgs/source_span/pubspec.yaml
new file mode 100644
index 0000000..8757b2d
--- /dev/null
+++ b/pkgs/source_span/pubspec.yaml
@@ -0,0 +1,17 @@
+name: source_span
+version: 1.10.1
+description: >-
+  Provides a standard representation for source code locations and spans.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/source_span
+
+environment:
+  sdk: ^3.1.0
+
+dependencies:
+  collection: ^1.15.0
+  path: ^1.8.0
+  term_glyph: ^1.2.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.0
diff --git a/pkgs/source_span/test/file_test.dart b/pkgs/source_span/test/file_test.dart
new file mode 100644
index 0000000..dff51ee
--- /dev/null
+++ b/pkgs/source_span/test/file_test.dart
@@ -0,0 +1,530 @@
+// 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:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late SourceFile file;
+  setUp(() {
+    file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop''', url: 'foo.dart');
+  });
+
+  group('errors', () {
+    group('for span()', () {
+      test('end must come after start', () {
+        expect(() => file.span(10, 5), throwsArgumentError);
+      });
+
+      test('start may not be negative', () {
+        expect(() => file.span(-1, 5), throwsRangeError);
+      });
+
+      test('end may not be outside the file', () {
+        expect(() => file.span(10, 100), throwsRangeError);
+      });
+    });
+
+    group('for location()', () {
+      test('offset may not be negative', () {
+        expect(() => file.location(-1), throwsRangeError);
+      });
+
+      test('offset may not be outside the file', () {
+        expect(() => file.location(100), throwsRangeError);
+      });
+    });
+
+    group('for getLine()', () {
+      test('offset may not be negative', () {
+        expect(() => file.getLine(-1), throwsRangeError);
+      });
+
+      test('offset may not be outside the file', () {
+        expect(() => file.getLine(100), throwsRangeError);
+      });
+    });
+
+    group('for getColumn()', () {
+      test('offset may not be negative', () {
+        expect(() => file.getColumn(-1), throwsRangeError);
+      });
+
+      test('offset may not be outside the file', () {
+        expect(() => file.getColumn(100), throwsRangeError);
+      });
+
+      test('line may not be negative', () {
+        expect(() => file.getColumn(1, line: -1), throwsRangeError);
+      });
+
+      test('line may not be outside the file', () {
+        expect(() => file.getColumn(1, line: 100), throwsRangeError);
+      });
+
+      test('line must be accurate', () {
+        expect(() => file.getColumn(1, line: 1), throwsRangeError);
+      });
+    });
+
+    group('getOffset()', () {
+      test('line may not be negative', () {
+        expect(() => file.getOffset(-1), throwsRangeError);
+      });
+
+      test('column may not be negative', () {
+        expect(() => file.getOffset(1, -1), throwsRangeError);
+      });
+
+      test('line may not be outside the file', () {
+        expect(() => file.getOffset(100), throwsRangeError);
+      });
+
+      test('column may not be outside the file', () {
+        expect(() => file.getOffset(2, 100), throwsRangeError);
+      });
+
+      test('column may not be outside the line', () {
+        expect(() => file.getOffset(1, 20), throwsRangeError);
+      });
+    });
+
+    group('for getText()', () {
+      test('end must come after start', () {
+        expect(() => file.getText(10, 5), throwsArgumentError);
+      });
+
+      test('start may not be negative', () {
+        expect(() => file.getText(-1, 5), throwsRangeError);
+      });
+
+      test('end may not be outside the file', () {
+        expect(() => file.getText(10, 100), throwsRangeError);
+      });
+    });
+
+    group('for span().union()', () {
+      test('source URLs must match', () {
+        final other = SourceSpan(SourceLocation(10), SourceLocation(11), '_');
+
+        expect(() => file.span(9, 10).union(other), throwsArgumentError);
+      });
+
+      test('spans may not be disjoint', () {
+        expect(() => file.span(9, 10).union(file.span(11, 12)),
+            throwsArgumentError);
+      });
+    });
+
+    test('for span().expand() source URLs must match', () {
+      final other = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop''', url: 'bar.dart').span(10, 11);
+
+      expect(() => file.span(9, 10).expand(other), throwsArgumentError);
+    });
+  });
+
+  test('fields work correctly', () {
+    expect(file.url, equals(Uri.parse('foo.dart')));
+    expect(file.lines, equals(3));
+    expect(file.length, equals(38));
+  });
+
+  group('new SourceFile()', () {
+    test('handles CRLF correctly', () {
+      expect(SourceFile.fromString('foo\r\nbar').getLine(6), equals(1));
+    });
+
+    test('handles a lone CR correctly', () {
+      expect(SourceFile.fromString('foo\rbar').getLine(5), equals(1));
+    });
+  });
+
+  group('span()', () {
+    test('returns a span between the given offsets', () {
+      final span = file.span(5, 10);
+      expect(span.start, equals(file.location(5)));
+      expect(span.end, equals(file.location(10)));
+    });
+
+    test('end defaults to the end of the file', () {
+      final span = file.span(5);
+      expect(span.start, equals(file.location(5)));
+      expect(span.end, equals(file.location(file.length)));
+    });
+  });
+
+  group('getLine()', () {
+    test('works for a middle character on the line', () {
+      expect(file.getLine(15), equals(1));
+    });
+
+    test('works for the first character of a line', () {
+      expect(file.getLine(12), equals(1));
+    });
+
+    test('works for a newline character', () {
+      expect(file.getLine(11), equals(0));
+    });
+
+    test('works for the last offset', () {
+      expect(file.getLine(file.length), equals(2));
+    });
+  });
+
+  group('getColumn()', () {
+    test('works for a middle character on the line', () {
+      expect(file.getColumn(15), equals(3));
+    });
+
+    test('works for the first character of a line', () {
+      expect(file.getColumn(12), equals(0));
+    });
+
+    test('works for a newline character', () {
+      expect(file.getColumn(11), equals(11));
+    });
+
+    test('works when line is passed as well', () {
+      expect(file.getColumn(12, line: 1), equals(0));
+    });
+
+    test('works for the last offset', () {
+      expect(file.getColumn(file.length), equals(11));
+    });
+  });
+
+  group('getOffset()', () {
+    test('works for a middle character on the line', () {
+      expect(file.getOffset(1, 3), equals(15));
+    });
+
+    test('works for the first character of a line', () {
+      expect(file.getOffset(1), equals(12));
+    });
+
+    test('works for a newline character', () {
+      expect(file.getOffset(0, 11), equals(11));
+    });
+
+    test('works for the last offset', () {
+      expect(file.getOffset(2, 11), equals(file.length));
+    });
+  });
+
+  group('getText()', () {
+    test('returns a substring of the source', () {
+      expect(file.getText(8, 15), equals('baz\nwhi'));
+    });
+
+    test('end defaults to the end of the file', () {
+      expect(file.getText(20), equals('g boom\nzip zap zop'));
+    });
+  });
+
+  group('FileLocation', () {
+    test('reports the correct line number', () {
+      expect(file.location(15).line, equals(1));
+    });
+
+    test('reports the correct column number', () {
+      expect(file.location(15).column, equals(3));
+    });
+
+    test('pointSpan() returns a FileSpan', () {
+      final location = file.location(15);
+      final span = location.pointSpan();
+      expect(span, isA<FileSpan>());
+      expect(span.start, equals(location));
+      expect(span.end, equals(location));
+      expect(span.text, isEmpty);
+    });
+  });
+
+  group('FileSpan', () {
+    test('text returns a substring of the source', () {
+      expect(file.span(8, 15).text, equals('baz\nwhi'));
+    });
+
+    test('text includes the last char when end is defaulted to EOF', () {
+      expect(file.span(29).text, equals('p zap zop'));
+    });
+
+    group('context', () {
+      test("contains the span's text", () {
+        final span = file.span(8, 15);
+        expect(span.context.contains(span.text), isTrue);
+        expect(span.context, equals('foo bar baz\nwhiz bang boom\n'));
+      });
+
+      test('contains the previous line for a point span at the end of a line',
+          () {
+        final span = file.span(25, 25);
+        expect(span.context, equals('whiz bang boom\n'));
+      });
+
+      test('contains the next line for a point span at the beginning of a line',
+          () {
+        final span = file.span(12, 12);
+        expect(span.context, equals('whiz bang boom\n'));
+      });
+
+      group('for a point span at the end of a file', () {
+        test('without a newline, contains the last line', () {
+          final span = file.span(file.length, file.length);
+          expect(span.context, equals('zip zap zop'));
+        });
+
+        test('with a newline, contains an empty line', () {
+          file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop
+''', url: 'foo.dart');
+
+          final span = file.span(file.length, file.length);
+          expect(span.context, isEmpty);
+        });
+      });
+    });
+
+    group('union()', () {
+      late FileSpan span;
+      setUp(() {
+        span = file.span(5, 12);
+      });
+
+      test('works with a preceding adjacent span', () {
+        final other = file.span(0, 5);
+        final result = span.union(other);
+        expect(result.start, equals(other.start));
+        expect(result.end, equals(span.end));
+        expect(result.text, equals('foo bar baz\n'));
+      });
+
+      test('works with a preceding overlapping span', () {
+        final other = file.span(0, 8);
+        final result = span.union(other);
+        expect(result.start, equals(other.start));
+        expect(result.end, equals(span.end));
+        expect(result.text, equals('foo bar baz\n'));
+      });
+
+      test('works with a following adjacent span', () {
+        final other = file.span(12, 16);
+        final result = span.union(other);
+        expect(result.start, equals(span.start));
+        expect(result.end, equals(other.end));
+        expect(result.text, equals('ar baz\nwhiz'));
+      });
+
+      test('works with a following overlapping span', () {
+        final other = file.span(9, 16);
+        final result = span.union(other);
+        expect(result.start, equals(span.start));
+        expect(result.end, equals(other.end));
+        expect(result.text, equals('ar baz\nwhiz'));
+      });
+
+      test('works with an internal overlapping span', () {
+        final other = file.span(7, 10);
+        expect(span.union(other), equals(span));
+      });
+
+      test('works with an external overlapping span', () {
+        final other = file.span(0, 16);
+        expect(span.union(other), equals(other));
+      });
+
+      test('returns a FileSpan for a FileSpan input', () {
+        expect(span.union(file.span(0, 5)), isA<FileSpan>());
+      });
+
+      test('returns a base SourceSpan for a SourceSpan input', () {
+        final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+            SourceLocation(5, sourceUrl: 'foo.dart'), 'hey, ');
+        final result = span.union(other);
+        expect(result, isNot(isA<FileSpan>()));
+        expect(result.start, equals(other.start));
+        expect(result.end, equals(span.end));
+        expect(result.text, equals('hey, ar baz\n'));
+      });
+    });
+
+    group('expand()', () {
+      late FileSpan span;
+      setUp(() {
+        span = file.span(5, 12);
+      });
+
+      test('works with a preceding nonadjacent span', () {
+        final other = file.span(0, 3);
+        final result = span.expand(other);
+        expect(result.start, equals(other.start));
+        expect(result.end, equals(span.end));
+        expect(result.text, equals('foo bar baz\n'));
+      });
+
+      test('works with a preceding overlapping span', () {
+        final other = file.span(0, 8);
+        final result = span.expand(other);
+        expect(result.start, equals(other.start));
+        expect(result.end, equals(span.end));
+        expect(result.text, equals('foo bar baz\n'));
+      });
+
+      test('works with a following nonadjacent span', () {
+        final other = file.span(14, 16);
+        final result = span.expand(other);
+        expect(result.start, equals(span.start));
+        expect(result.end, equals(other.end));
+        expect(result.text, equals('ar baz\nwhiz'));
+      });
+
+      test('works with a following overlapping span', () {
+        final other = file.span(9, 16);
+        final result = span.expand(other);
+        expect(result.start, equals(span.start));
+        expect(result.end, equals(other.end));
+        expect(result.text, equals('ar baz\nwhiz'));
+      });
+
+      test('works with an internal overlapping span', () {
+        final other = file.span(7, 10);
+        expect(span.expand(other), equals(span));
+      });
+
+      test('works with an external overlapping span', () {
+        final other = file.span(0, 16);
+        expect(span.expand(other), equals(other));
+      });
+    });
+
+    group('subspan()', () {
+      late FileSpan span;
+      setUp(() {
+        span = file.span(5, 11); // "ar baz"
+      });
+
+      group('errors', () {
+        test('start must be greater than zero', () {
+          expect(() => span.subspan(-1), throwsRangeError);
+        });
+
+        test('start must be less than or equal to length', () {
+          expect(() => span.subspan(span.length + 1), throwsRangeError);
+        });
+
+        test('end must be greater than start', () {
+          expect(() => span.subspan(2, 1), throwsRangeError);
+        });
+
+        test('end must be less than or equal to length', () {
+          expect(() => span.subspan(0, span.length + 1), throwsRangeError);
+        });
+      });
+
+      test('preserves the source URL', () {
+        final result = span.subspan(1, 2);
+        expect(result.start.sourceUrl, equals(span.sourceUrl));
+        expect(result.end.sourceUrl, equals(span.sourceUrl));
+      });
+
+      group('returns the original span', () {
+        test('with an implicit end',
+            () => expect(span.subspan(0), equals(span)));
+
+        test('with an explicit end',
+            () => expect(span.subspan(0, span.length), equals(span)));
+      });
+
+      group('within a single line', () {
+        test('returns a strict substring of the original span', () {
+          final result = span.subspan(1, 5);
+          expect(result.text, equals('r ba'));
+          expect(result.start.offset, equals(6));
+          expect(result.start.line, equals(0));
+          expect(result.start.column, equals(6));
+          expect(result.end.offset, equals(10));
+          expect(result.end.line, equals(0));
+          expect(result.end.column, equals(10));
+        });
+
+        test('an implicit end goes to the end of the original span', () {
+          final result = span.subspan(1);
+          expect(result.text, equals('r baz'));
+          expect(result.start.offset, equals(6));
+          expect(result.start.line, equals(0));
+          expect(result.start.column, equals(6));
+          expect(result.end.offset, equals(11));
+          expect(result.end.line, equals(0));
+          expect(result.end.column, equals(11));
+        });
+
+        test('can return an empty span', () {
+          final result = span.subspan(3, 3);
+          expect(result.text, isEmpty);
+          expect(result.start.offset, equals(8));
+          expect(result.start.line, equals(0));
+          expect(result.start.column, equals(8));
+          expect(result.end, equals(result.start));
+        });
+      });
+
+      group('across multiple lines', () {
+        setUp(() {
+          span = file.span(22, 30); // "boom\nzip"
+        });
+
+        test('with start and end in the middle of a line', () {
+          final result = span.subspan(3, 6);
+          expect(result.text, equals('m\nz'));
+          expect(result.start.offset, equals(25));
+          expect(result.start.line, equals(1));
+          expect(result.start.column, equals(13));
+          expect(result.end.offset, equals(28));
+          expect(result.end.line, equals(2));
+          expect(result.end.column, equals(1));
+        });
+
+        test('with start at the end of a line', () {
+          final result = span.subspan(4, 6);
+          expect(result.text, equals('\nz'));
+          expect(result.start.offset, equals(26));
+          expect(result.start.line, equals(1));
+          expect(result.start.column, equals(14));
+        });
+
+        test('with start at the beginning of a line', () {
+          final result = span.subspan(5, 6);
+          expect(result.text, equals('z'));
+          expect(result.start.offset, equals(27));
+          expect(result.start.line, equals(2));
+          expect(result.start.column, equals(0));
+        });
+
+        test('with end at the end of a line', () {
+          final result = span.subspan(3, 4);
+          expect(result.text, equals('m'));
+          expect(result.end.offset, equals(26));
+          expect(result.end.line, equals(1));
+          expect(result.end.column, equals(14));
+        });
+
+        test('with end at the beginning of a line', () {
+          final result = span.subspan(3, 5);
+          expect(result.text, equals('m\n'));
+          expect(result.end.offset, equals(27));
+          expect(result.end.line, equals(2));
+          expect(result.end.column, equals(0));
+        });
+      });
+    });
+  });
+}
diff --git a/pkgs/source_span/test/highlight_test.dart b/pkgs/source_span/test/highlight_test.dart
new file mode 100644
index 0000000..93c42db
--- /dev/null
+++ b/pkgs/source_span/test/highlight_test.dart
@@ -0,0 +1,605 @@
+// 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.
+
+// ignore_for_file: prefer_interpolation_to_compose_strings
+
+import 'package:source_span/source_span.dart';
+import 'package:source_span/src/colors.dart' as colors;
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+
+void main() {
+  late bool oldAscii;
+  setUpAll(() {
+    oldAscii = glyph.ascii;
+    glyph.ascii = true;
+  });
+
+  tearDownAll(() {
+    glyph.ascii = oldAscii;
+  });
+
+  late SourceFile file;
+  setUp(() {
+    file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop
+''');
+  });
+
+  test('points to the span in the source', () {
+    expect(file.span(4, 7).highlight(), equals("""
+  ,
+1 | foo bar baz
+  |     ^^^
+  '"""));
+  });
+
+  test('gracefully handles a missing source URL', () {
+    final span = SourceFile.fromString('foo bar baz').span(4, 7);
+    expect(span.highlight(), equals("""
+  ,
+1 | foo bar baz
+  |     ^^^
+  '"""));
+  });
+
+  group('highlights a point span', () {
+    test('in the middle of a line', () {
+      expect(file.location(4).pointSpan().highlight(), equals("""
+  ,
+1 | foo bar baz
+  |     ^
+  '"""));
+    });
+
+    test('at the beginning of the file', () {
+      expect(file.location(0).pointSpan().highlight(), equals("""
+  ,
+1 | foo bar baz
+  | ^
+  '"""));
+    });
+
+    test('at the beginning of a line', () {
+      expect(file.location(12).pointSpan().highlight(), equals("""
+  ,
+2 | whiz bang boom
+  | ^
+  '"""));
+    });
+
+    test('at the end of a line', () {
+      expect(file.location(11).pointSpan().highlight(), equals("""
+  ,
+1 | foo bar baz
+  |            ^
+  '"""));
+    });
+
+    test('at the end of the file', () {
+      expect(file.location(38).pointSpan().highlight(), equals("""
+  ,
+3 | zip zap zop
+  |            ^
+  '"""));
+    });
+
+    test('after the end of the file', () {
+      expect(file.location(39).pointSpan().highlight(), equals("""
+  ,
+4 | 
+  | ^
+  '"""));
+    });
+
+    test('at the end of the file with no trailing newline', () {
+      file = SourceFile.fromString('zip zap zop');
+      expect(file.location(10).pointSpan().highlight(), equals("""
+  ,
+1 | zip zap zop
+  |           ^
+  '"""));
+    });
+
+    test('after the end of the file with no trailing newline', () {
+      file = SourceFile.fromString('zip zap zop');
+      expect(file.location(11).pointSpan().highlight(), equals("""
+  ,
+1 | zip zap zop
+  |            ^
+  '"""));
+    });
+
+    test('in an empty file', () {
+      expect(SourceFile.fromString('').location(0).pointSpan().highlight(),
+          equals("""
+  ,
+1 | 
+  | ^
+  '"""));
+    });
+
+    test('on an empty line', () {
+      final file = SourceFile.fromString('foo\n\nbar');
+      expect(file.location(4).pointSpan().highlight(), equals("""
+  ,
+2 | 
+  | ^
+  '"""));
+    });
+  });
+
+  test('highlights a single-line file without a newline', () {
+    expect(SourceFile.fromString('foo bar').span(0, 7).highlight(), equals("""
+  ,
+1 | foo bar
+  | ^^^^^^^
+  '"""));
+  });
+
+  test('highlights text including a trailing newline', () {
+    expect(file.span(8, 12).highlight(), equals("""
+  ,
+1 | foo bar baz
+  |         ^^^
+  '"""));
+  });
+
+  test('highlights a single empty line', () {
+    expect(
+        SourceFile.fromString('foo\n\nbar').span(4, 5).highlight(), equals("""
+  ,
+2 | 
+  | ^
+  '"""));
+  });
+
+  test('highlights a trailing newline', () {
+    expect(file.span(11, 12).highlight(), equals("""
+  ,
+1 | foo bar baz
+  |            ^
+  '"""));
+  });
+
+  group('with a multiline span', () {
+    test('highlights the middle of the first and last lines', () {
+      expect(file.span(4, 34).highlight(), equals("""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | | whiz bang boom
+3 | | zip zap zop
+  | '-------^
+  '"""));
+    });
+
+    test('works when it begins at the end of a line', () {
+      expect(file.span(11, 34).highlight(), equals("""
+  ,
+1 |   foo bar baz
+  | ,------------^
+2 | | whiz bang boom
+3 | | zip zap zop
+  | '-------^
+  '"""));
+    });
+
+    test('works when it ends at the beginning of a line', () {
+      expect(file.span(4, 28).highlight(), equals("""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | | whiz bang boom
+3 | | zip zap zop
+  | '-^
+  '"""));
+    });
+
+    test('highlights the full first line', () {
+      expect(file.span(0, 34).highlight(), equals("""
+  ,
+1 | / foo bar baz
+2 | | whiz bang boom
+3 | | zip zap zop
+  | '-------^
+  '"""));
+    });
+
+    test("highlights the full first line even if it's indented", () {
+      final file = SourceFile.fromString('''
+  foo bar baz
+  whiz bang boom
+  zip zap zop
+''');
+
+      expect(file.span(2, 38).highlight(), equals("""
+  ,
+1 | /   foo bar baz
+2 | |   whiz bang boom
+3 | |   zip zap zop
+  | '-------^
+  '"""));
+    });
+
+    test("highlights the full first line if it's empty", () {
+      final file = SourceFile.fromString('''
+foo
+
+bar
+''');
+
+      expect(file.span(4, 9).highlight(), equals(r"""
+  ,
+2 | / 
+3 | \ bar
+  '"""));
+    });
+
+    test('highlights the full last line', () {
+      expect(file.span(4, 27).highlight(), equals(r"""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | \ whiz bang boom
+  '"""));
+    });
+
+    test('highlights the full last line with no trailing newline', () {
+      expect(file.span(4, 26).highlight(), equals(r"""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | \ whiz bang boom
+  '"""));
+    });
+
+    test('highlights the full last line with a trailing Windows newline', () {
+      final file = SourceFile.fromString('''
+foo bar baz\r
+whiz bang boom\r
+zip zap zop\r
+''');
+
+      expect(file.span(4, 29).highlight(), equals(r"""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | \ whiz bang boom
+  '"""));
+    });
+
+    test('highlights the full last line at the end of the file', () {
+      expect(file.span(4, 39).highlight(), equals(r"""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | | whiz bang boom
+3 | \ zip zap zop
+  '"""));
+    });
+
+    test(
+        'highlights the full last line at the end of the file with no trailing '
+        'newline', () {
+      final file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop''');
+
+      expect(file.span(4, 38).highlight(), equals(r"""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | | whiz bang boom
+3 | \ zip zap zop
+  '"""));
+    });
+
+    test("highlights the full last line if it's empty", () {
+      final file = SourceFile.fromString('''
+foo
+
+bar
+''');
+
+      expect(file.span(0, 5).highlight(), equals(r"""
+  ,
+1 | / foo
+2 | \ 
+  '"""));
+    });
+
+    test('highlights multiple empty lines', () {
+      final file = SourceFile.fromString('foo\n\n\n\nbar');
+      expect(file.span(4, 7).highlight(), equals(r"""
+  ,
+2 | / 
+3 | | 
+4 | \ 
+  '"""));
+    });
+
+    // Regression test for #32
+    test('highlights the end of a line and an empty line', () {
+      final file = SourceFile.fromString('foo\n\n');
+      expect(file.span(3, 5).highlight(), equals(r"""
+  ,
+1 |   foo
+  | ,----^
+2 | \ 
+  '"""));
+    });
+  });
+
+  group('prints tabs as spaces', () {
+    group('in a single-line span', () {
+      test('before the highlighted section', () {
+        final span = SourceFile.fromString('foo\tbar baz').span(4, 7);
+
+        expect(span.highlight(), equals("""
+  ,
+1 | foo    bar baz
+  |        ^^^
+  '"""));
+      });
+
+      test('within the highlighted section', () {
+        final span = SourceFile.fromString('foo bar\tbaz bang').span(4, 11);
+
+        expect(span.highlight(), equals("""
+  ,
+1 | foo bar    baz bang
+  |     ^^^^^^^^^^
+  '"""));
+      });
+
+      test('after the highlighted section', () {
+        final span = SourceFile.fromString('foo bar\tbaz').span(4, 7);
+
+        expect(span.highlight(), equals("""
+  ,
+1 | foo bar    baz
+  |     ^^^
+  '"""));
+      });
+    });
+
+    group('in a multi-line span', () {
+      test('before the highlighted section', () {
+        final span = SourceFile.fromString('''
+foo\tbar baz
+whiz bang boom
+''').span(4, 21);
+
+        expect(span.highlight(), equals("""
+  ,
+1 |   foo    bar baz
+  | ,--------^
+2 | | whiz bang boom
+  | '---------^
+  '"""));
+      });
+
+      test('within the first highlighted line', () {
+        final span = SourceFile.fromString('''
+foo bar\tbaz
+whiz bang boom
+''').span(4, 21);
+
+        expect(span.highlight(), equals("""
+  ,
+1 |   foo bar    baz
+  | ,-----^
+2 | | whiz bang boom
+  | '---------^
+  '"""));
+      });
+
+      test('at the beginning of the first highlighted line', () {
+        final span = SourceFile.fromString('''
+foo bar\tbaz
+whiz bang boom
+''').span(7, 21);
+
+        expect(span.highlight(), equals("""
+  ,
+1 |   foo bar    baz
+  | ,--------^
+2 | | whiz bang boom
+  | '---------^
+  '"""));
+      });
+
+      test('within a middle highlighted line', () {
+        final span = SourceFile.fromString('''
+foo bar baz
+whiz\tbang boom
+zip zap zop
+''').span(4, 34);
+
+        expect(span.highlight(), equals("""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | | whiz    bang boom
+3 | | zip zap zop
+  | '-------^
+  '"""));
+      });
+
+      test('within the last highlighted line', () {
+        final span = SourceFile.fromString('''
+foo bar baz
+whiz\tbang boom
+''').span(4, 21);
+
+        expect(span.highlight(), equals("""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | | whiz    bang boom
+  | '------------^
+  '"""));
+      });
+
+      test('at the end of the last highlighted line', () {
+        final span = SourceFile.fromString('''
+foo bar baz
+whiz\tbang boom
+''').span(4, 17);
+
+        expect(span.highlight(), equals("""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | | whiz    bang boom
+  | '--------^
+  '"""));
+      });
+
+      test('after the highlighted section', () {
+        final span = SourceFile.fromString('''
+foo bar baz
+whiz bang\tboom
+''').span(4, 21);
+
+        expect(span.highlight(), equals("""
+  ,
+1 |   foo bar baz
+  | ,-----^
+2 | | whiz bang    boom
+  | '---------^
+  '"""));
+      });
+    });
+  });
+
+  group('supports lines of preceding and following context for a span', () {
+    test('within a single line', () {
+      final span = SourceSpanWithContext(
+          SourceLocation(20, line: 2, column: 5, sourceUrl: 'foo.dart'),
+          SourceLocation(27, line: 2, column: 12, sourceUrl: 'foo.dart'),
+          'foo bar',
+          'previous\nlines\n-----foo bar-----\nfollowing line\n');
+
+      expect(span.highlight(), equals("""
+  ,
+1 | previous
+2 | lines
+3 | -----foo bar-----
+  |      ^^^^^^^
+4 | following line
+  '"""));
+    });
+
+    test('covering a full line', () {
+      final span = SourceSpanWithContext(
+          SourceLocation(15, line: 2, column: 0, sourceUrl: 'foo.dart'),
+          SourceLocation(33, line: 3, column: 0, sourceUrl: 'foo.dart'),
+          '-----foo bar-----\n',
+          'previous\nlines\n-----foo bar-----\nfollowing line\n');
+
+      expect(span.highlight(), equals("""
+  ,
+1 | previous
+2 | lines
+3 | -----foo bar-----
+  | ^^^^^^^^^^^^^^^^^
+4 | following line
+  '"""));
+    });
+
+    test('covering multiple full lines', () {
+      final span = SourceSpanWithContext(
+          SourceLocation(15, line: 2, column: 0, sourceUrl: 'foo.dart'),
+          SourceLocation(23, line: 4, column: 0, sourceUrl: 'foo.dart'),
+          'foo\nbar\n',
+          'previous\nlines\nfoo\nbar\nfollowing line\n');
+
+      expect(span.highlight(), equals(r"""
+  ,
+1 |   previous
+2 |   lines
+3 | / foo
+4 | \ bar
+5 |   following line
+  '"""));
+    });
+  });
+
+  group('colors', () {
+    test("doesn't colorize if color is false", () {
+      expect(file.span(4, 7).highlight(color: false), equals("""
+  ,
+1 | foo bar baz
+  |     ^^^
+  '"""));
+    });
+
+    test('colorizes if color is true', () {
+      expect(file.span(4, 7).highlight(color: true), equals('''
+${colors.blue}  ,${colors.none}
+${colors.blue}1 |${colors.none} foo ${colors.red}bar${colors.none} baz
+${colors.blue}  |${colors.none} ${colors.red}    ^^^${colors.none}
+${colors.blue}  '${colors.none}'''));
+    });
+
+    test("uses the given color if it's passed", () {
+      expect(file.span(4, 7).highlight(color: colors.yellow), equals('''
+${colors.blue}  ,${colors.none}
+${colors.blue}1 |${colors.none} foo ${colors.yellow}bar${colors.none} baz
+${colors.blue}  |${colors.none} ${colors.yellow}    ^^^${colors.none}
+${colors.blue}  '${colors.none}'''));
+    });
+
+    test('colorizes a multiline span', () {
+      expect(file.span(4, 34).highlight(color: true), equals('''
+${colors.blue}  ,${colors.none}
+${colors.blue}1 |${colors.none}   foo ${colors.red}bar baz${colors.none}
+${colors.blue}  |${colors.none} ${colors.red},${colors.none}${colors.red}-----^${colors.none}
+${colors.blue}2 |${colors.none} ${colors.red}|${colors.none} ${colors.red}whiz bang boom${colors.none}
+${colors.blue}3 |${colors.none} ${colors.red}|${colors.none} ${colors.red}zip zap${colors.none} zop
+${colors.blue}  |${colors.none} ${colors.red}'${colors.none}${colors.red}-------^${colors.none}
+${colors.blue}  '${colors.none}'''));
+    });
+
+    test('colorizes a multiline span that highlights full lines', () {
+      expect(file.span(0, 39).highlight(color: true), equals('''
+${colors.blue}  ,${colors.none}
+${colors.blue}1 |${colors.none} ${colors.red}/${colors.none} ${colors.red}foo bar baz${colors.none}
+${colors.blue}2 |${colors.none} ${colors.red}|${colors.none} ${colors.red}whiz bang boom${colors.none}
+${colors.blue}3 |${colors.none} ${colors.red}\\${colors.none} ${colors.red}zip zap zop${colors.none}
+${colors.blue}  '${colors.none}'''));
+    });
+  });
+
+  group('line numbers have appropriate padding', () {
+    test('with line number 9', () {
+      expect(
+          SourceFile.fromString('\n' * 8 + 'foo bar baz\n')
+              .span(8, 11)
+              .highlight(),
+          equals("""
+  ,
+9 | foo bar baz
+  | ^^^
+  '"""));
+    });
+
+    test('with line number 10', () {
+      expect(
+          SourceFile.fromString('\n' * 9 + 'foo bar baz\n')
+              .span(9, 12)
+              .highlight(),
+          equals("""
+   ,
+10 | foo bar baz
+   | ^^^
+   '"""));
+    });
+  });
+}
diff --git a/pkgs/source_span/test/location_test.dart b/pkgs/source_span/test/location_test.dart
new file mode 100644
index 0000000..bbe259b
--- /dev/null
+++ b/pkgs/source_span/test/location_test.dart
@@ -0,0 +1,97 @@
+// 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:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late SourceLocation location;
+  setUp(() {
+    location = SourceLocation(15, line: 2, column: 6, sourceUrl: 'foo.dart');
+  });
+
+  group('errors', () {
+    group('for new SourceLocation()', () {
+      test('offset may not be negative', () {
+        expect(() => SourceLocation(-1), throwsRangeError);
+      });
+
+      test('line may not be negative', () {
+        expect(() => SourceLocation(0, line: -1), throwsRangeError);
+      });
+
+      test('column may not be negative', () {
+        expect(() => SourceLocation(0, column: -1), throwsRangeError);
+      });
+    });
+
+    test('for distance() source URLs must match', () {
+      expect(() => location.distance(SourceLocation(0)), throwsArgumentError);
+    });
+
+    test('for compareTo() source URLs must match', () {
+      expect(() => location.compareTo(SourceLocation(0)), throwsArgumentError);
+    });
+  });
+
+  test('fields work correctly', () {
+    expect(location.sourceUrl, equals(Uri.parse('foo.dart')));
+    expect(location.offset, equals(15));
+    expect(location.line, equals(2));
+    expect(location.column, equals(6));
+  });
+
+  group('toolString', () {
+    test('returns a computer-readable representation', () {
+      expect(location.toolString, equals('foo.dart:3:7'));
+    });
+
+    test('gracefully handles a missing source URL', () {
+      final location = SourceLocation(15, line: 2, column: 6);
+      expect(location.toolString, equals('unknown source:3:7'));
+    });
+  });
+
+  test('distance returns the absolute distance between locations', () {
+    final other = SourceLocation(10, sourceUrl: 'foo.dart');
+    expect(location.distance(other), equals(5));
+    expect(other.distance(location), equals(5));
+  });
+
+  test('pointSpan returns an empty span at location', () {
+    final span = location.pointSpan();
+    expect(span.start, equals(location));
+    expect(span.end, equals(location));
+    expect(span.text, isEmpty);
+  });
+
+  group('compareTo()', () {
+    test('sorts by offset', () {
+      final other = SourceLocation(20, sourceUrl: 'foo.dart');
+      expect(location.compareTo(other), lessThan(0));
+      expect(other.compareTo(location), greaterThan(0));
+    });
+
+    test('considers equal locations equal', () {
+      expect(location.compareTo(location), equals(0));
+    });
+  });
+
+  group('equality', () {
+    test('two locations with the same offset and source are equal', () {
+      final other = SourceLocation(15, sourceUrl: 'foo.dart');
+      expect(location, equals(other));
+    });
+
+    test("a different offset isn't equal", () {
+      final other = SourceLocation(10, sourceUrl: 'foo.dart');
+      expect(location, isNot(equals(other)));
+    });
+
+    test("a different source isn't equal", () {
+      final other = SourceLocation(15, sourceUrl: 'bar.dart');
+      expect(location, isNot(equals(other)));
+    });
+  });
+}
diff --git a/pkgs/source_span/test/multiple_highlight_test.dart b/pkgs/source_span/test/multiple_highlight_test.dart
new file mode 100644
index 0000000..139d53c
--- /dev/null
+++ b/pkgs/source_span/test/multiple_highlight_test.dart
@@ -0,0 +1,423 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+
+void main() {
+  late bool oldAscii;
+  setUpAll(() {
+    oldAscii = glyph.ascii;
+    glyph.ascii = true;
+  });
+
+  tearDownAll(() {
+    glyph.ascii = oldAscii;
+  });
+
+  late SourceFile file;
+  setUp(() {
+    file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop
+fwee fwoo fwip
+argle bargle boo
+gibble bibble bop
+''', url: 'file1.txt');
+  });
+
+  test('highlights spans on separate lines', () {
+    expect(
+        file.span(17, 21).highlightMultiple(
+            'one', {file.span(31, 34): 'two', file.span(4, 7): 'three'}),
+        equals("""
+  ,
+1 | foo bar baz
+  |     === three
+2 | whiz bang boom
+  |      ^^^^ one
+3 | zip zap zop
+  |     === two
+  '"""));
+  });
+
+  test('highlights spans on the same line', () {
+    expect(
+        file.span(17, 21).highlightMultiple(
+            'one', {file.span(22, 26): 'two', file.span(12, 16): 'three'}),
+        equals("""
+  ,
+2 | whiz bang boom
+  |      ^^^^ one
+  | ==== three
+  |           ==== two
+  '"""));
+  });
+
+  test('highlights overlapping spans on the same line', () {
+    expect(
+        file.span(17, 21).highlightMultiple(
+            'one', {file.span(20, 26): 'two', file.span(12, 18): 'three'}),
+        equals("""
+  ,
+2 | whiz bang boom
+  |      ^^^^ one
+  | ====== three
+  |         ====== two
+  '"""));
+  });
+
+  test('highlights multiple multiline spans', () {
+    expect(
+        file.span(27, 54).highlightMultiple(
+            'one', {file.span(54, 89): 'two', file.span(0, 27): 'three'}),
+        equals("""
+  ,
+1 | / foo bar baz
+2 | | whiz bang boom
+  | '--- three
+3 | / zip zap zop
+4 | | fwee fwoo fwip
+  | '--- one
+5 | / argle bargle boo
+6 | | gibble bibble bop
+  | '--- two
+  '"""));
+  });
+
+  test('highlights multiple overlapping multiline spans', () {
+    expect(
+        file.span(12, 70).highlightMultiple(
+            'one', {file.span(54, 89): 'two', file.span(0, 27): 'three'}),
+        equals("""
+  ,
+1 | /- foo bar baz
+2 | |/ whiz bang boom
+  | '+--- three
+3 |  | zip zap zop
+4 |  | fwee fwoo fwip
+5 | /+ argle bargle boo
+  | |'--- one
+6 | |  gibble bibble bop
+  | '---- two
+  '"""));
+  });
+
+  test('highlights many layers of overlaps', () {
+    expect(
+        file.span(0, 54).highlightMultiple('one', {
+          file.span(12, 77): 'two',
+          file.span(27, 84): 'three',
+          file.span(39, 88): 'four'
+        }),
+        equals("""
+  ,
+1 | /--- foo bar baz
+2 | |/-- whiz bang boom
+3 | ||/- zip zap zop
+4 | |||/ fwee fwoo fwip
+  | '+++--- one
+5 |  ||| argle bargle boo
+6 |  ||| gibble bibble bop
+  |  '++------^ two
+  |   '+-------------^ three
+  |    '--- four
+  '"""));
+  });
+
+  group("highlights a multiline span that's a subset", () {
+    test('with no first or last line overlap', () {
+      expect(
+          file
+              .span(27, 53)
+              .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+          equals("""
+  ,
+2 | /- whiz bang boom
+3 | |/ zip zap zop
+4 | || fwee fwoo fwip
+  | |'--- inner
+5 | |  argle bargle boo
+  | '---- outer
+  '"""));
+    });
+
+    test('overlapping the whole first line', () {
+      expect(
+          file
+              .span(12, 53)
+              .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+          equals("""
+  ,
+2 | // whiz bang boom
+3 | || zip zap zop
+4 | || fwee fwoo fwip
+  | |'--- inner
+5 | |  argle bargle boo
+  | '---- outer
+  '"""));
+    });
+
+    test('overlapping part of first line', () {
+      expect(
+          file
+              .span(17, 53)
+              .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+          equals("""
+  ,
+2 | /- whiz bang boom
+  | |,------^
+3 | || zip zap zop
+4 | || fwee fwoo fwip
+  | |'--- inner
+5 | |  argle bargle boo
+  | '---- outer
+  '"""));
+    });
+
+    test('overlapping the whole last line', () {
+      expect(
+          file
+              .span(27, 70)
+              .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+          equals("""
+  ,
+2 | /- whiz bang boom
+3 | |/ zip zap zop
+4 | || fwee fwoo fwip
+5 | || argle bargle boo
+  | |'--- inner
+  | '---- outer
+  '"""));
+    });
+
+    test('overlapping part of the last line', () {
+      expect(
+          file
+              .span(27, 66)
+              .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+          equals("""
+  ,
+2 | /- whiz bang boom
+3 | |/ zip zap zop
+4 | || fwee fwoo fwip
+5 | || argle bargle boo
+  | |'------------^ inner
+  | '---- outer
+  '"""));
+    });
+  });
+
+  group('a single-line span in a multiline span', () {
+    test('on the first line', () {
+      expect(
+          file
+              .span(17, 21)
+              .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+          equals("""
+  ,
+2 | / whiz bang boom
+  | |      ^^^^ inner
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+  | '--- outer
+  '"""));
+    });
+
+    test('in the middle', () {
+      expect(
+          file
+              .span(31, 34)
+              .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+          equals("""
+  ,
+2 | / whiz bang boom
+3 | | zip zap zop
+  | |     ^^^ inner
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+  | '--- outer
+  '"""));
+    });
+
+    test('on the last line', () {
+      expect(
+          file
+              .span(60, 66)
+              .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+          equals("""
+  ,
+2 | / whiz bang boom
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+  | |       ^^^^^^ inner
+  | '--- outer
+  '"""));
+    });
+  });
+
+  group('writes headers when highlighting multiple files', () {
+    test('writes all file URLs', () {
+      final span2 = SourceFile.fromString('''
+quibble bibble boop
+''', url: 'file2.txt').span(8, 14);
+
+      expect(
+          file.span(31, 34).highlightMultiple('one', {span2: 'two'}), equals("""
+  ,--> file1.txt
+3 | zip zap zop
+  |     ^^^ one
+  '
+  ,--> file2.txt
+1 | quibble bibble boop
+  |         ====== two
+  '"""));
+    });
+
+    test('allows secondary spans to have null URL', () {
+      final span2 = SourceSpan(SourceLocation(1), SourceLocation(4), 'foo');
+
+      expect(
+          file.span(31, 34).highlightMultiple('one', {span2: 'two'}), equals("""
+  ,--> file1.txt
+3 | zip zap zop
+  |     ^^^ one
+  '
+  ,
+1 | foo
+  | === two
+  '"""));
+    });
+
+    test('allows primary span to have null URL', () {
+      final span1 = SourceSpan(SourceLocation(1), SourceLocation(4), 'foo');
+
+      expect(
+          span1.highlightMultiple('one', {file.span(31, 34): 'two'}), equals("""
+  ,
+1 | foo
+  | ^^^ one
+  '
+  ,--> file1.txt
+3 | zip zap zop
+  |     === two
+  '"""));
+    });
+  });
+
+  test('highlights multiple null URLs as separate files', () {
+    final span1 = SourceSpan(SourceLocation(1), SourceLocation(4), 'foo');
+    final span2 = SourceSpan(SourceLocation(1), SourceLocation(4), 'bar');
+
+    expect(span1.highlightMultiple('one', {span2: 'two'}), equals("""
+  ,
+1 | foo
+  | ^^^ one
+  '
+  ,
+1 | bar
+  | === two
+  '"""));
+  });
+
+  group('indents mutli-line labels', () {
+    test('for the primary label', () {
+      expect(file.span(17, 21).highlightMultiple('line 1\nline 2\nline 3', {}),
+          equals("""
+  ,
+2 | whiz bang boom
+  |      ^^^^ line 1
+  |           line 2
+  |           line 3
+  '"""));
+    });
+
+    group('for a secondary label', () {
+      test('on the same line', () {
+        expect(
+            file.span(17, 21).highlightMultiple(
+                'primary', {file.span(22, 26): 'line 1\nline 2\nline 3'}),
+            equals("""
+  ,
+2 | whiz bang boom
+  |      ^^^^ primary
+  |           ==== line 1
+  |                line 2
+  |                line 3
+  '"""));
+      });
+
+      test('on a different line', () {
+        expect(
+            file.span(17, 21).highlightMultiple(
+                'primary', {file.span(31, 34): 'line 1\nline 2\nline 3'}),
+            equals("""
+  ,
+2 | whiz bang boom
+  |      ^^^^ primary
+3 | zip zap zop
+  |     === line 1
+  |         line 2
+  |         line 3
+  '"""));
+      });
+    });
+
+    group('for a multiline span', () {
+      test('that covers the whole last line', () {
+        expect(
+            file.span(12, 70).highlightMultiple('line 1\nline 2\nline 3', {}),
+            equals("""
+  ,
+2 | / whiz bang boom
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+  | '--- line 1
+  |      line 2
+  |      line 3
+  '"""));
+      });
+
+      test('that covers part of the last line', () {
+        expect(
+            file.span(12, 66).highlightMultiple('line 1\nline 2\nline 3', {}),
+            equals("""
+  ,
+2 | / whiz bang boom
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+  | '------------^ line 1
+  |                line 2
+  |                line 3
+  '"""));
+      });
+    });
+
+    test('with an overlapping span', () {
+      expect(
+          file.span(12, 70).highlightMultiple('line 1\nline 2\nline 3',
+              {file.span(54, 89): 'two', file.span(0, 27): 'three'}),
+          equals("""
+  ,
+1 | /- foo bar baz
+2 | |/ whiz bang boom
+  | '+--- three
+3 |  | zip zap zop
+4 |  | fwee fwoo fwip
+5 | /+ argle bargle boo
+  | |'--- line 1
+  | |     line 2
+  | |     line 3
+6 | |  gibble bibble bop
+  | '---- two
+  '"""));
+    });
+  });
+}
diff --git a/pkgs/source_span/test/span_test.dart b/pkgs/source_span/test/span_test.dart
new file mode 100644
index 0000000..22c498e
--- /dev/null
+++ b/pkgs/source_span/test/span_test.dart
@@ -0,0 +1,432 @@
+// 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:source_span/source_span.dart';
+import 'package:source_span/src/colors.dart' as colors;
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+
+void main() {
+  late bool oldAscii;
+
+  setUpAll(() {
+    oldAscii = glyph.ascii;
+    glyph.ascii = true;
+  });
+
+  tearDownAll(() {
+    glyph.ascii = oldAscii;
+  });
+
+  late SourceSpan span;
+  setUp(() {
+    span = SourceSpan(SourceLocation(5, sourceUrl: 'foo.dart'),
+        SourceLocation(12, sourceUrl: 'foo.dart'), 'foo bar');
+  });
+
+  group('errors', () {
+    group('for new SourceSpan()', () {
+      test('source URLs must match', () {
+        final start = SourceLocation(0, sourceUrl: 'foo.dart');
+        final end = SourceLocation(1, sourceUrl: 'bar.dart');
+        expect(() => SourceSpan(start, end, '_'), throwsArgumentError);
+      });
+
+      test('end must come after start', () {
+        final start = SourceLocation(1);
+        final end = SourceLocation(0);
+        expect(() => SourceSpan(start, end, '_'), throwsArgumentError);
+      });
+
+      test('text must be the right length', () {
+        final start = SourceLocation(0);
+        final end = SourceLocation(1);
+        expect(() => SourceSpan(start, end, 'abc'), throwsArgumentError);
+      });
+    });
+
+    group('for new SourceSpanWithContext()', () {
+      test('context must contain text', () {
+        final start = SourceLocation(2);
+        final end = SourceLocation(5);
+        expect(() => SourceSpanWithContext(start, end, 'abc', '--axc--'),
+            throwsArgumentError);
+      });
+
+      test('text starts at start.column in context', () {
+        final start = SourceLocation(3);
+        final end = SourceLocation(5);
+        expect(() => SourceSpanWithContext(start, end, 'abc', '--abc--'),
+            throwsArgumentError);
+      });
+
+      test('text starts at start.column of line in multi-line context', () {
+        final start = SourceLocation(4, line: 55, column: 3);
+        final end = SourceLocation(7, line: 55, column: 6);
+        expect(() => SourceSpanWithContext(start, end, 'abc', '\n--abc--'),
+            throwsArgumentError);
+        expect(() => SourceSpanWithContext(start, end, 'abc', '\n----abc--'),
+            throwsArgumentError);
+        expect(() => SourceSpanWithContext(start, end, 'abc', '\n\n--abc--'),
+            throwsArgumentError);
+
+        // However, these are valid:
+        SourceSpanWithContext(start, end, 'abc', '\n---abc--');
+        SourceSpanWithContext(start, end, 'abc', '\n\n---abc--');
+      });
+
+      test('text can occur multiple times in context', () {
+        final start1 = SourceLocation(4, line: 55, column: 2);
+        final end1 = SourceLocation(7, line: 55, column: 5);
+        final start2 = SourceLocation(4, line: 55, column: 8);
+        final end2 = SourceLocation(7, line: 55, column: 11);
+        SourceSpanWithContext(start1, end1, 'abc', '--abc---abc--\n');
+        SourceSpanWithContext(start1, end1, 'abc', '--abc--abc--\n');
+        SourceSpanWithContext(start2, end2, 'abc', '--abc---abc--\n');
+        SourceSpanWithContext(start2, end2, 'abc', '---abc--abc--\n');
+        expect(
+            () => SourceSpanWithContext(start1, end1, 'abc', '---abc--abc--\n'),
+            throwsArgumentError);
+        expect(
+            () => SourceSpanWithContext(start2, end2, 'abc', '--abc--abc--\n'),
+            throwsArgumentError);
+      });
+    });
+
+    group('for union()', () {
+      test('source URLs must match', () {
+        final other = SourceSpan(SourceLocation(12, sourceUrl: 'bar.dart'),
+            SourceLocation(13, sourceUrl: 'bar.dart'), '_');
+
+        expect(() => span.union(other), throwsArgumentError);
+      });
+
+      test('spans may not be disjoint', () {
+        final other = SourceSpan(SourceLocation(13, sourceUrl: 'foo.dart'),
+            SourceLocation(14, sourceUrl: 'foo.dart'), '_');
+
+        expect(() => span.union(other), throwsArgumentError);
+      });
+    });
+
+    test('for compareTo() source URLs must match', () {
+      final other = SourceSpan(SourceLocation(12, sourceUrl: 'bar.dart'),
+          SourceLocation(13, sourceUrl: 'bar.dart'), '_');
+
+      expect(() => span.compareTo(other), throwsArgumentError);
+    });
+  });
+
+  test('fields work correctly', () {
+    expect(span.start, equals(SourceLocation(5, sourceUrl: 'foo.dart')));
+    expect(span.end, equals(SourceLocation(12, sourceUrl: 'foo.dart')));
+    expect(span.sourceUrl, equals(Uri.parse('foo.dart')));
+    expect(span.length, equals(7));
+  });
+
+  group('union()', () {
+    test('works with a preceding adjacent span', () {
+      final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+          SourceLocation(5, sourceUrl: 'foo.dart'), 'hey, ');
+
+      final result = span.union(other);
+      expect(result.start, equals(other.start));
+      expect(result.end, equals(span.end));
+      expect(result.text, equals('hey, foo bar'));
+    });
+
+    test('works with a preceding overlapping span', () {
+      final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+          SourceLocation(8, sourceUrl: 'foo.dart'), 'hey, foo');
+
+      final result = span.union(other);
+      expect(result.start, equals(other.start));
+      expect(result.end, equals(span.end));
+      expect(result.text, equals('hey, foo bar'));
+    });
+
+    test('works with a following adjacent span', () {
+      final other = SourceSpan(SourceLocation(12, sourceUrl: 'foo.dart'),
+          SourceLocation(16, sourceUrl: 'foo.dart'), ' baz');
+
+      final result = span.union(other);
+      expect(result.start, equals(span.start));
+      expect(result.end, equals(other.end));
+      expect(result.text, equals('foo bar baz'));
+    });
+
+    test('works with a following overlapping span', () {
+      final other = SourceSpan(SourceLocation(9, sourceUrl: 'foo.dart'),
+          SourceLocation(16, sourceUrl: 'foo.dart'), 'bar baz');
+
+      final result = span.union(other);
+      expect(result.start, equals(span.start));
+      expect(result.end, equals(other.end));
+      expect(result.text, equals('foo bar baz'));
+    });
+
+    test('works with an internal overlapping span', () {
+      final other = SourceSpan(SourceLocation(7, sourceUrl: 'foo.dart'),
+          SourceLocation(10, sourceUrl: 'foo.dart'), 'o b');
+
+      expect(span.union(other), equals(span));
+    });
+
+    test('works with an external overlapping span', () {
+      final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+          SourceLocation(16, sourceUrl: 'foo.dart'), 'hey, foo bar baz');
+
+      expect(span.union(other), equals(other));
+    });
+  });
+
+  group('subspan()', () {
+    group('errors', () {
+      test('start must be greater than zero', () {
+        expect(() => span.subspan(-1), throwsRangeError);
+      });
+
+      test('start must be less than or equal to length', () {
+        expect(() => span.subspan(span.length + 1), throwsRangeError);
+      });
+
+      test('end must be greater than start', () {
+        expect(() => span.subspan(2, 1), throwsRangeError);
+      });
+
+      test('end must be less than or equal to length', () {
+        expect(() => span.subspan(0, span.length + 1), throwsRangeError);
+      });
+    });
+
+    test('preserves the source URL', () {
+      final result = span.subspan(1, 2);
+      expect(result.start.sourceUrl, equals(span.sourceUrl));
+      expect(result.end.sourceUrl, equals(span.sourceUrl));
+    });
+
+    test('preserves the context', () {
+      final start = SourceLocation(2);
+      final end = SourceLocation(5);
+      final span = SourceSpanWithContext(start, end, 'abc', '--abc--');
+      expect(span.subspan(1, 2).context, equals('--abc--'));
+    });
+
+    group('returns the original span', () {
+      test('with an implicit end', () => expect(span.subspan(0), equals(span)));
+
+      test('with an explicit end',
+          () => expect(span.subspan(0, span.length), equals(span)));
+    });
+
+    group('within a single line', () {
+      test('returns a strict substring of the original span', () {
+        final result = span.subspan(1, 5);
+        expect(result.text, equals('oo b'));
+        expect(result.start.offset, equals(6));
+        expect(result.start.line, equals(0));
+        expect(result.start.column, equals(6));
+        expect(result.end.offset, equals(10));
+        expect(result.end.line, equals(0));
+        expect(result.end.column, equals(10));
+      });
+
+      test('an implicit end goes to the end of the original span', () {
+        final result = span.subspan(1);
+        expect(result.text, equals('oo bar'));
+        expect(result.start.offset, equals(6));
+        expect(result.start.line, equals(0));
+        expect(result.start.column, equals(6));
+        expect(result.end.offset, equals(12));
+        expect(result.end.line, equals(0));
+        expect(result.end.column, equals(12));
+      });
+
+      test('can return an empty span', () {
+        final result = span.subspan(3, 3);
+        expect(result.text, isEmpty);
+        expect(result.start.offset, equals(8));
+        expect(result.start.line, equals(0));
+        expect(result.start.column, equals(8));
+        expect(result.end, equals(result.start));
+      });
+    });
+
+    group('across multiple lines', () {
+      setUp(() {
+        span = SourceSpan(
+            SourceLocation(5, line: 2, column: 0),
+            SourceLocation(16, line: 4, column: 3),
+            'foo\n'
+            'bar\n'
+            'baz');
+      });
+
+      test('with start and end in the middle of a line', () {
+        final result = span.subspan(2, 5);
+        expect(result.text, equals('o\nb'));
+        expect(result.start.offset, equals(7));
+        expect(result.start.line, equals(2));
+        expect(result.start.column, equals(2));
+        expect(result.end.offset, equals(10));
+        expect(result.end.line, equals(3));
+        expect(result.end.column, equals(1));
+      });
+
+      test('with start at the end of a line', () {
+        final result = span.subspan(3, 5);
+        expect(result.text, equals('\nb'));
+        expect(result.start.offset, equals(8));
+        expect(result.start.line, equals(2));
+        expect(result.start.column, equals(3));
+      });
+
+      test('with start at the beginning of a line', () {
+        final result = span.subspan(4, 5);
+        expect(result.text, equals('b'));
+        expect(result.start.offset, equals(9));
+        expect(result.start.line, equals(3));
+        expect(result.start.column, equals(0));
+      });
+
+      test('with end at the end of a line', () {
+        final result = span.subspan(2, 3);
+        expect(result.text, equals('o'));
+        expect(result.end.offset, equals(8));
+        expect(result.end.line, equals(2));
+        expect(result.end.column, equals(3));
+      });
+
+      test('with end at the beginning of a line', () {
+        final result = span.subspan(2, 4);
+        expect(result.text, equals('o\n'));
+        expect(result.end.offset, equals(9));
+        expect(result.end.line, equals(3));
+        expect(result.end.column, equals(0));
+      });
+    });
+  });
+
+  group('message()', () {
+    test('prints the text being described', () {
+      expect(span.message('oh no'), equals("""
+line 1, column 6 of foo.dart: oh no
+  ,
+1 | foo bar
+  | ^^^^^^^
+  '"""));
+    });
+
+    test('gracefully handles a missing source URL', () {
+      final span = SourceSpan(SourceLocation(5), SourceLocation(12), 'foo bar');
+
+      expect(span.message('oh no'), equalsIgnoringWhitespace("""
+line 1, column 6: oh no
+  ,
+1 | foo bar
+  | ^^^^^^^
+  '"""));
+    });
+
+    test('gracefully handles empty text', () {
+      final span = SourceSpan(SourceLocation(5), SourceLocation(5), '');
+
+      expect(span.message('oh no'), equals('line 1, column 6: oh no'));
+    });
+
+    test("doesn't colorize if color is false", () {
+      expect(span.message('oh no', color: false), equals("""
+line 1, column 6 of foo.dart: oh no
+  ,
+1 | foo bar
+  | ^^^^^^^
+  '"""));
+    });
+
+    test('colorizes if color is true', () {
+      expect(span.message('oh no', color: true), equals("""
+line 1, column 6 of foo.dart: oh no
+${colors.blue}  ,${colors.none}
+${colors.blue}1 |${colors.none} ${colors.red}foo bar${colors.none}
+${colors.blue}  |${colors.none} ${colors.red}^^^^^^^${colors.none}
+${colors.blue}  '${colors.none}"""));
+    });
+
+    test("uses the given color if it's passed", () {
+      expect(span.message('oh no', color: colors.yellow), equals("""
+line 1, column 6 of foo.dart: oh no
+${colors.blue}  ,${colors.none}
+${colors.blue}1 |${colors.none} ${colors.yellow}foo bar${colors.none}
+${colors.blue}  |${colors.none} ${colors.yellow}^^^^^^^${colors.none}
+${colors.blue}  '${colors.none}"""));
+    });
+
+    test('with context, underlines the right column', () {
+      final spanWithContext = SourceSpanWithContext(
+          SourceLocation(5, sourceUrl: 'foo.dart'),
+          SourceLocation(12, sourceUrl: 'foo.dart'),
+          'foo bar',
+          '-----foo bar-----');
+
+      expect(spanWithContext.message('oh no', color: colors.yellow), equals("""
+line 1, column 6 of foo.dart: oh no
+${colors.blue}  ,${colors.none}
+${colors.blue}1 |${colors.none} -----${colors.yellow}foo bar${colors.none}-----
+${colors.blue}  |${colors.none} ${colors.yellow}     ^^^^^^^${colors.none}
+${colors.blue}  '${colors.none}"""));
+    });
+  });
+
+  group('compareTo()', () {
+    test('sorts by start location first', () {
+      final other = SourceSpan(SourceLocation(6, sourceUrl: 'foo.dart'),
+          SourceLocation(14, sourceUrl: 'foo.dart'), 'oo bar b');
+
+      expect(span.compareTo(other), lessThan(0));
+      expect(other.compareTo(span), greaterThan(0));
+    });
+
+    test('sorts by length second', () {
+      final other = SourceSpan(SourceLocation(5, sourceUrl: 'foo.dart'),
+          SourceLocation(14, sourceUrl: 'foo.dart'), 'foo bar b');
+
+      expect(span.compareTo(other), lessThan(0));
+      expect(other.compareTo(span), greaterThan(0));
+    });
+
+    test('considers equal spans equal', () {
+      expect(span.compareTo(span), equals(0));
+    });
+  });
+
+  group('equality', () {
+    test('two spans with the same locations are equal', () {
+      final other = SourceSpan(SourceLocation(5, sourceUrl: 'foo.dart'),
+          SourceLocation(12, sourceUrl: 'foo.dart'), 'foo bar');
+
+      expect(span, equals(other));
+    });
+
+    test("a different start isn't equal", () {
+      final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+          SourceLocation(12, sourceUrl: 'foo.dart'), 'hey, foo bar');
+
+      expect(span, isNot(equals(other)));
+    });
+
+    test("a different end isn't equal", () {
+      final other = SourceSpan(SourceLocation(5, sourceUrl: 'foo.dart'),
+          SourceLocation(16, sourceUrl: 'foo.dart'), 'foo bar baz');
+
+      expect(span, isNot(equals(other)));
+    });
+
+    test("a different source URL isn't equal", () {
+      final other = SourceSpan(SourceLocation(5, sourceUrl: 'bar.dart'),
+          SourceLocation(12, sourceUrl: 'bar.dart'), 'foo bar');
+
+      expect(span, isNot(equals(other)));
+    });
+  });
+}
diff --git a/pkgs/source_span/test/utils_test.dart b/pkgs/source_span/test/utils_test.dart
new file mode 100644
index 0000000..91397c0
--- /dev/null
+++ b/pkgs/source_span/test/utils_test.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/src/utils.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('find line start', () {
+    test('skip entries in wrong column', () {
+      const context = '0_bb\n1_bbb\n2b____\n3bbb\n';
+      final index = findLineStart(context, 'b', 1)!;
+      expect(index, 11);
+      expect(context.substring(index - 1, index + 3), '\n2b_');
+    });
+
+    test('end of line column for empty text', () {
+      const context = '0123\n56789\nabcdefgh\n';
+      final index = findLineStart(context, '', 5)!;
+      expect(index, 5);
+      expect(context[index], '5');
+    });
+
+    test('column at the end of the file for empty text', () {
+      var context = '0\n2\n45\n';
+      var index = findLineStart(context, '', 2)!;
+      expect(index, 4);
+      expect(context[index], '4');
+
+      context = '0\n2\n45';
+      index = findLineStart(context, '', 2)!;
+      expect(index, 4);
+    });
+
+    test('empty text in empty context', () {
+      final index = findLineStart('', '', 0);
+      expect(index, 0);
+    });
+
+    test('found on the first line', () {
+      const context = '0\n2\n45\n';
+      final index = findLineStart(context, '0', 0);
+      expect(index, 0);
+    });
+
+    test('finds text that starts with a newline', () {
+      const context = '0\n2\n45\n';
+      final index = findLineStart(context, '\n2', 1);
+      expect(index, 0);
+    });
+
+    test('not found', () {
+      const context = '0\n2\n45\n';
+      final index = findLineStart(context, '0', 1);
+      expect(index, isNull);
+    });
+  });
+}
diff --git a/pkgs/sse/.gitignore b/pkgs/sse/.gitignore
new file mode 100644
index 0000000..1467782
--- /dev/null
+++ b/pkgs/sse/.gitignore
@@ -0,0 +1,3 @@
+.dart_tool
+pubspec.lock
+test/web/index.dart.js.deps
diff --git a/pkgs/sse/AUTHORS b/pkgs/sse/AUTHORS
new file mode 100644
index 0000000..7c12ae6
--- /dev/null
+++ b/pkgs/sse/AUTHORS
@@ -0,0 +1,6 @@
+# Below is a list of people and organizations that have contributed
+# to the Dart project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
diff --git a/pkgs/sse/CHANGELOG.md b/pkgs/sse/CHANGELOG.md
new file mode 100644
index 0000000..0387ba9
--- /dev/null
+++ b/pkgs/sse/CHANGELOG.md
@@ -0,0 +1,178 @@
+## 4.1.7
+
+- Move to `dart-lang/tools` monorepo.
+
+## 4.1.6
+
+- Require package `web: '>=0.5.0 <2.0.0'`.
+
+## 4.1.5
+
+- Drop unneeded dependency on `package:js`.
+- Update the minimum Dart SDK version to `3.3.0`.
+- Support the latest `package:web`.
+
+## 4.1.4
+
+- Fix incorrect cast causing failure with `dart2wasm`.
+
+## 4.1.3
+
+- Update the minimum Dart SDK version to `3.2.0`.
+
+## 4.1.2
+
+- Send `fetch` requests instead of `XHR` requests.
+- Add an optional `debugKey` parameter to `SseClient` to include in logging.
+- Add a dependency on `package:js`.
+- Update the minimum Dart SDK version to `2.16.0`.
+
+## 4.1.1
+
+- Apply `keepAlive` logic to `SocketException`s.
+- Switch from using `package:pedantic` to `package:lints`
+- Rev the minimum required SDK to 2.15.
+- Populate the pubspec `repository` field.
+
+## 4.1.0
+
+- Limit the number of concurrent requests to prevent Chrome from automatically
+  dropping them on the floor.
+
+## 4.0.0
+
+- Support null safety.
+
+## 3.8.3
+
+- Require the latest shelf and remove dead code.
+
+## 3.8.2
+
+- Complete `onConnected` with an error if the `SseClient` receives an error
+  before the connection is successfully opened.
+
+## 3.8.1
+
+- Fix an issue where closing the `SseConnection` stream would result in an
+  error.
+
+## 3.8.0
+
+- Add `onConnected` to replace `onOpen`.
+- Fix an issue where failed requests would not add a `done` event to the
+  connection `sink`.
+
+## 3.7.0
+
+- Deprecate the client's `onOpen` getter. Messages will now be buffered until a
+  connection is established.
+
+## 3.6.1
+
+- Drop dependency on `package:uuid`.
+
+## 3.6.0
+
+- Improve performance by buffering out of order messages in the server instead
+  of the client.
+
+\*\* Note \*\* This is not modelled as a breaking change as the server can
+handle messages from older clients. However, clients should be using the latest
+server if they require order guarantees.
+
+## 3.5.0
+
+- Add new `shutdown` methods on `SseHandler` and `SseConnection` to allow
+  closing connections immediately, ignoring any keep-alive periods.
+
+## 3.4.0
+
+- Remove `onClose` from `SseConnection` and ensure the corresponding
+  `sink.close` correctly fires.
+
+## 3.3.0
+
+- Add an `onClose` event to the `SseConnection`. This allows consumers to listen
+  to this event in lue of `sseConnection.sink.done` as that is not guaranteed to
+  fire.
+
+## 3.2.2
+
+- Fix an issue where `keepAlive` may cause state errors when attempting to send
+  messages on a closed stream.
+
+## 3.2.1
+
+- Fix an issue where `keepAlive` would only allow a single reconnection.
+
+## 3.2.0
+
+- Re-expose `isInKeepAlivePeriod` flag on `SseConnection`. This flag will be
+  `true` when a connection has been dropped and is in the keep-alive period
+  waiting for a client to reconnect.
+
+## 3.1.2
+
+- Fix an issue where the `SseClient` would not send a `done` event when there
+  was an error with the SSE connection.
+
+## 3.1.1
+
+- Make `isInKeepAlive` on `SseConnection` private.
+
+**Note that this is a breaking change but in actuality no one should be
+depending on this API.**
+
+## 3.1.0
+
+- Add optional `keepAlive` parameter to the `SseHandler`. If `keepAlive` is
+  supplied, the connection will remain active for this period after a disconnect
+  and can be reconnected transparently. If there is no reconnect within that
+  period, the connection will be closed normally.
+
+## 3.0.0
+
+- Add retry logic.
+
+**Possible Breaking Change Error messages may now be delayed up to 5 seconds in
+the client.**
+
+## 2.1.2
+
+- Remove `package:http` dependency.
+
+## 2.1.1
+
+- Use proper headers delimiter.
+
+## 2.1.0
+
+- Support Firefox.
+
+## 2.0.3
+
+- Fix an issue where messages could come out of order.
+
+## 2.0.2
+
+- Support the latest `package:stream_channel`.
+- Require Dart SDK `>=2.1.0 <3.0.0`.
+
+## 2.0.1
+
+- Update to `package:uuid` version 2.0.
+
+## 2.0.0
+
+- No longer expose `close` and `onClose` on an `SseConnection`. This is simply
+  handled by the underlying `stream` / `sink`.
+- Fix a bug where resources of the `SseConnection` were not properly closed.
+
+## 1.0.0
+
+- Internal cleanup.
+
+## 0.0.1
+
+- Initial commit.
diff --git a/pkgs/sse/LICENSE b/pkgs/sse/LICENSE
new file mode 100644
index 0000000..a0d5f54
--- /dev/null
+++ b/pkgs/sse/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/sse/README.md b/pkgs/sse/README.md
new file mode 100644
index 0000000..ef51415
--- /dev/null
+++ b/pkgs/sse/README.md
@@ -0,0 +1,14 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/sse.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/sse.yaml)
+[![pub package](https://img.shields.io/pub/v/sse.svg)](https://pub.dev/packages/sse)
+[![package publisher](https://img.shields.io/pub/publisher/sse.svg)](https://pub.dev/packages/sse/publisher)
+
+This package provides support for bi-directional communication through Server
+Sent Events and corresponding POST requests.
+
+This package is not intended to be a general purpose SSE package, but instead is
+a bidirectional protocol for use when Websockets are unavailable. That is, both
+the client and the server expose a `sink` and `stream` on which to send and
+receive messages respectively.
+
+Both the server and client have implicit assumptions on each other and therefore
+a client from this package must be paired with a server from this package.
diff --git a/pkgs/sse/analysis_options.yaml b/pkgs/sse/analysis_options.yaml
new file mode 100644
index 0000000..6729bd9
--- /dev/null
+++ b/pkgs/sse/analysis_options.yaml
@@ -0,0 +1,13 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+
+linter:
+  rules:
+    - avoid_unused_constructor_parameters
+    - cancel_subscriptions
+    - literal_only_boolean_expressions
+    - no_adjacent_strings_in_list
diff --git a/pkgs/sse/example/index.dart b/pkgs/sse/example/index.dart
new file mode 100644
index 0000000..0ed7596
--- /dev/null
+++ b/pkgs/sse/example/index.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:sse/client/sse_client.dart';
+
+/// A basic example which should be used in a browser that supports SSE.
+void main() {
+  var channel = SseClient('/sseHandler');
+
+  channel.stream.listen((s) {
+    // Listen for messages and send them back.
+    channel.sink.add(s);
+  });
+}
diff --git a/pkgs/sse/example/server.dart b/pkgs/sse/example/server.dart
new file mode 100644
index 0000000..b6ee750
--- /dev/null
+++ b/pkgs/sse/example/server.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:shelf/shelf_io.dart' as io;
+import 'package:sse/server/sse_handler.dart';
+
+/// A basic server which sets up an SSE handler.
+///
+/// When a client connects it will send a simple message and print the
+/// response.
+void main() async {
+  var handler = SseHandler(Uri.parse('/sseHandler'));
+  await io.serve(handler.handler, 'localhost', 0);
+  var connections = handler.connections;
+  while (await connections.hasNext) {
+    var connection = await connections.next;
+    connection.sink.add('foo');
+    connection.stream.listen(print);
+  }
+}
diff --git a/pkgs/sse/lib/client/sse_client.dart b/pkgs/sse/lib/client/sse_client.dart
new file mode 100644
index 0000000..4d3df49
--- /dev/null
+++ b/pkgs/sse/lib/client/sse_client.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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:js_interop';
+
+import 'package:logging/logging.dart';
+import 'package:pool/pool.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:web/web.dart';
+
+import '../src/util/uuid.dart';
+
+/// Limit for the number of concurrent outgoing requests.
+///
+/// Chrome drops outgoing requests on the floor after some threshold. To prevent
+/// these errors we buffer outgoing requests with a pool.
+///
+/// Note Chrome's limit is 6000. So this gives us plenty of headroom.
+final _requestPool = Pool(1000);
+
+/// A client for bi-directional sse communication.
+///
+/// The client can send any JSON-encodable messages to the server by adding
+/// them to the [sink] and listen to messages from the server on the [stream].
+class SseClient extends StreamChannelMixin<String?> {
+  final String _clientId;
+
+  final _incomingController = StreamController<String>();
+
+  final _outgoingController = StreamController<String>();
+
+  final _logger = Logger('SseClient');
+
+  final _onConnected = Completer<void>();
+
+  int _lastMessageId = -1;
+
+  late EventSource _eventSource;
+
+  late String _serverUrl;
+
+  Timer? _errorTimer;
+
+  /// [serverUrl] is the URL under which the server is listening for
+  /// incoming bi-directional SSE connections. [debugKey] is an optional key
+  /// that can be used to identify the SSE connection.
+  SseClient(String serverUrl, {String? debugKey})
+      : _clientId = debugKey == null
+            ? generateUuidV4()
+            : '$debugKey-${generateUuidV4()}' {
+    _serverUrl = '$serverUrl?sseClientId=$_clientId';
+    _eventSource =
+        EventSource(_serverUrl, EventSourceInit(withCredentials: true));
+    _eventSource.onOpen.first.whenComplete(() {
+      _onConnected.complete();
+      _outgoingController.stream
+          .listen(_onOutgoingMessage, onDone: _onOutgoingDone);
+    });
+    _eventSource.addEventListener('message', _onIncomingMessage.toJS);
+    _eventSource.addEventListener('control', _onIncomingControlMessage.toJS);
+
+    _eventSource.onOpen.listen((_) {
+      _errorTimer?.cancel();
+    });
+    _eventSource.onError.listen((error) {
+      if (!(_errorTimer?.isActive ?? false)) {
+        // By default the SSE client uses keep-alive.
+        // Allow for a retry to connect before giving up.
+        _errorTimer = Timer(const Duration(seconds: 5), () {
+          _closeWithError(error);
+        });
+      }
+    });
+  }
+
+  @Deprecated('Use onConnected instead.')
+  Stream<Event> get onOpen => _eventSource.onOpen;
+
+  Future<void> get onConnected => _onConnected.future;
+
+  /// Add messages to this [StreamSink] to send them to the server.
+  ///
+  /// The message added to the sink has to be JSON encodable. Messages that fail
+  /// to encode will be logged through a [Logger].
+  @override
+  StreamSink<String> get sink => _outgoingController.sink;
+
+  /// [Stream] of messages sent from the server to this client.
+  ///
+  /// A message is a decoded JSON object.
+  @override
+  Stream<String> get stream => _incomingController.stream;
+
+  void close() {
+    _eventSource.close();
+    // If the initial connection was never established. Add a listener so close
+    // adds a done event to [sink].
+    if (!_onConnected.isCompleted) _outgoingController.stream.drain<void>();
+    _incomingController.close();
+    _outgoingController.close();
+  }
+
+  void _closeWithError(Object error) {
+    _incomingController.addError(error);
+    close();
+    if (!_onConnected.isCompleted) {
+      // This call must happen after the call to close() which checks
+      // whether the completer was completed earlier.
+      _onConnected.completeError(error);
+    }
+  }
+
+  void _onIncomingControlMessage(Event message) {
+    var data = (message as MessageEvent).data;
+    if (data.dartify() == 'close') {
+      close();
+    } else {
+      throw UnsupportedError('[$_clientId] Illegal Control Message "$data"');
+    }
+  }
+
+  void _onIncomingMessage(Event message) {
+    var decoded =
+        jsonDecode(((message as MessageEvent).data as JSString).toDart);
+    _incomingController.add(decoded as String);
+  }
+
+  void _onOutgoingDone() {
+    close();
+  }
+
+  void _onOutgoingMessage(String? message) async {
+    String? encodedMessage;
+    await _requestPool.withResource(() async {
+      try {
+        encodedMessage = jsonEncode(message);
+        // ignore: avoid_catching_errors
+      } on JsonUnsupportedObjectError catch (e) {
+        _logger.warning('[$_clientId] Unable to encode outgoing message: $e');
+        // ignore: avoid_catching_errors
+      } on ArgumentError catch (e) {
+        _logger.warning('[$_clientId] Invalid argument: $e');
+      }
+      try {
+        final url = '$_serverUrl&messageId=${++_lastMessageId}';
+        await _fetch(
+            url,
+            RequestInit(
+                method: 'POST',
+                body: encodedMessage?.toJS,
+                credentials: 'include'));
+      } catch (error) {
+        final augmentedError =
+            '[$_clientId] SSE client failed to send $message:\n $error';
+        _logger.severe(augmentedError);
+        _closeWithError(augmentedError);
+      }
+    });
+  }
+}
+
+Future<void> _fetch(String resourceUrl, RequestInit options) =>
+    window.fetch(resourceUrl.toJS, options).toDart;
diff --git a/pkgs/sse/lib/server/sse_handler.dart b/pkgs/sse/lib/server/sse_handler.dart
new file mode 100644
index 0000000..bfed935
--- /dev/null
+++ b/pkgs/sse/lib/server/sse_handler.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'package:sse/src/server/sse_handler.dart' show SseConnection, SseHandler;
diff --git a/pkgs/sse/lib/src/server/sse_handler.dart b/pkgs/sse/lib/src/server/sse_handler.dart
new file mode 100644
index 0000000..376fe27
--- /dev/null
+++ b/pkgs/sse/lib/src/server/sse_handler.dart
@@ -0,0 +1,299 @@
+// 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:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:collection/collection.dart';
+import 'package:logging/logging.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:stream_channel/stream_channel.dart';
+
+// RFC 2616 requires carriage return delimiters.
+String _sseHeaders(String? origin) => 'HTTP/1.1 200 OK\r\n'
+    'Content-Type: text/event-stream\r\n'
+    'Cache-Control: no-cache\r\n'
+    'Connection: keep-alive\r\n'
+    'Access-Control-Allow-Credentials: true\r\n'
+    "${origin != null ? 'Access-Control-Allow-Origin: $origin\r\n' : ''}"
+    '\r\n\r\n';
+
+class _SseMessage {
+  final int id;
+  final String message;
+  _SseMessage(this.id, this.message);
+}
+
+/// A bi-directional SSE connection between server and browser.
+class SseConnection extends StreamChannelMixin<String> {
+  /// Incoming messages from the Browser client.
+  final _incomingController = StreamController<String>();
+
+  /// Outgoing messages to the Browser client.
+  final _outgoingController = StreamController<String>();
+
+  Sink _sink;
+
+  /// How long to wait after a connection drops before considering it closed.
+  final Duration? _keepAlive;
+
+  /// A timer counting down the KeepAlive period (null if hasn't disconnected).
+  Timer? _keepAliveTimer;
+
+  /// Whether this connection is currently in the KeepAlive timeout period.
+  bool get isInKeepAlivePeriod => _keepAliveTimer?.isActive ?? false;
+
+  /// The id of the last processed incoming message.
+  int _lastProcessedId = -1;
+
+  /// Incoming messages that have yet to be processed.
+  final _pendingMessages =
+      HeapPriorityQueue<_SseMessage>((a, b) => a.id.compareTo(b.id));
+
+  final _closedCompleter = Completer<void>();
+
+  /// Wraps the `_outgoingController.stream` to buffer events to enable keep
+  /// alive.
+  late StreamQueue _outgoingStreamQueue;
+
+  /// Creates an [SseConnection] for the supplied [_sink].
+  ///
+  /// If [keepAlive] is supplied, the connection will remain active for this
+  /// period after a disconnect and can be reconnected transparently. If there
+  /// is no reconnect within that period, the connection will be closed
+  /// normally.
+  ///
+  /// If [keepAlive] is not supplied, the connection will be closed immediately
+  /// after a disconnect.
+  SseConnection(this._sink, {Duration? keepAlive}) : _keepAlive = keepAlive {
+    _outgoingStreamQueue = StreamQueue(_outgoingController.stream);
+    unawaited(_setUpListener());
+    _outgoingController.onCancel = _close;
+    _incomingController.onCancel = _close;
+  }
+
+  Future<void> _setUpListener() async {
+    while (
+        !_outgoingController.isClosed && await _outgoingStreamQueue.hasNext) {
+      // If we're in a KeepAlive timeout, there's nowhere to send messages so
+      // wait a short period and check again.
+      if (isInKeepAlivePeriod) {
+        await Future<void>.delayed(const Duration(milliseconds: 200));
+        continue;
+      }
+
+      // Peek the data so we don't remove it from the stream if we're unable to
+      // send it.
+      final data = await _outgoingStreamQueue.peek;
+
+      // Ignore outgoing messages since the connection may have closed while
+      // waiting for the keep alive.
+      if (_closedCompleter.isCompleted) break;
+
+      try {
+        // JSON encode the message to escape new lines.
+        _sink.add('data: ${json.encode(data)}\n');
+        _sink.add('\n');
+        await _outgoingStreamQueue.next; // Consume from stream if no errors.
+      } catch (e) {
+        if ((e is StateError || e is SocketException) &&
+            (_keepAlive != null && !_closedCompleter.isCompleted)) {
+          // If we got here then the sink may have closed but the stream.onDone
+          // hasn't fired yet, so pause the subscription and skip calling
+          // `next` so the message remains in the queue to try again.
+          _handleDisconnect();
+        } else {
+          rethrow;
+        }
+      }
+    }
+  }
+
+  /// The message added to the sink has to be JSON encodable.
+  @override
+  StreamSink<String> get sink => _outgoingController.sink;
+
+  // Add messages to this [StreamSink] to send them to the server.
+  /// [Stream] of messages sent from the server to this client.
+  ///
+  /// A message is a decoded JSON object.
+  @override
+  Stream<String> get stream => _incomingController.stream;
+
+  /// Adds an incoming [message] to the [stream].
+  ///
+  /// This will buffer messages to guarantee order.
+  void _addIncomingMessage(int id, String message) {
+    _pendingMessages.add(_SseMessage(id, message));
+    while (_pendingMessages.isNotEmpty) {
+      var pendingMessage = _pendingMessages.first;
+      // Only process the next incremental message.
+      if (pendingMessage.id - _lastProcessedId <= 1) {
+        _incomingController.sink.add(pendingMessage.message);
+        _lastProcessedId = pendingMessage.id;
+        _pendingMessages.removeFirst();
+      } else {
+        // A message came out of order. Wait until we receive the previous
+        // messages to process.
+        break;
+      }
+    }
+  }
+
+  void _acceptReconnection(Sink sink) {
+    _keepAliveTimer?.cancel();
+    _sink = sink;
+  }
+
+  void _handleDisconnect() {
+    if (_keepAlive == null) {
+      // Close immediately if we're not keeping alive.
+      _close();
+    } else if (!isInKeepAlivePeriod && !_closedCompleter.isCompleted) {
+      // Otherwise if we didn't already have an active timer and we've not
+      // already been completely closed, set a timer to close after the timeout
+      // period.
+      // If the connection comes back, this will be cancelled and all messages
+      // left in the queue tried again.
+      _keepAliveTimer = Timer(_keepAlive, _close);
+    }
+  }
+
+  void _close() {
+    if (!_closedCompleter.isCompleted) {
+      _closedCompleter.complete();
+      // Cancel any existing timer in case we were told to explicitly shut down
+      // to avoid keeping the process alive.
+      _keepAliveTimer?.cancel();
+      _sink.close();
+      if (!_outgoingController.isClosed) {
+        _outgoingStreamQueue.cancel(immediate: true);
+        _outgoingController.close();
+      }
+      if (!_incomingController.isClosed) _incomingController.close();
+    }
+  }
+
+  /// Immediately close the connection, ignoring any keepAlive period.
+  void shutdown() {
+    _close();
+  }
+}
+
+/// [SseHandler] handles requests on a user defined path to create
+/// two-way communications of JSON encodable data between server and clients.
+///
+/// A server sends messages to a client through an SSE channel, while
+/// a client sends message to a server through HTTP POST requests.
+class SseHandler {
+  final _logger = Logger('SseHandler');
+  final Uri _uri;
+  final Duration? _keepAlive;
+  final _connections = <String?, SseConnection>{};
+  final _connectionController = StreamController<SseConnection>();
+
+  StreamQueue<SseConnection>? _connectionsStream;
+
+  /// [_uri] is the URL under which the server is listening for
+  /// incoming bi-directional SSE connections.
+  ///
+  /// If [keepAlive] is supplied, connections will remain active for this
+  /// period after a disconnect and can be reconnected transparently. If there
+  /// is no reconnect within that period, the connection will be closed
+  /// normally.
+  ///
+  /// If [keepAlive] is not supplied, connections will be closed immediately
+  /// after a disconnect.
+  SseHandler(this._uri, {Duration? keepAlive}) : _keepAlive = keepAlive;
+
+  StreamQueue<SseConnection> get connections =>
+      _connectionsStream ??= StreamQueue(_connectionController.stream);
+
+  shelf.Handler get handler => _handle;
+
+  int get numberOfClients => _connections.length;
+
+  shelf.Response _createSseConnection(shelf.Request req, String path) {
+    req.hijack((channel) async {
+      var sink = utf8.encoder.startChunkedConversion(channel.sink);
+      sink.add(_sseHeaders(req.headers['origin']));
+      var clientId = req.url.queryParameters['sseClientId'];
+
+      // Check if we already have a connection for this ID that is in the
+      // process of timing out
+      // (in which case we can reconnect it transparently).
+      if (_connections[clientId] != null &&
+          _connections[clientId]!.isInKeepAlivePeriod) {
+        _connections[clientId]!._acceptReconnection(sink);
+      } else {
+        var connection = SseConnection(sink, keepAlive: _keepAlive);
+        _connections[clientId] = connection;
+        unawaited(connection._closedCompleter.future.then((_) {
+          _connections.remove(clientId);
+        }));
+        _connectionController.add(connection);
+      }
+      // Remove connection when it is remotely closed or the stream is
+      // cancelled.
+      channel.stream.listen((_) {
+        // SSE is unidirectional. Responses are handled through POST requests.
+      }, onDone: () {
+        _connections[clientId]?._handleDisconnect();
+      });
+    });
+  }
+
+  String _getOriginalPath(shelf.Request req) => req.requestedUri.path;
+
+  Future<shelf.Response> _handle(shelf.Request req) async {
+    var path = _getOriginalPath(req);
+    if (_uri.path != path) {
+      return shelf.Response.notFound('');
+    }
+
+    if (req.headers['accept'] == 'text/event-stream' && req.method == 'GET') {
+      return _createSseConnection(req, path);
+    }
+
+    if (req.headers['accept'] != 'text/event-stream' && req.method == 'POST') {
+      return _handleIncomingMessage(req, path);
+    }
+
+    return shelf.Response.notFound('');
+  }
+
+  Future<shelf.Response> _handleIncomingMessage(
+      shelf.Request req, String path) async {
+    String? clientId;
+    try {
+      clientId = req.url.queryParameters['sseClientId'];
+      var messageId = int.parse(req.url.queryParameters['messageId'] ?? '0');
+      var message = await req.readAsString();
+      var jsonObject = json.decode(message) as String;
+      _connections[clientId]?._addIncomingMessage(messageId, jsonObject);
+    } catch (e, st) {
+      _logger.fine('[$clientId] Failed to handle incoming message. $e $st');
+    }
+    return shelf.Response.ok('', headers: {
+      'access-control-allow-credentials': 'true',
+      'access-control-allow-origin': _originFor(req),
+    });
+  }
+
+  String _originFor(shelf.Request req) =>
+      // Firefox does not set header "origin".
+      // https://bugzilla.mozilla.org/show_bug.cgi?id=1508661
+      req.headers['origin'] ?? req.headers['host']!;
+
+  /// Immediately close all connections, ignoring any keepAlive periods.
+  void shutdown() {
+    for (final connection in _connections.values) {
+      connection.shutdown();
+    }
+  }
+}
+
+void closeSink(SseConnection connection) => connection._sink.close();
diff --git a/pkgs/sse/lib/src/util/uuid.dart b/pkgs/sse/lib/src/util/uuid.dart
new file mode 100644
index 0000000..a1aa398
--- /dev/null
+++ b/pkgs/sse/lib/src/util/uuid.dart
@@ -0,0 +1,32 @@
+// 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:math' show Random;
+
+/// Returns a unique ID in the format:
+///
+///     f47ac10b-58cc-4372-a567-0e02b2c3d479
+///
+/// The generated uuids are 128 bit numbers encoded in a specific string format.
+/// For more information, see
+/// [en.wikipedia.org/wiki/Universally_unique_identifier](http://en.wikipedia.org/wiki/Universally_unique_identifier).
+String generateUuidV4() {
+  final random = Random();
+
+  int generateBits(int bitCount) => random.nextInt(1 << bitCount);
+
+  String printDigits(int value, int count) =>
+      value.toRadixString(16).padLeft(count, '0');
+  String bitsDigits(int bitCount, int digitCount) =>
+      printDigits(generateBits(bitCount), digitCount);
+
+  // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12.
+  var special = 8 + random.nextInt(4);
+
+  return '${bitsDigits(16, 4)}${bitsDigits(16, 4)}-'
+      '${bitsDigits(16, 4)}-'
+      '4${bitsDigits(12, 3)}-'
+      '${printDigits(special, 1)}${bitsDigits(12, 3)}-'
+      '${bitsDigits(16, 4)}${bitsDigits(16, 4)}${bitsDigits(16, 4)}';
+}
diff --git a/pkgs/sse/pubspec.yaml b/pkgs/sse/pubspec.yaml
new file mode 100644
index 0000000..bd70f74
--- /dev/null
+++ b/pkgs/sse/pubspec.yaml
@@ -0,0 +1,25 @@
+name: sse
+version: 4.1.7
+description: >-
+  Provides client and server functionality for setting up bi-directional
+  communication through Server Sent Events (SSE) and corresponding POST
+  requests.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/sse
+
+environment:
+  sdk: ^3.3.0
+
+dependencies:
+  async: ^2.0.8
+  collection: ^1.0.0
+  logging: ^1.0.0
+  pool: ^1.5.0
+  shelf: ^1.1.0
+  stream_channel: ^2.0.0
+  web: '>=0.5.0 <2.0.0'
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  shelf_static: ^1.0.0
+  test: ^1.16.6
+  webdriver: ^3.0.0
diff --git a/pkgs/sse/test/sse_test.dart b/pkgs/sse/test/sse_test.dart
new file mode 100644
index 0000000..0455baa
--- /dev/null
+++ b/pkgs/sse/test/sse_test.dart
@@ -0,0 +1,270 @@
+// 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:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as io;
+import 'package:shelf_static/shelf_static.dart';
+import 'package:sse/server/sse_handler.dart';
+import 'package:sse/src/server/sse_handler.dart' show closeSink;
+import 'package:test/test.dart';
+import 'package:webdriver/async_io.dart';
+
+void main() {
+  late HttpServer server;
+  late WebDriver webdriver;
+  late SseHandler handler;
+  late Process chromeDriver;
+
+  setUpAll(() async {
+    try {
+      chromeDriver = await Process.start(
+          'chromedriver', ['--port=4444', '--url-base=wd/hub']);
+    } catch (e) {
+      throw StateError(
+          'Could not start ChromeDriver. Is it installed?\nError: $e');
+    }
+  });
+
+  tearDownAll(() {
+    chromeDriver.kill();
+  });
+
+  group('SSE', () {
+    setUp(() async {
+      handler = SseHandler(Uri.parse('/test'));
+
+      var cascade = shelf.Cascade()
+          .add(handler.handler)
+          .add(_faviconHandler)
+          .add(createStaticHandler('test/web',
+              listDirectories: true, defaultDocument: 'index.html'));
+
+      server = await io.serve(cascade.handler, 'localhost', 0);
+      var capabilities = Capabilities.chrome
+        ..addAll({
+          Capabilities.chromeOptions: {
+            'args': ['--headless']
+          }
+        });
+      webdriver = await createDriver(desired: capabilities);
+    });
+
+    tearDown(() async {
+      await webdriver.quit();
+      await server.close();
+    });
+
+    test('can round trip messages', () async {
+      await webdriver.get('http://localhost:${server.port}');
+      var connection = await handler.connections.next;
+      connection.sink.add('blah');
+      expect(await connection.stream.first, 'blah');
+    });
+
+    test('can send a significant number of requests', () async {
+      await webdriver.get('http://localhost:${server.port}');
+      var connection = await handler.connections.next;
+      var limit = 7000;
+      for (var i = 0; i < limit; i++) {
+        connection.sink.add('$i');
+      }
+      await connection.stream.take(limit).drain<void>();
+    });
+
+    test('messages arrive in-order', () async {
+      expect(handler.numberOfClients, 0);
+      await webdriver.get('http://localhost:${server.port}');
+      var connection = await handler.connections.next;
+      expect(handler.numberOfClients, 1);
+
+      var expected = <String>[];
+      var count = 100;
+      for (var i = 0; i < count; i++) {
+        expected.add(i.toString());
+      }
+      connection.sink.add('send $count');
+
+      expect(await connection.stream.take(count).toList(), equals(expected));
+    });
+
+    test('multiple clients can connect', () async {
+      var connections = handler.connections;
+      await webdriver.get('http://localhost:${server.port}');
+      await connections.next;
+      await webdriver.get('http://localhost:${server.port}');
+      await connections.next;
+    });
+
+    test('routes data correctly', () async {
+      var connections = handler.connections;
+      await webdriver.get('http://localhost:${server.port}');
+      var connectionA = await connections.next;
+      connectionA.sink.add('foo');
+      expect(await connectionA.stream.first, 'foo');
+
+      await webdriver.get('http://localhost:${server.port}');
+      var connectionB = await connections.next;
+      connectionB.sink.add('bar');
+      expect(await connectionB.stream.first, 'bar');
+    });
+
+    test('can close from the server', () async {
+      expect(handler.numberOfClients, 0);
+      await webdriver.get('http://localhost:${server.port}');
+      var connection = await handler.connections.next;
+      expect(handler.numberOfClients, 1);
+      await connection.sink.close();
+      await pumpEventQueue();
+      expect(handler.numberOfClients, 0);
+    });
+
+    test('client reconnects after being disconnected', () async {
+      expect(handler.numberOfClients, 0);
+      await webdriver.get('http://localhost:${server.port}');
+      var connection = await handler.connections.next;
+      expect(handler.numberOfClients, 1);
+      await connection.sink.close();
+      await pumpEventQueue();
+      expect(handler.numberOfClients, 0);
+
+      // Ensure the client reconnects
+      await handler.connections.next;
+    });
+
+    test('can close from the client-side', () async {
+      expect(handler.numberOfClients, 0);
+      await webdriver.get('http://localhost:${server.port}');
+      var connection = await handler.connections.next;
+      expect(handler.numberOfClients, 1);
+
+      var closeButton = await webdriver.findElement(const By.tagName('button'));
+      await closeButton.click();
+
+      // Should complete since the connection is closed.
+      await connection.stream.drain<void>();
+      expect(handler.numberOfClients, 0);
+    });
+
+    test('cancelling the listener closes the connection', () async {
+      expect(handler.numberOfClients, 0);
+      await webdriver.get('http://localhost:${server.port}');
+      var connection = await handler.connections.next;
+      expect(handler.numberOfClients, 1);
+
+      var sub = connection.stream.listen((_) {});
+      await sub.cancel();
+      await pumpEventQueue();
+      expect(handler.numberOfClients, 0);
+    });
+
+    test('disconnects when navigating away', () async {
+      await webdriver.get('http://localhost:${server.port}');
+      expect(handler.numberOfClients, 1);
+
+      await webdriver.get('chrome://version/');
+      expect(handler.numberOfClients, 0);
+    });
+  });
+
+  group('SSE with server keep-alive', () {
+    setUp(() async {
+      handler =
+          SseHandler(Uri.parse('/test'), keepAlive: const Duration(seconds: 5));
+
+      var cascade = shelf.Cascade()
+          .add(handler.handler)
+          .add(_faviconHandler)
+          .add(createStaticHandler('test/web',
+              listDirectories: true, defaultDocument: 'index.html'));
+
+      server = await io.serve(cascade.handler, 'localhost', 0);
+      var capabilities = Capabilities.chrome
+        ..addAll({
+          Capabilities.chromeOptions: {
+            'args': ['--headless']
+          }
+        });
+      webdriver = await createDriver(desired: capabilities);
+    });
+
+    tearDown(() async {
+      await webdriver.quit();
+      await server.close();
+    });
+
+    test('client reconnect use the same connection', () async {
+      expect(handler.numberOfClients, 0);
+      await webdriver.get('http://localhost:${server.port}');
+      var connection = await handler.connections.next;
+      expect(handler.numberOfClients, 1);
+
+      // Close the underlying connection.
+      closeSink(connection);
+      // Ensure we can still round-trip data on the original connection and that
+      // the connection is no longer marked keep-alive once it's reconnected.
+      connection.sink.add('bar');
+      var queue = StreamQueue(connection.stream);
+      expect(await queue.next, 'bar');
+
+      // Now check that we can reconnect multiple times.
+      closeSink(connection);
+      connection.sink.add('bar');
+      expect(await queue.next, 'bar');
+      expect(handler.numberOfClients, 1);
+    });
+
+    test('messages sent during disconnect arrive in-order', () async {
+      expect(handler.numberOfClients, 0);
+      await webdriver.get('http://localhost:${server.port}');
+      var connection = await handler.connections.next;
+      expect(handler.numberOfClients, 1);
+
+      // Close the underlying connection.
+      closeSink(connection);
+      connection.sink.add('one');
+      connection.sink.add('two');
+      await pumpEventQueue();
+
+      // Ensure there's still a connection.
+      expect(handler.numberOfClients, 1);
+
+      // Ensure messages arrive in the same order
+      expect(await connection.stream.take(2).toList(), equals(['one', 'two']));
+    });
+
+    test('explicit shutdown does not wait for keepAlive', () async {
+      expect(handler.numberOfClients, 0);
+      await webdriver.get('http://localhost:${server.port}');
+      await handler.connections.next;
+      expect(handler.numberOfClients, 1);
+
+      // Close the underlying connection.
+      handler.shutdown();
+
+      // Wait for a short period to allow the connection to close, but not
+      // long enough that the 30second keep-alive may have expired.
+      var maxPumps = 50;
+      while (handler.numberOfClients > 0 && maxPumps-- > 0) {
+        await pumpEventQueue(times: 1);
+      }
+
+      // Ensure there are not connected clients.
+      expect(handler.numberOfClients, 0);
+    });
+  }, timeout: const Timeout(Duration(seconds: 120)));
+}
+
+FutureOr<shelf.Response> _faviconHandler(shelf.Request request) {
+  if (request.url.path.endsWith('favicon.ico')) {
+    return shelf.Response.ok('');
+  }
+  return shelf.Response.notFound('');
+}
diff --git a/pkgs/sse/test/web/index.dart b/pkgs/sse/test/web/index.dart
new file mode 100644
index 0000000..c4d78cd
--- /dev/null
+++ b/pkgs/sse/test/web/index.dart
@@ -0,0 +1,25 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:sse/client/sse_client.dart';
+import 'package:web/web.dart';
+
+void main() {
+  var channel = SseClient('/test');
+
+  document.querySelector('button')!.onClick.listen((_) {
+    channel.sink.close();
+  });
+
+  channel.stream.listen((s) {
+    if (s.startsWith('send ')) {
+      var count = int.parse(s.split(' ').last);
+      for (var i = 0; i < count; i++) {
+        channel.sink.add('$i');
+      }
+    } else {
+      channel.sink.add(s);
+    }
+  });
+}
diff --git a/pkgs/sse/test/web/index.dart.js b/pkgs/sse/test/web/index.dart.js
new file mode 100644
index 0000000..e1b37b9
--- /dev/null
+++ b/pkgs/sse/test/web/index.dart.js
@@ -0,0 +1,8851 @@
+// Generated by dart2js (NullSafetyMode.sound, csp, intern-composite-values), the Dart to JavaScript compiler version: 3.4.0-157.0.dev.
+// The code supports the following hooks:
+// dartPrint(message):
+//    if this function is defined it is called instead of the Dart [print]
+//    method.
+//
+// dartMainRunner(main, args):
+//    if this function is defined, the Dart [main] method will not be invoked
+//    directly. Instead, a closure that will invoke [main], and its arguments
+//    [args] is passed to [dartMainRunner].
+//
+// dartDeferredLibraryLoader(uri, successCallback, errorCallback, loadId, loadPriority):
+//    if this function is defined, it will be called when a deferred library
+//    is loaded. It should load and eval the javascript of `uri`, and call
+//    successCallback. If it fails to do so, it should call errorCallback with
+//    an error. The loadId argument is the deferred import that resulted in
+//    this uri being loaded. The loadPriority argument is the priority the
+//    library should be loaded with as specified in the code via the
+//    load-priority annotation (0: normal, 1: high).
+// dartDeferredLibraryMultiLoader(uris, successCallback, errorCallback, loadId, loadPriority):
+//    if this function is defined, it will be called when a deferred library
+//    is loaded. It should load and eval the javascript of every URI in `uris`,
+//    and call successCallback. If it fails to do so, it should call
+//    errorCallback with an error. The loadId argument is the deferred import
+//    that resulted in this uri being loaded. The loadPriority argument is the
+//    priority the library should be loaded with as specified in the code via
+//    the load-priority annotation (0: normal, 1: high).
+//
+// dartCallInstrumentation(id, qualifiedName):
+//    if this function is defined, it will be called at each entry of a
+//    method or constructor. Used only when compiling programs with
+//    --experiment-call-instrumentation.
+(function dartProgram() {
+  function copyProperties(from, to) {
+    var keys = Object.keys(from);
+    for (var i = 0; i < keys.length; i++) {
+      var key = keys[i];
+      to[key] = from[key];
+    }
+  }
+  function mixinPropertiesHard(from, to) {
+    var keys = Object.keys(from);
+    for (var i = 0; i < keys.length; i++) {
+      var key = keys[i];
+      if (!to.hasOwnProperty(key)) {
+        to[key] = from[key];
+      }
+    }
+  }
+  function mixinPropertiesEasy(from, to) {
+    Object.assign(to, from);
+  }
+  var supportsDirectProtoAccess = function() {
+    var cls = function() {
+    };
+    cls.prototype = {p: {}};
+    var object = new cls();
+    if (!(Object.getPrototypeOf(object) && Object.getPrototypeOf(object).p === cls.prototype.p))
+      return false;
+    try {
+      if (typeof navigator != "undefined" && typeof navigator.userAgent == "string" && navigator.userAgent.indexOf("Chrome/") >= 0)
+        return true;
+      if (typeof version == "function" && version.length == 0) {
+        var v = version();
+        if (/^\d+\.\d+\.\d+\.\d+$/.test(v))
+          return true;
+      }
+    } catch (_) {
+    }
+    return false;
+  }();
+  function inherit(cls, sup) {
+    cls.prototype.constructor = cls;
+    cls.prototype["$is" + cls.name] = cls;
+    if (sup != null) {
+      if (supportsDirectProtoAccess) {
+        Object.setPrototypeOf(cls.prototype, sup.prototype);
+        return;
+      }
+      var clsPrototype = Object.create(sup.prototype);
+      copyProperties(cls.prototype, clsPrototype);
+      cls.prototype = clsPrototype;
+    }
+  }
+  function inheritMany(sup, classes) {
+    for (var i = 0; i < classes.length; i++) {
+      inherit(classes[i], sup);
+    }
+  }
+  function mixinEasy(cls, mixin) {
+    mixinPropertiesEasy(mixin.prototype, cls.prototype);
+    cls.prototype.constructor = cls;
+  }
+  function mixinHard(cls, mixin) {
+    mixinPropertiesHard(mixin.prototype, cls.prototype);
+    cls.prototype.constructor = cls;
+  }
+  function lazy(holder, name, getterName, initializer) {
+    var uninitializedSentinel = holder;
+    holder[name] = uninitializedSentinel;
+    holder[getterName] = function() {
+      if (holder[name] === uninitializedSentinel) {
+        holder[name] = initializer();
+      }
+      holder[getterName] = function() {
+        return this[name];
+      };
+      return holder[name];
+    };
+  }
+  function lazyFinal(holder, name, getterName, initializer) {
+    var uninitializedSentinel = holder;
+    holder[name] = uninitializedSentinel;
+    holder[getterName] = function() {
+      if (holder[name] === uninitializedSentinel) {
+        var value = initializer();
+        if (holder[name] !== uninitializedSentinel) {
+          A.throwLateFieldADI(name);
+        }
+        holder[name] = value;
+      }
+      var finalValue = holder[name];
+      holder[getterName] = function() {
+        return finalValue;
+      };
+      return finalValue;
+    };
+  }
+  function makeConstList(list) {
+    list.immutable$list = Array;
+    list.fixed$length = Array;
+    return list;
+  }
+  function convertToFastObject(properties) {
+    function t() {
+    }
+    t.prototype = properties;
+    new t();
+    return properties;
+  }
+  function convertAllToFastObject(arrayOfObjects) {
+    for (var i = 0; i < arrayOfObjects.length; ++i) {
+      convertToFastObject(arrayOfObjects[i]);
+    }
+  }
+  var functionCounter = 0;
+  function instanceTearOffGetter(isIntercepted, parameters) {
+    var cache = null;
+    return isIntercepted ? function(receiver) {
+      if (cache === null)
+        cache = A.closureFromTearOff(parameters);
+      return new cache(receiver, this);
+    } : function() {
+      if (cache === null)
+        cache = A.closureFromTearOff(parameters);
+      return new cache(this, null);
+    };
+  }
+  function staticTearOffGetter(parameters) {
+    var cache = null;
+    return function() {
+      if (cache === null)
+        cache = A.closureFromTearOff(parameters).prototype;
+      return cache;
+    };
+  }
+  var typesOffset = 0;
+  function tearOffParameters(container, isStatic, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, needsDirectAccess) {
+    if (typeof funType == "number") {
+      funType += typesOffset;
+    }
+    return {co: container, iS: isStatic, iI: isIntercepted, rC: requiredParameterCount, dV: optionalParameterDefaultValues, cs: callNames, fs: funsOrNames, fT: funType, aI: applyIndex || 0, nDA: needsDirectAccess};
+  }
+  function installStaticTearOff(holder, getterName, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex) {
+    var parameters = tearOffParameters(holder, true, false, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, false);
+    var getterFunction = staticTearOffGetter(parameters);
+    holder[getterName] = getterFunction;
+  }
+  function installInstanceTearOff(prototype, getterName, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, needsDirectAccess) {
+    isIntercepted = !!isIntercepted;
+    var parameters = tearOffParameters(prototype, false, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, !!needsDirectAccess);
+    var getterFunction = instanceTearOffGetter(isIntercepted, parameters);
+    prototype[getterName] = getterFunction;
+  }
+  function setOrUpdateInterceptorsByTag(newTags) {
+    var tags = init.interceptorsByTag;
+    if (!tags) {
+      init.interceptorsByTag = newTags;
+      return;
+    }
+    copyProperties(newTags, tags);
+  }
+  function setOrUpdateLeafTags(newTags) {
+    var tags = init.leafTags;
+    if (!tags) {
+      init.leafTags = newTags;
+      return;
+    }
+    copyProperties(newTags, tags);
+  }
+  function updateTypes(newTypes) {
+    var types = init.types;
+    var length = types.length;
+    types.push.apply(types, newTypes);
+    return length;
+  }
+  function updateHolder(holder, newHolder) {
+    copyProperties(newHolder, holder);
+    return holder;
+  }
+  var hunkHelpers = function() {
+    var mkInstance = function(isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, applyIndex) {
+        return function(container, getterName, name, funType) {
+          return installInstanceTearOff(container, getterName, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, [name], funType, applyIndex, false);
+        };
+      },
+      mkStatic = function(requiredParameterCount, optionalParameterDefaultValues, callNames, applyIndex) {
+        return function(container, getterName, name, funType) {
+          return installStaticTearOff(container, getterName, requiredParameterCount, optionalParameterDefaultValues, callNames, [name], funType, applyIndex);
+        };
+      };
+    return {inherit: inherit, inheritMany: inheritMany, mixin: mixinEasy, mixinHard: mixinHard, installStaticTearOff: installStaticTearOff, installInstanceTearOff: installInstanceTearOff, _instance_0u: mkInstance(0, 0, null, ["call$0"], 0), _instance_1u: mkInstance(0, 1, null, ["call$1"], 0), _instance_2u: mkInstance(0, 2, null, ["call$2"], 0), _instance_0i: mkInstance(1, 0, null, ["call$0"], 0), _instance_1i: mkInstance(1, 1, null, ["call$1"], 0), _instance_2i: mkInstance(1, 2, null, ["call$2"], 0), _static_0: mkStatic(0, null, ["call$0"], 0), _static_1: mkStatic(1, null, ["call$1"], 0), _static_2: mkStatic(2, null, ["call$2"], 0), makeConstList: makeConstList, lazy: lazy, lazyFinal: lazyFinal, updateHolder: updateHolder, convertToFastObject: convertToFastObject, updateTypes: updateTypes, setOrUpdateInterceptorsByTag: setOrUpdateInterceptorsByTag, setOrUpdateLeafTags: setOrUpdateLeafTags};
+  }();
+  function initializeDeferredHunk(hunk) {
+    typesOffset = init.types.length;
+    hunk(hunkHelpers, init, holders, $);
+  }
+  var J = {
+    makeDispatchRecord(interceptor, proto, extension, indexability) {
+      return {i: interceptor, p: proto, e: extension, x: indexability};
+    },
+    getNativeInterceptor(object) {
+      var proto, objectProto, $constructor, interceptor, t1,
+        record = object[init.dispatchPropertyName];
+      if (record == null)
+        if ($.initNativeDispatchFlag == null) {
+          A.initNativeDispatch();
+          record = object[init.dispatchPropertyName];
+        }
+      if (record != null) {
+        proto = record.p;
+        if (false === proto)
+          return record.i;
+        if (true === proto)
+          return object;
+        objectProto = Object.getPrototypeOf(object);
+        if (proto === objectProto)
+          return record.i;
+        if (record.e === objectProto)
+          throw A.wrapException(A.UnimplementedError$("Return interceptor for " + A.S(proto(object, record))));
+      }
+      $constructor = object.constructor;
+      if ($constructor == null)
+        interceptor = null;
+      else {
+        t1 = $._JS_INTEROP_INTERCEPTOR_TAG;
+        if (t1 == null)
+          t1 = $._JS_INTEROP_INTERCEPTOR_TAG = init.getIsolateTag("_$dart_js");
+        interceptor = $constructor[t1];
+      }
+      if (interceptor != null)
+        return interceptor;
+      interceptor = A.lookupAndCacheInterceptor(object);
+      if (interceptor != null)
+        return interceptor;
+      if (typeof object == "function")
+        return B.JavaScriptFunction_methods;
+      proto = Object.getPrototypeOf(object);
+      if (proto == null)
+        return B.PlainJavaScriptObject_methods;
+      if (proto === Object.prototype)
+        return B.PlainJavaScriptObject_methods;
+      if (typeof $constructor == "function") {
+        t1 = $._JS_INTEROP_INTERCEPTOR_TAG;
+        if (t1 == null)
+          t1 = $._JS_INTEROP_INTERCEPTOR_TAG = init.getIsolateTag("_$dart_js");
+        Object.defineProperty($constructor, t1, {value: B.UnknownJavaScriptObject_methods, enumerable: false, writable: true, configurable: true});
+        return B.UnknownJavaScriptObject_methods;
+      }
+      return B.UnknownJavaScriptObject_methods;
+    },
+    JSArray_JSArray$fixed($length, $E) {
+      if ($length < 0 || $length > 4294967295)
+        throw A.wrapException(A.RangeError$range($length, 0, 4294967295, "length", null));
+      return J.JSArray_JSArray$markFixed(new Array($length), $E);
+    },
+    JSArray_JSArray$growable($length, $E) {
+      if ($length < 0)
+        throw A.wrapException(A.ArgumentError$("Length must be a non-negative integer: " + $length, null));
+      return A._setArrayType(new Array($length), $E._eval$1("JSArray<0>"));
+    },
+    JSArray_JSArray$markFixed(allocation, $E) {
+      return J.JSArray_markFixedList(A._setArrayType(allocation, $E._eval$1("JSArray<0>")), $E);
+    },
+    JSArray_markFixedList(list, $T) {
+      list.fixed$length = Array;
+      return list;
+    },
+    JSArray_markUnmodifiableList(list) {
+      list.fixed$length = Array;
+      list.immutable$list = Array;
+      return list;
+    },
+    getInterceptor$(receiver) {
+      if (typeof receiver == "number") {
+        if (Math.floor(receiver) == receiver)
+          return J.JSInt.prototype;
+        return J.JSNumNotInt.prototype;
+      }
+      if (typeof receiver == "string")
+        return J.JSString.prototype;
+      if (receiver == null)
+        return J.JSNull.prototype;
+      if (typeof receiver == "boolean")
+        return J.JSBool.prototype;
+      if (Array.isArray(receiver))
+        return J.JSArray.prototype;
+      if (typeof receiver != "object") {
+        if (typeof receiver == "function")
+          return J.JavaScriptFunction.prototype;
+        if (typeof receiver == "symbol")
+          return J.JavaScriptSymbol.prototype;
+        if (typeof receiver == "bigint")
+          return J.JavaScriptBigInt.prototype;
+        return receiver;
+      }
+      if (receiver instanceof A.Object)
+        return receiver;
+      return J.getNativeInterceptor(receiver);
+    },
+    getInterceptor$asx(receiver) {
+      if (typeof receiver == "string")
+        return J.JSString.prototype;
+      if (receiver == null)
+        return receiver;
+      if (Array.isArray(receiver))
+        return J.JSArray.prototype;
+      if (typeof receiver != "object") {
+        if (typeof receiver == "function")
+          return J.JavaScriptFunction.prototype;
+        if (typeof receiver == "symbol")
+          return J.JavaScriptSymbol.prototype;
+        if (typeof receiver == "bigint")
+          return J.JavaScriptBigInt.prototype;
+        return receiver;
+      }
+      if (receiver instanceof A.Object)
+        return receiver;
+      return J.getNativeInterceptor(receiver);
+    },
+    getInterceptor$ax(receiver) {
+      if (receiver == null)
+        return receiver;
+      if (Array.isArray(receiver))
+        return J.JSArray.prototype;
+      if (typeof receiver != "object") {
+        if (typeof receiver == "function")
+          return J.JavaScriptFunction.prototype;
+        if (typeof receiver == "symbol")
+          return J.JavaScriptSymbol.prototype;
+        if (typeof receiver == "bigint")
+          return J.JavaScriptBigInt.prototype;
+        return receiver;
+      }
+      if (receiver instanceof A.Object)
+        return receiver;
+      return J.getNativeInterceptor(receiver);
+    },
+    getInterceptor$s(receiver) {
+      if (typeof receiver == "string")
+        return J.JSString.prototype;
+      if (receiver == null)
+        return receiver;
+      if (!(receiver instanceof A.Object))
+        return J.UnknownJavaScriptObject.prototype;
+      return receiver;
+    },
+    get$hashCode$(receiver) {
+      return J.getInterceptor$(receiver).get$hashCode(receiver);
+    },
+    get$iterator$ax(receiver) {
+      return J.getInterceptor$ax(receiver).get$iterator(receiver);
+    },
+    get$length$asx(receiver) {
+      return J.getInterceptor$asx(receiver).get$length(receiver);
+    },
+    get$runtimeType$(receiver) {
+      return J.getInterceptor$(receiver).get$runtimeType(receiver);
+    },
+    $eq$(receiver, a0) {
+      if (receiver == null)
+        return a0 == null;
+      if (typeof receiver != "object")
+        return a0 != null && receiver === a0;
+      return J.getInterceptor$(receiver).$eq(receiver, a0);
+    },
+    matchAsPrefix$2$s(receiver, a0, a1) {
+      return J.getInterceptor$s(receiver).matchAsPrefix$2(receiver, a0, a1);
+    },
+    noSuchMethod$1$(receiver, a0) {
+      return J.getInterceptor$(receiver).noSuchMethod$1(receiver, a0);
+    },
+    toString$0$(receiver) {
+      return J.getInterceptor$(receiver).toString$0(receiver);
+    },
+    Interceptor: function Interceptor() {
+    },
+    JSBool: function JSBool() {
+    },
+    JSNull: function JSNull() {
+    },
+    JavaScriptObject: function JavaScriptObject() {
+    },
+    LegacyJavaScriptObject: function LegacyJavaScriptObject() {
+    },
+    PlainJavaScriptObject: function PlainJavaScriptObject() {
+    },
+    UnknownJavaScriptObject: function UnknownJavaScriptObject() {
+    },
+    JavaScriptFunction: function JavaScriptFunction() {
+    },
+    JavaScriptBigInt: function JavaScriptBigInt() {
+    },
+    JavaScriptSymbol: function JavaScriptSymbol() {
+    },
+    JSArray: function JSArray(t0) {
+      this.$ti = t0;
+    },
+    JSUnmodifiableArray: function JSUnmodifiableArray(t0) {
+      this.$ti = t0;
+    },
+    ArrayIterator: function ArrayIterator(t0, t1, t2) {
+      var _ = this;
+      _._iterable = t0;
+      _._length = t1;
+      _._index = 0;
+      _._current = null;
+      _.$ti = t2;
+    },
+    JSNumber: function JSNumber() {
+    },
+    JSInt: function JSInt() {
+    },
+    JSNumNotInt: function JSNumNotInt() {
+    },
+    JSString: function JSString() {
+    }
+  },
+  A = {JS_CONST: function JS_CONST() {
+    },
+    checkNotNullable(value, $name, $T) {
+      return value;
+    },
+    isToStringVisiting(object) {
+      var t1, i;
+      for (t1 = $.toStringVisiting.length, i = 0; i < t1; ++i)
+        if (object === $.toStringVisiting[i])
+          return true;
+      return false;
+    },
+    IterableElementError_noElement() {
+      return new A.StateError("No element");
+    },
+    IterableElementError_tooFew() {
+      return new A.StateError("Too few elements");
+    },
+    LateError: function LateError(t0) {
+      this._message = t0;
+    },
+    nullFuture_closure: function nullFuture_closure() {
+    },
+    EfficientLengthIterable: function EfficientLengthIterable() {
+    },
+    ListIterable: function ListIterable() {
+    },
+    ListIterator: function ListIterator(t0, t1, t2) {
+      var _ = this;
+      _.__internal$_iterable = t0;
+      _.__internal$_length = t1;
+      _.__internal$_index = 0;
+      _.__internal$_current = null;
+      _.$ti = t2;
+    },
+    FixedLengthListMixin: function FixedLengthListMixin() {
+    },
+    Symbol: function Symbol(t0) {
+      this._name = t0;
+    },
+    unminifyOrTag(rawClassName) {
+      var preserved = init.mangledGlobalNames[rawClassName];
+      if (preserved != null)
+        return preserved;
+      return rawClassName;
+    },
+    isJsIndexable(object, record) {
+      var result;
+      if (record != null) {
+        result = record.x;
+        if (result != null)
+          return result;
+      }
+      return type$.JavaScriptIndexingBehavior_dynamic._is(object);
+    },
+    S(value) {
+      var result;
+      if (typeof value == "string")
+        return value;
+      if (typeof value == "number") {
+        if (value !== 0)
+          return "" + value;
+      } else if (true === value)
+        return "true";
+      else if (false === value)
+        return "false";
+      else if (value == null)
+        return "null";
+      result = J.toString$0$(value);
+      return result;
+    },
+    Primitives_objectHashCode(object) {
+      var hash,
+        property = $.Primitives__identityHashCodeProperty;
+      if (property == null)
+        property = $.Primitives__identityHashCodeProperty = Symbol("identityHashCode");
+      hash = object[property];
+      if (hash == null) {
+        hash = Math.random() * 0x3fffffff | 0;
+        object[property] = hash;
+      }
+      return hash;
+    },
+    Primitives_parseInt(source, radix) {
+      var decimalMatch, maxCharCode, digitsPart, t1, i, _null = null,
+        match = /^\s*[+-]?((0x[a-f0-9]+)|(\d+)|([a-z0-9]+))\s*$/i.exec(source);
+      if (match == null)
+        return _null;
+      if (3 >= match.length)
+        return A.ioore(match, 3);
+      decimalMatch = match[3];
+      if (radix == null) {
+        if (decimalMatch != null)
+          return parseInt(source, 10);
+        if (match[2] != null)
+          return parseInt(source, 16);
+        return _null;
+      }
+      if (radix < 2 || radix > 36)
+        throw A.wrapException(A.RangeError$range(radix, 2, 36, "radix", _null));
+      if (radix === 10 && decimalMatch != null)
+        return parseInt(source, 10);
+      if (radix < 10 || decimalMatch == null) {
+        maxCharCode = radix <= 10 ? 47 + radix : 86 + radix;
+        digitsPart = match[1];
+        for (t1 = digitsPart.length, i = 0; i < t1; ++i)
+          if ((digitsPart.charCodeAt(i) | 32) > maxCharCode)
+            return _null;
+      }
+      return parseInt(source, radix);
+    },
+    Primitives_objectTypeName(object) {
+      return A.Primitives__objectTypeNameNewRti(object);
+    },
+    Primitives__objectTypeNameNewRti(object) {
+      var interceptor, dispatchName, $constructor, constructorName;
+      if (object instanceof A.Object)
+        return A._rtiToString(A.instanceType(object), null);
+      interceptor = J.getInterceptor$(object);
+      if (interceptor === B.Interceptor_methods || interceptor === B.JavaScriptObject_methods || type$.UnknownJavaScriptObject._is(object)) {
+        dispatchName = B.C_JS_CONST(object);
+        if (dispatchName !== "Object" && dispatchName !== "")
+          return dispatchName;
+        $constructor = object.constructor;
+        if (typeof $constructor == "function") {
+          constructorName = $constructor.name;
+          if (typeof constructorName == "string" && constructorName !== "Object" && constructorName !== "")
+            return constructorName;
+        }
+      }
+      return A._rtiToString(A.instanceType(object), null);
+    },
+    Primitives_safeToString(object) {
+      if (typeof object == "number" || A._isBool(object))
+        return J.toString$0$(object);
+      if (typeof object == "string")
+        return JSON.stringify(object);
+      if (object instanceof A.Closure)
+        return object.toString$0(0);
+      return "Instance of '" + A.Primitives_objectTypeName(object) + "'";
+    },
+    Primitives_stringFromCharCode(charCode) {
+      var bits;
+      if (0 <= charCode) {
+        if (charCode <= 65535)
+          return String.fromCharCode(charCode);
+        if (charCode <= 1114111) {
+          bits = charCode - 65536;
+          return String.fromCharCode((B.JSInt_methods._shrOtherPositive$1(bits, 10) | 55296) >>> 0, bits & 1023 | 56320);
+        }
+      }
+      throw A.wrapException(A.RangeError$range(charCode, 0, 1114111, null, null));
+    },
+    Primitives_lazyAsJsDate(receiver) {
+      if (receiver.date === void 0)
+        receiver.date = new Date(receiver._value);
+      return receiver.date;
+    },
+    Primitives_getYear(receiver) {
+      return receiver.isUtc ? A.Primitives_lazyAsJsDate(receiver).getUTCFullYear() + 0 : A.Primitives_lazyAsJsDate(receiver).getFullYear() + 0;
+    },
+    Primitives_getMonth(receiver) {
+      return receiver.isUtc ? A.Primitives_lazyAsJsDate(receiver).getUTCMonth() + 1 : A.Primitives_lazyAsJsDate(receiver).getMonth() + 1;
+    },
+    Primitives_getDay(receiver) {
+      return receiver.isUtc ? A.Primitives_lazyAsJsDate(receiver).getUTCDate() + 0 : A.Primitives_lazyAsJsDate(receiver).getDate() + 0;
+    },
+    Primitives_getHours(receiver) {
+      return receiver.isUtc ? A.Primitives_lazyAsJsDate(receiver).getUTCHours() + 0 : A.Primitives_lazyAsJsDate(receiver).getHours() + 0;
+    },
+    Primitives_getMinutes(receiver) {
+      return receiver.isUtc ? A.Primitives_lazyAsJsDate(receiver).getUTCMinutes() + 0 : A.Primitives_lazyAsJsDate(receiver).getMinutes() + 0;
+    },
+    Primitives_getSeconds(receiver) {
+      return receiver.isUtc ? A.Primitives_lazyAsJsDate(receiver).getUTCSeconds() + 0 : A.Primitives_lazyAsJsDate(receiver).getSeconds() + 0;
+    },
+    Primitives_getMilliseconds(receiver) {
+      return receiver.isUtc ? A.Primitives_lazyAsJsDate(receiver).getUTCMilliseconds() + 0 : A.Primitives_lazyAsJsDate(receiver).getMilliseconds() + 0;
+    },
+    Primitives_functionNoSuchMethod($function, positionalArguments, namedArguments) {
+      var $arguments, namedArgumentList, t1 = {};
+      t1.argumentCount = 0;
+      $arguments = [];
+      namedArgumentList = [];
+      t1.argumentCount = positionalArguments.length;
+      B.JSArray_methods.addAll$1($arguments, positionalArguments);
+      t1.names = "";
+      if (namedArguments != null && namedArguments.__js_helper$_length !== 0)
+        namedArguments.forEach$1(0, new A.Primitives_functionNoSuchMethod_closure(t1, namedArgumentList, $arguments));
+      return J.noSuchMethod$1$($function, new A.JSInvocationMirror(B.Symbol_call, 0, $arguments, namedArgumentList, 0));
+    },
+    Primitives_applyFunction($function, positionalArguments, namedArguments) {
+      var t1, argumentCount, jsStub;
+      if (Array.isArray(positionalArguments))
+        t1 = namedArguments == null || namedArguments.__js_helper$_length === 0;
+      else
+        t1 = false;
+      if (t1) {
+        argumentCount = positionalArguments.length;
+        if (argumentCount === 0) {
+          if (!!$function.call$0)
+            return $function.call$0();
+        } else if (argumentCount === 1) {
+          if (!!$function.call$1)
+            return $function.call$1(positionalArguments[0]);
+        } else if (argumentCount === 2) {
+          if (!!$function.call$2)
+            return $function.call$2(positionalArguments[0], positionalArguments[1]);
+        } else if (argumentCount === 3) {
+          if (!!$function.call$3)
+            return $function.call$3(positionalArguments[0], positionalArguments[1], positionalArguments[2]);
+        } else if (argumentCount === 4) {
+          if (!!$function.call$4)
+            return $function.call$4(positionalArguments[0], positionalArguments[1], positionalArguments[2], positionalArguments[3]);
+        } else if (argumentCount === 5)
+          if (!!$function.call$5)
+            return $function.call$5(positionalArguments[0], positionalArguments[1], positionalArguments[2], positionalArguments[3], positionalArguments[4]);
+        jsStub = $function["call" + "$" + argumentCount];
+        if (jsStub != null)
+          return jsStub.apply($function, positionalArguments);
+      }
+      return A.Primitives__generalApplyFunction($function, positionalArguments, namedArguments);
+    },
+    Primitives__generalApplyFunction($function, positionalArguments, namedArguments) {
+      var defaultValuesClosure, t1, defaultValues, interceptor, jsFunction, maxArguments, missingDefaults, keys, _i, defaultValue, used, key,
+        $arguments = Array.isArray(positionalArguments) ? positionalArguments : A.List_List$of(positionalArguments, true, type$.dynamic),
+        argumentCount = $arguments.length,
+        requiredParameterCount = $function.$requiredArgCount;
+      if (argumentCount < requiredParameterCount)
+        return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+      defaultValuesClosure = $function.$defaultValues;
+      t1 = defaultValuesClosure == null;
+      defaultValues = !t1 ? defaultValuesClosure() : null;
+      interceptor = J.getInterceptor$($function);
+      jsFunction = interceptor["call*"];
+      if (typeof jsFunction == "string")
+        jsFunction = interceptor[jsFunction];
+      if (t1) {
+        if (namedArguments != null && namedArguments.__js_helper$_length !== 0)
+          return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+        if (argumentCount === requiredParameterCount)
+          return jsFunction.apply($function, $arguments);
+        return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+      }
+      if (Array.isArray(defaultValues)) {
+        if (namedArguments != null && namedArguments.__js_helper$_length !== 0)
+          return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+        maxArguments = requiredParameterCount + defaultValues.length;
+        if (argumentCount > maxArguments)
+          return A.Primitives_functionNoSuchMethod($function, $arguments, null);
+        if (argumentCount < maxArguments) {
+          missingDefaults = defaultValues.slice(argumentCount - requiredParameterCount);
+          if ($arguments === positionalArguments)
+            $arguments = A.List_List$of($arguments, true, type$.dynamic);
+          B.JSArray_methods.addAll$1($arguments, missingDefaults);
+        }
+        return jsFunction.apply($function, $arguments);
+      } else {
+        if (argumentCount > requiredParameterCount)
+          return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+        if ($arguments === positionalArguments)
+          $arguments = A.List_List$of($arguments, true, type$.dynamic);
+        keys = Object.keys(defaultValues);
+        if (namedArguments == null)
+          for (t1 = keys.length, _i = 0; _i < keys.length; keys.length === t1 || (0, A.throwConcurrentModificationError)(keys), ++_i) {
+            defaultValue = defaultValues[A._asString(keys[_i])];
+            if (B.C__Required === defaultValue)
+              return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+            B.JSArray_methods.add$1($arguments, defaultValue);
+          }
+        else {
+          for (t1 = keys.length, used = 0, _i = 0; _i < keys.length; keys.length === t1 || (0, A.throwConcurrentModificationError)(keys), ++_i) {
+            key = A._asString(keys[_i]);
+            if (namedArguments.containsKey$1(key)) {
+              ++used;
+              B.JSArray_methods.add$1($arguments, namedArguments.$index(0, key));
+            } else {
+              defaultValue = defaultValues[key];
+              if (B.C__Required === defaultValue)
+                return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+              B.JSArray_methods.add$1($arguments, defaultValue);
+            }
+          }
+          if (used !== namedArguments.__js_helper$_length)
+            return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+        }
+        return jsFunction.apply($function, $arguments);
+      }
+    },
+    ioore(receiver, index) {
+      if (receiver == null)
+        J.get$length$asx(receiver);
+      throw A.wrapException(A.diagnoseIndexError(receiver, index));
+    },
+    diagnoseIndexError(indexable, index) {
+      var $length, _s5_ = "index";
+      if (!A._isInt(index))
+        return new A.ArgumentError(true, index, _s5_, null);
+      $length = A._asInt(J.get$length$asx(indexable));
+      if (index < 0 || index >= $length)
+        return A.IndexError$withLength(index, $length, indexable, null, _s5_);
+      return A.RangeError$value(index, _s5_);
+    },
+    wrapException(ex) {
+      return A.initializeExceptionWrapper(new Error(), ex);
+    },
+    initializeExceptionWrapper(wrapper, ex) {
+      var t1;
+      if (ex == null)
+        ex = new A.TypeError();
+      wrapper.dartException = ex;
+      t1 = A.toStringWrapper;
+      if ("defineProperty" in Object) {
+        Object.defineProperty(wrapper, "message", {get: t1});
+        wrapper.name = "";
+      } else
+        wrapper.toString = t1;
+      return wrapper;
+    },
+    toStringWrapper() {
+      return J.toString$0$(this.dartException);
+    },
+    throwExpression(ex) {
+      throw A.wrapException(ex);
+    },
+    throwExpressionWithWrapper(ex, wrapper) {
+      throw A.initializeExceptionWrapper(wrapper, ex);
+    },
+    throwConcurrentModificationError(collection) {
+      throw A.wrapException(A.ConcurrentModificationError$(collection));
+    },
+    TypeErrorDecoder_extractPattern(message) {
+      var match, $arguments, argumentsExpr, expr, method, receiver;
+      message = A.quoteStringForRegExp(message.replace(String({}), "$receiver$"));
+      match = message.match(/\\\$[a-zA-Z]+\\\$/g);
+      if (match == null)
+        match = A._setArrayType([], type$.JSArray_String);
+      $arguments = match.indexOf("\\$arguments\\$");
+      argumentsExpr = match.indexOf("\\$argumentsExpr\\$");
+      expr = match.indexOf("\\$expr\\$");
+      method = match.indexOf("\\$method\\$");
+      receiver = match.indexOf("\\$receiver\\$");
+      return new A.TypeErrorDecoder(message.replace(new RegExp("\\\\\\$arguments\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$argumentsExpr\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$expr\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$method\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$receiver\\\\\\$", "g"), "((?:x|[^x])*)"), $arguments, argumentsExpr, expr, method, receiver);
+    },
+    TypeErrorDecoder_provokeCallErrorOn(expression) {
+      return function($expr$) {
+        var $argumentsExpr$ = "$arguments$";
+        try {
+          $expr$.$method$($argumentsExpr$);
+        } catch (e) {
+          return e.message;
+        }
+      }(expression);
+    },
+    TypeErrorDecoder_provokePropertyErrorOn(expression) {
+      return function($expr$) {
+        try {
+          $expr$.$method$;
+        } catch (e) {
+          return e.message;
+        }
+      }(expression);
+    },
+    JsNoSuchMethodError$(_message, match) {
+      var t1 = match == null,
+        t2 = t1 ? null : match.method;
+      return new A.JsNoSuchMethodError(_message, t2, t1 ? null : match.receiver);
+    },
+    unwrapException(ex) {
+      var t1;
+      if (ex == null)
+        return new A.NullThrownFromJavaScriptException(ex);
+      if (ex instanceof A.ExceptionAndStackTrace) {
+        t1 = ex.dartException;
+        return A.saveStackTrace(ex, t1 == null ? type$.Object._as(t1) : t1);
+      }
+      if (typeof ex !== "object")
+        return ex;
+      if ("dartException" in ex)
+        return A.saveStackTrace(ex, ex.dartException);
+      return A._unwrapNonDartException(ex);
+    },
+    saveStackTrace(ex, error) {
+      if (type$.Error._is(error))
+        if (error.$thrownJsError == null)
+          error.$thrownJsError = ex;
+      return error;
+    },
+    _unwrapNonDartException(ex) {
+      var message, number, ieErrorCode, nsme, notClosure, nullCall, nullLiteralCall, undefCall, undefLiteralCall, nullProperty, undefProperty, undefLiteralProperty, match;
+      if (!("message" in ex))
+        return ex;
+      message = ex.message;
+      if ("number" in ex && typeof ex.number == "number") {
+        number = ex.number;
+        ieErrorCode = number & 65535;
+        if ((B.JSInt_methods._shrOtherPositive$1(number, 16) & 8191) === 10)
+          switch (ieErrorCode) {
+            case 438:
+              return A.saveStackTrace(ex, A.JsNoSuchMethodError$(A.S(message) + " (Error " + ieErrorCode + ")", null));
+            case 445:
+            case 5007:
+              A.S(message);
+              return A.saveStackTrace(ex, new A.NullError());
+          }
+      }
+      if (ex instanceof TypeError) {
+        nsme = $.$get$TypeErrorDecoder_noSuchMethodPattern();
+        notClosure = $.$get$TypeErrorDecoder_notClosurePattern();
+        nullCall = $.$get$TypeErrorDecoder_nullCallPattern();
+        nullLiteralCall = $.$get$TypeErrorDecoder_nullLiteralCallPattern();
+        undefCall = $.$get$TypeErrorDecoder_undefinedCallPattern();
+        undefLiteralCall = $.$get$TypeErrorDecoder_undefinedLiteralCallPattern();
+        nullProperty = $.$get$TypeErrorDecoder_nullPropertyPattern();
+        $.$get$TypeErrorDecoder_nullLiteralPropertyPattern();
+        undefProperty = $.$get$TypeErrorDecoder_undefinedPropertyPattern();
+        undefLiteralProperty = $.$get$TypeErrorDecoder_undefinedLiteralPropertyPattern();
+        match = nsme.matchTypeError$1(message);
+        if (match != null)
+          return A.saveStackTrace(ex, A.JsNoSuchMethodError$(A._asString(message), match));
+        else {
+          match = notClosure.matchTypeError$1(message);
+          if (match != null) {
+            match.method = "call";
+            return A.saveStackTrace(ex, A.JsNoSuchMethodError$(A._asString(message), match));
+          } else if (nullCall.matchTypeError$1(message) != null || nullLiteralCall.matchTypeError$1(message) != null || undefCall.matchTypeError$1(message) != null || undefLiteralCall.matchTypeError$1(message) != null || nullProperty.matchTypeError$1(message) != null || nullLiteralCall.matchTypeError$1(message) != null || undefProperty.matchTypeError$1(message) != null || undefLiteralProperty.matchTypeError$1(message) != null) {
+            A._asString(message);
+            return A.saveStackTrace(ex, new A.NullError());
+          }
+        }
+        return A.saveStackTrace(ex, new A.UnknownJsTypeError(typeof message == "string" ? message : ""));
+      }
+      if (ex instanceof RangeError) {
+        if (typeof message == "string" && message.indexOf("call stack") !== -1)
+          return new A.StackOverflowError();
+        message = function(ex) {
+          try {
+            return String(ex);
+          } catch (e) {
+          }
+          return null;
+        }(ex);
+        return A.saveStackTrace(ex, new A.ArgumentError(false, null, null, typeof message == "string" ? message.replace(/^RangeError:\s*/, "") : message));
+      }
+      if (typeof InternalError == "function" && ex instanceof InternalError)
+        if (typeof message == "string" && message === "too much recursion")
+          return new A.StackOverflowError();
+      return ex;
+    },
+    getTraceFromException(exception) {
+      var trace;
+      if (exception instanceof A.ExceptionAndStackTrace)
+        return exception.stackTrace;
+      if (exception == null)
+        return new A._StackTrace(exception);
+      trace = exception.$cachedTrace;
+      if (trace != null)
+        return trace;
+      trace = new A._StackTrace(exception);
+      if (typeof exception === "object")
+        exception.$cachedTrace = trace;
+      return trace;
+    },
+    objectHashCode(object) {
+      if (object == null)
+        return J.get$hashCode$(object);
+      if (typeof object == "object")
+        return A.Primitives_objectHashCode(object);
+      return J.get$hashCode$(object);
+    },
+    _invokeClosure(closure, numberOfArguments, arg1, arg2, arg3, arg4) {
+      type$.Function._as(closure);
+      switch (A._asInt(numberOfArguments)) {
+        case 0:
+          return closure.call$0();
+        case 1:
+          return closure.call$1(arg1);
+        case 2:
+          return closure.call$2(arg1, arg2);
+        case 3:
+          return closure.call$3(arg1, arg2, arg3);
+        case 4:
+          return closure.call$4(arg1, arg2, arg3, arg4);
+      }
+      throw A.wrapException(new A._Exception("Unsupported number of arguments for wrapped closure"));
+    },
+    convertDartClosureToJS(closure, arity) {
+      var $function = closure.$identity;
+      if (!!$function)
+        return $function;
+      $function = A.convertDartClosureToJSUncached(closure, arity);
+      closure.$identity = $function;
+      return $function;
+    },
+    convertDartClosureToJSUncached(closure, arity) {
+      var entry;
+      switch (arity) {
+        case 0:
+          entry = closure.call$0;
+          break;
+        case 1:
+          entry = closure.call$1;
+          break;
+        case 2:
+          entry = closure.call$2;
+          break;
+        case 3:
+          entry = closure.call$3;
+          break;
+        case 4:
+          entry = closure.call$4;
+          break;
+        default:
+          entry = null;
+      }
+      if (entry != null)
+        return entry.bind(closure);
+      return function(closure, arity, invoke) {
+        return function(a1, a2, a3, a4) {
+          return invoke(closure, arity, a1, a2, a3, a4);
+        };
+      }(closure, arity, A._invokeClosure);
+    },
+    Closure_fromTearOff(parameters) {
+      var $prototype, $constructor, t2, trampoline, applyTrampoline, i, stub, stub0, stubName, stubCallName,
+        container = parameters.co,
+        isStatic = parameters.iS,
+        isIntercepted = parameters.iI,
+        needsDirectAccess = parameters.nDA,
+        applyTrampolineIndex = parameters.aI,
+        funsOrNames = parameters.fs,
+        callNames = parameters.cs,
+        $name = funsOrNames[0],
+        callName = callNames[0],
+        $function = container[$name],
+        t1 = parameters.fT;
+      t1.toString;
+      $prototype = isStatic ? Object.create(new A.StaticClosure().constructor.prototype) : Object.create(new A.BoundClosure(null, null).constructor.prototype);
+      $prototype.$initialize = $prototype.constructor;
+      $constructor = isStatic ? function static_tear_off() {
+        this.$initialize();
+      } : function tear_off(a, b) {
+        this.$initialize(a, b);
+      };
+      $prototype.constructor = $constructor;
+      $constructor.prototype = $prototype;
+      $prototype.$_name = $name;
+      $prototype.$_target = $function;
+      t2 = !isStatic;
+      if (t2)
+        trampoline = A.Closure_forwardCallTo($name, $function, isIntercepted, needsDirectAccess);
+      else {
+        $prototype.$static_name = $name;
+        trampoline = $function;
+      }
+      $prototype.$signature = A.Closure__computeSignatureFunctionNewRti(t1, isStatic, isIntercepted);
+      $prototype[callName] = trampoline;
+      for (applyTrampoline = trampoline, i = 1; i < funsOrNames.length; ++i) {
+        stub = funsOrNames[i];
+        if (typeof stub == "string") {
+          stub0 = container[stub];
+          stubName = stub;
+          stub = stub0;
+        } else
+          stubName = "";
+        stubCallName = callNames[i];
+        if (stubCallName != null) {
+          if (t2)
+            stub = A.Closure_forwardCallTo(stubName, stub, isIntercepted, needsDirectAccess);
+          $prototype[stubCallName] = stub;
+        }
+        if (i === applyTrampolineIndex)
+          applyTrampoline = stub;
+      }
+      $prototype["call*"] = applyTrampoline;
+      $prototype.$requiredArgCount = parameters.rC;
+      $prototype.$defaultValues = parameters.dV;
+      return $constructor;
+    },
+    Closure__computeSignatureFunctionNewRti(functionType, isStatic, isIntercepted) {
+      if (typeof functionType == "number")
+        return functionType;
+      if (typeof functionType == "string") {
+        if (isStatic)
+          throw A.wrapException("Cannot compute signature for static tearoff.");
+        return function(recipe, evalOnReceiver) {
+          return function() {
+            return evalOnReceiver(this, recipe);
+          };
+        }(functionType, A.BoundClosure_evalRecipe);
+      }
+      throw A.wrapException("Error in functionType of tearoff");
+    },
+    Closure_cspForwardCall(arity, needsDirectAccess, stubName, $function) {
+      var getReceiver = A.BoundClosure_receiverOf;
+      switch (needsDirectAccess ? -1 : arity) {
+        case 0:
+          return function(entry, receiverOf) {
+            return function() {
+              return receiverOf(this)[entry]();
+            };
+          }(stubName, getReceiver);
+        case 1:
+          return function(entry, receiverOf) {
+            return function(a) {
+              return receiverOf(this)[entry](a);
+            };
+          }(stubName, getReceiver);
+        case 2:
+          return function(entry, receiverOf) {
+            return function(a, b) {
+              return receiverOf(this)[entry](a, b);
+            };
+          }(stubName, getReceiver);
+        case 3:
+          return function(entry, receiverOf) {
+            return function(a, b, c) {
+              return receiverOf(this)[entry](a, b, c);
+            };
+          }(stubName, getReceiver);
+        case 4:
+          return function(entry, receiverOf) {
+            return function(a, b, c, d) {
+              return receiverOf(this)[entry](a, b, c, d);
+            };
+          }(stubName, getReceiver);
+        case 5:
+          return function(entry, receiverOf) {
+            return function(a, b, c, d, e) {
+              return receiverOf(this)[entry](a, b, c, d, e);
+            };
+          }(stubName, getReceiver);
+        default:
+          return function(f, receiverOf) {
+            return function() {
+              return f.apply(receiverOf(this), arguments);
+            };
+          }($function, getReceiver);
+      }
+    },
+    Closure_forwardCallTo(stubName, $function, isIntercepted, needsDirectAccess) {
+      if (isIntercepted)
+        return A.Closure_forwardInterceptedCallTo(stubName, $function, needsDirectAccess);
+      return A.Closure_cspForwardCall($function.length, needsDirectAccess, stubName, $function);
+    },
+    Closure_cspForwardInterceptedCall(arity, needsDirectAccess, stubName, $function) {
+      var getReceiver = A.BoundClosure_receiverOf,
+        getInterceptor = A.BoundClosure_interceptorOf;
+      switch (needsDirectAccess ? -1 : arity) {
+        case 0:
+          throw A.wrapException(new A.RuntimeError("Intercepted function with no arguments."));
+        case 1:
+          return function(entry, interceptorOf, receiverOf) {
+            return function() {
+              return interceptorOf(this)[entry](receiverOf(this));
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 2:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a) {
+              return interceptorOf(this)[entry](receiverOf(this), a);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 3:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a, b) {
+              return interceptorOf(this)[entry](receiverOf(this), a, b);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 4:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a, b, c) {
+              return interceptorOf(this)[entry](receiverOf(this), a, b, c);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 5:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a, b, c, d) {
+              return interceptorOf(this)[entry](receiverOf(this), a, b, c, d);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 6:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a, b, c, d, e) {
+              return interceptorOf(this)[entry](receiverOf(this), a, b, c, d, e);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        default:
+          return function(f, interceptorOf, receiverOf) {
+            return function() {
+              var a = [receiverOf(this)];
+              Array.prototype.push.apply(a, arguments);
+              return f.apply(interceptorOf(this), a);
+            };
+          }($function, getInterceptor, getReceiver);
+      }
+    },
+    Closure_forwardInterceptedCallTo(stubName, $function, needsDirectAccess) {
+      var arity, t1;
+      if ($.BoundClosure__interceptorFieldNameCache == null)
+        $.BoundClosure__interceptorFieldNameCache = A.BoundClosure__computeFieldNamed("interceptor");
+      if ($.BoundClosure__receiverFieldNameCache == null)
+        $.BoundClosure__receiverFieldNameCache = A.BoundClosure__computeFieldNamed("receiver");
+      arity = $function.length;
+      t1 = A.Closure_cspForwardInterceptedCall(arity, needsDirectAccess, stubName, $function);
+      return t1;
+    },
+    closureFromTearOff(parameters) {
+      return A.Closure_fromTearOff(parameters);
+    },
+    BoundClosure_evalRecipe(closure, recipe) {
+      return A._Universe_evalInEnvironment(init.typeUniverse, A.instanceType(closure._receiver), recipe);
+    },
+    BoundClosure_receiverOf(closure) {
+      return closure._receiver;
+    },
+    BoundClosure_interceptorOf(closure) {
+      return closure._interceptor;
+    },
+    BoundClosure__computeFieldNamed(fieldName) {
+      var t1, i, $name,
+        template = new A.BoundClosure("receiver", "interceptor"),
+        names = J.JSArray_markFixedList(Object.getOwnPropertyNames(template), type$.nullable_Object);
+      for (t1 = names.length, i = 0; i < t1; ++i) {
+        $name = names[i];
+        if (template[$name] === fieldName)
+          return $name;
+      }
+      throw A.wrapException(A.ArgumentError$("Field name " + fieldName + " not found.", null));
+    },
+    throwCyclicInit(staticName) {
+      throw A.wrapException(new A._CyclicInitializationError(staticName));
+    },
+    getIsolateAffinityTag($name) {
+      return init.getIsolateTag($name);
+    },
+    lookupAndCacheInterceptor(obj) {
+      var interceptor, interceptorClass, altTag, mark, t1,
+        tag = A._asString($.getTagFunction.call$1(obj)),
+        record = $.dispatchRecordsForInstanceTags[tag];
+      if (record != null) {
+        Object.defineProperty(obj, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true});
+        return record.i;
+      }
+      interceptor = $.interceptorsForUncacheableTags[tag];
+      if (interceptor != null)
+        return interceptor;
+      interceptorClass = init.interceptorsByTag[tag];
+      if (interceptorClass == null) {
+        altTag = A._asStringQ($.alternateTagFunction.call$2(obj, tag));
+        if (altTag != null) {
+          record = $.dispatchRecordsForInstanceTags[altTag];
+          if (record != null) {
+            Object.defineProperty(obj, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true});
+            return record.i;
+          }
+          interceptor = $.interceptorsForUncacheableTags[altTag];
+          if (interceptor != null)
+            return interceptor;
+          interceptorClass = init.interceptorsByTag[altTag];
+          tag = altTag;
+        }
+      }
+      if (interceptorClass == null)
+        return null;
+      interceptor = interceptorClass.prototype;
+      mark = tag[0];
+      if (mark === "!") {
+        record = A.makeLeafDispatchRecord(interceptor);
+        $.dispatchRecordsForInstanceTags[tag] = record;
+        Object.defineProperty(obj, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true});
+        return record.i;
+      }
+      if (mark === "~") {
+        $.interceptorsForUncacheableTags[tag] = interceptor;
+        return interceptor;
+      }
+      if (mark === "-") {
+        t1 = A.makeLeafDispatchRecord(interceptor);
+        Object.defineProperty(Object.getPrototypeOf(obj), init.dispatchPropertyName, {value: t1, enumerable: false, writable: true, configurable: true});
+        return t1.i;
+      }
+      if (mark === "+")
+        return A.patchInteriorProto(obj, interceptor);
+      if (mark === "*")
+        throw A.wrapException(A.UnimplementedError$(tag));
+      if (init.leafTags[tag] === true) {
+        t1 = A.makeLeafDispatchRecord(interceptor);
+        Object.defineProperty(Object.getPrototypeOf(obj), init.dispatchPropertyName, {value: t1, enumerable: false, writable: true, configurable: true});
+        return t1.i;
+      } else
+        return A.patchInteriorProto(obj, interceptor);
+    },
+    patchInteriorProto(obj, interceptor) {
+      var proto = Object.getPrototypeOf(obj);
+      Object.defineProperty(proto, init.dispatchPropertyName, {value: J.makeDispatchRecord(interceptor, proto, null, null), enumerable: false, writable: true, configurable: true});
+      return interceptor;
+    },
+    makeLeafDispatchRecord(interceptor) {
+      return J.makeDispatchRecord(interceptor, false, null, !!interceptor.$isJavaScriptIndexingBehavior);
+    },
+    makeDefaultDispatchRecord(tag, interceptorClass, proto) {
+      var interceptor = interceptorClass.prototype;
+      if (init.leafTags[tag] === true)
+        return A.makeLeafDispatchRecord(interceptor);
+      else
+        return J.makeDispatchRecord(interceptor, proto, null, null);
+    },
+    initNativeDispatch() {
+      if (true === $.initNativeDispatchFlag)
+        return;
+      $.initNativeDispatchFlag = true;
+      A.initNativeDispatchContinue();
+    },
+    initNativeDispatchContinue() {
+      var map, tags, fun, i, tag, proto, record, interceptorClass;
+      $.dispatchRecordsForInstanceTags = Object.create(null);
+      $.interceptorsForUncacheableTags = Object.create(null);
+      A.initHooks();
+      map = init.interceptorsByTag;
+      tags = Object.getOwnPropertyNames(map);
+      if (typeof window != "undefined") {
+        window;
+        fun = function() {
+        };
+        for (i = 0; i < tags.length; ++i) {
+          tag = tags[i];
+          proto = $.prototypeForTagFunction.call$1(tag);
+          if (proto != null) {
+            record = A.makeDefaultDispatchRecord(tag, map[tag], proto);
+            if (record != null) {
+              Object.defineProperty(proto, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true});
+              fun.prototype = proto;
+            }
+          }
+        }
+      }
+      for (i = 0; i < tags.length; ++i) {
+        tag = tags[i];
+        if (/^[A-Za-z_]/.test(tag)) {
+          interceptorClass = map[tag];
+          map["!" + tag] = interceptorClass;
+          map["~" + tag] = interceptorClass;
+          map["-" + tag] = interceptorClass;
+          map["+" + tag] = interceptorClass;
+          map["*" + tag] = interceptorClass;
+        }
+      }
+    },
+    initHooks() {
+      var transformers, i, transformer, getTag, getUnknownTag, prototypeForTag,
+        hooks = B.C_JS_CONST0();
+      hooks = A.applyHooksTransformer(B.C_JS_CONST1, A.applyHooksTransformer(B.C_JS_CONST2, A.applyHooksTransformer(B.C_JS_CONST3, A.applyHooksTransformer(B.C_JS_CONST3, A.applyHooksTransformer(B.C_JS_CONST4, A.applyHooksTransformer(B.C_JS_CONST5, A.applyHooksTransformer(B.C_JS_CONST6(B.C_JS_CONST), hooks)))))));
+      if (typeof dartNativeDispatchHooksTransformer != "undefined") {
+        transformers = dartNativeDispatchHooksTransformer;
+        if (typeof transformers == "function")
+          transformers = [transformers];
+        if (Array.isArray(transformers))
+          for (i = 0; i < transformers.length; ++i) {
+            transformer = transformers[i];
+            if (typeof transformer == "function")
+              hooks = transformer(hooks) || hooks;
+          }
+      }
+      getTag = hooks.getTag;
+      getUnknownTag = hooks.getUnknownTag;
+      prototypeForTag = hooks.prototypeForTag;
+      $.getTagFunction = new A.initHooks_closure(getTag);
+      $.alternateTagFunction = new A.initHooks_closure0(getUnknownTag);
+      $.prototypeForTagFunction = new A.initHooks_closure1(prototypeForTag);
+    },
+    applyHooksTransformer(transformer, hooks) {
+      return transformer(hooks) || hooks;
+    },
+    createRecordTypePredicate(shape, fieldRtis) {
+      var $length = fieldRtis.length,
+        $function = init.rttc["" + $length + ";" + shape];
+      if ($function == null)
+        return null;
+      if ($length === 0)
+        return $function;
+      if ($length === $function.length)
+        return $function.apply(null, fieldRtis);
+      return $function(fieldRtis);
+    },
+    quoteStringForRegExp(string) {
+      if (/[[\]{}()*+?.\\^$|]/.test(string))
+        return string.replace(/[[\]{}()*+?.\\^$|]/g, "\\$&");
+      return string;
+    },
+    ConstantMapView: function ConstantMapView(t0, t1) {
+      this._collection$_map = t0;
+      this.$ti = t1;
+    },
+    ConstantMap: function ConstantMap() {
+    },
+    ConstantStringMap: function ConstantStringMap(t0, t1, t2) {
+      this._jsIndex = t0;
+      this._values = t1;
+      this.$ti = t2;
+    },
+    JSInvocationMirror: function JSInvocationMirror(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._memberName = t0;
+      _.__js_helper$_kind = t1;
+      _._arguments = t2;
+      _._namedArgumentNames = t3;
+      _._typeArgumentCount = t4;
+    },
+    Primitives_functionNoSuchMethod_closure: function Primitives_functionNoSuchMethod_closure(t0, t1, t2) {
+      this._box_0 = t0;
+      this.namedArgumentList = t1;
+      this.$arguments = t2;
+    },
+    TypeErrorDecoder: function TypeErrorDecoder(t0, t1, t2, t3, t4, t5) {
+      var _ = this;
+      _._pattern = t0;
+      _._arguments = t1;
+      _._argumentsExpr = t2;
+      _._expr = t3;
+      _._method = t4;
+      _._receiver = t5;
+    },
+    NullError: function NullError() {
+    },
+    JsNoSuchMethodError: function JsNoSuchMethodError(t0, t1, t2) {
+      this.__js_helper$_message = t0;
+      this._method = t1;
+      this._receiver = t2;
+    },
+    UnknownJsTypeError: function UnknownJsTypeError(t0) {
+      this.__js_helper$_message = t0;
+    },
+    NullThrownFromJavaScriptException: function NullThrownFromJavaScriptException(t0) {
+      this._irritant = t0;
+    },
+    ExceptionAndStackTrace: function ExceptionAndStackTrace(t0, t1) {
+      this.dartException = t0;
+      this.stackTrace = t1;
+    },
+    _StackTrace: function _StackTrace(t0) {
+      this._exception = t0;
+      this._trace = null;
+    },
+    Closure: function Closure() {
+    },
+    Closure0Args: function Closure0Args() {
+    },
+    Closure2Args: function Closure2Args() {
+    },
+    TearOffClosure: function TearOffClosure() {
+    },
+    StaticClosure: function StaticClosure() {
+    },
+    BoundClosure: function BoundClosure(t0, t1) {
+      this._receiver = t0;
+      this._interceptor = t1;
+    },
+    _CyclicInitializationError: function _CyclicInitializationError(t0) {
+      this.variableName = t0;
+    },
+    RuntimeError: function RuntimeError(t0) {
+      this.message = t0;
+    },
+    _Required: function _Required() {
+    },
+    JsLinkedHashMap: function JsLinkedHashMap(t0) {
+      var _ = this;
+      _.__js_helper$_length = 0;
+      _._last = _._first = _.__js_helper$_rest = _._nums = _._strings = null;
+      _._modifications = 0;
+      _.$ti = t0;
+    },
+    LinkedHashMapCell: function LinkedHashMapCell(t0, t1) {
+      this.hashMapCellKey = t0;
+      this.hashMapCellValue = t1;
+      this._next = null;
+    },
+    LinkedHashMapKeyIterable: function LinkedHashMapKeyIterable(t0, t1) {
+      this._map = t0;
+      this.$ti = t1;
+    },
+    LinkedHashMapKeyIterator: function LinkedHashMapKeyIterator(t0, t1, t2) {
+      var _ = this;
+      _._map = t0;
+      _._modifications = t1;
+      _.__js_helper$_current = _._cell = null;
+      _.$ti = t2;
+    },
+    initHooks_closure: function initHooks_closure(t0) {
+      this.getTag = t0;
+    },
+    initHooks_closure0: function initHooks_closure0(t0) {
+      this.getUnknownTag = t0;
+    },
+    initHooks_closure1: function initHooks_closure1(t0) {
+      this.prototypeForTag = t0;
+    },
+    StringMatch: function StringMatch(t0, t1) {
+      this.start = t0;
+      this.pattern = t1;
+    },
+    _checkValidIndex(index, list, $length) {
+      if (index >>> 0 !== index || index >= $length)
+        throw A.wrapException(A.diagnoseIndexError(list, index));
+    },
+    NativeByteBuffer: function NativeByteBuffer() {
+    },
+    NativeTypedData: function NativeTypedData() {
+    },
+    NativeByteData: function NativeByteData() {
+    },
+    NativeTypedArray: function NativeTypedArray() {
+    },
+    NativeTypedArrayOfDouble: function NativeTypedArrayOfDouble() {
+    },
+    NativeTypedArrayOfInt: function NativeTypedArrayOfInt() {
+    },
+    NativeFloat32List: function NativeFloat32List() {
+    },
+    NativeFloat64List: function NativeFloat64List() {
+    },
+    NativeInt16List: function NativeInt16List() {
+    },
+    NativeInt32List: function NativeInt32List() {
+    },
+    NativeInt8List: function NativeInt8List() {
+    },
+    NativeUint16List: function NativeUint16List() {
+    },
+    NativeUint32List: function NativeUint32List() {
+    },
+    NativeUint8ClampedList: function NativeUint8ClampedList() {
+    },
+    NativeUint8List: function NativeUint8List() {
+    },
+    _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin: function _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin() {
+    },
+    _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin: function _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin() {
+    },
+    _NativeTypedArrayOfInt_NativeTypedArray_ListMixin: function _NativeTypedArrayOfInt_NativeTypedArray_ListMixin() {
+    },
+    _NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin: function _NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin() {
+    },
+    Rti__getQuestionFromStar(universe, rti) {
+      var question = rti._precomputed1;
+      return question == null ? rti._precomputed1 = A._Universe__lookupQuestionRti(universe, rti._primary, true) : question;
+    },
+    Rti__getFutureFromFutureOr(universe, rti) {
+      var future = rti._precomputed1;
+      return future == null ? rti._precomputed1 = A._Universe__lookupInterfaceRti(universe, "Future", [rti._primary]) : future;
+    },
+    Rti__isUnionOfFunctionType(rti) {
+      var kind = rti._kind;
+      if (kind === 6 || kind === 7 || kind === 8)
+        return A.Rti__isUnionOfFunctionType(rti._primary);
+      return kind === 12 || kind === 13;
+    },
+    Rti__getCanonicalRecipe(rti) {
+      return rti._canonicalRecipe;
+    },
+    findType(recipe) {
+      return A._Universe_eval(init.typeUniverse, recipe, false);
+    },
+    _substitute(universe, rti, typeArguments, depth) {
+      var baseType, substitutedBaseType, interfaceTypeArguments, substitutedInterfaceTypeArguments, base, substitutedBase, $arguments, substitutedArguments, t1, fields, substitutedFields, returnType, substitutedReturnType, functionParameters, substitutedFunctionParameters, bounds, substitutedBounds, index, argument,
+        kind = rti._kind;
+      switch (kind) {
+        case 5:
+        case 1:
+        case 2:
+        case 3:
+        case 4:
+          return rti;
+        case 6:
+          baseType = rti._primary;
+          substitutedBaseType = A._substitute(universe, baseType, typeArguments, depth);
+          if (substitutedBaseType === baseType)
+            return rti;
+          return A._Universe__lookupStarRti(universe, substitutedBaseType, true);
+        case 7:
+          baseType = rti._primary;
+          substitutedBaseType = A._substitute(universe, baseType, typeArguments, depth);
+          if (substitutedBaseType === baseType)
+            return rti;
+          return A._Universe__lookupQuestionRti(universe, substitutedBaseType, true);
+        case 8:
+          baseType = rti._primary;
+          substitutedBaseType = A._substitute(universe, baseType, typeArguments, depth);
+          if (substitutedBaseType === baseType)
+            return rti;
+          return A._Universe__lookupFutureOrRti(universe, substitutedBaseType, true);
+        case 9:
+          interfaceTypeArguments = rti._rest;
+          substitutedInterfaceTypeArguments = A._substituteArray(universe, interfaceTypeArguments, typeArguments, depth);
+          if (substitutedInterfaceTypeArguments === interfaceTypeArguments)
+            return rti;
+          return A._Universe__lookupInterfaceRti(universe, rti._primary, substitutedInterfaceTypeArguments);
+        case 10:
+          base = rti._primary;
+          substitutedBase = A._substitute(universe, base, typeArguments, depth);
+          $arguments = rti._rest;
+          substitutedArguments = A._substituteArray(universe, $arguments, typeArguments, depth);
+          if (substitutedBase === base && substitutedArguments === $arguments)
+            return rti;
+          return A._Universe__lookupBindingRti(universe, substitutedBase, substitutedArguments);
+        case 11:
+          t1 = rti._primary;
+          fields = rti._rest;
+          substitutedFields = A._substituteArray(universe, fields, typeArguments, depth);
+          if (substitutedFields === fields)
+            return rti;
+          return A._Universe__lookupRecordRti(universe, t1, substitutedFields);
+        case 12:
+          returnType = rti._primary;
+          substitutedReturnType = A._substitute(universe, returnType, typeArguments, depth);
+          functionParameters = rti._rest;
+          substitutedFunctionParameters = A._substituteFunctionParameters(universe, functionParameters, typeArguments, depth);
+          if (substitutedReturnType === returnType && substitutedFunctionParameters === functionParameters)
+            return rti;
+          return A._Universe__lookupFunctionRti(universe, substitutedReturnType, substitutedFunctionParameters);
+        case 13:
+          bounds = rti._rest;
+          depth += bounds.length;
+          substitutedBounds = A._substituteArray(universe, bounds, typeArguments, depth);
+          base = rti._primary;
+          substitutedBase = A._substitute(universe, base, typeArguments, depth);
+          if (substitutedBounds === bounds && substitutedBase === base)
+            return rti;
+          return A._Universe__lookupGenericFunctionRti(universe, substitutedBase, substitutedBounds, true);
+        case 14:
+          index = rti._primary;
+          if (index < depth)
+            return rti;
+          argument = typeArguments[index - depth];
+          if (argument == null)
+            return rti;
+          return argument;
+        default:
+          throw A.wrapException(A.AssertionError$("Attempted to substitute unexpected RTI kind " + kind));
+      }
+    },
+    _substituteArray(universe, rtiArray, typeArguments, depth) {
+      var changed, i, rti, substitutedRti,
+        $length = rtiArray.length,
+        result = A._Utils_newArrayOrEmpty($length);
+      for (changed = false, i = 0; i < $length; ++i) {
+        rti = rtiArray[i];
+        substitutedRti = A._substitute(universe, rti, typeArguments, depth);
+        if (substitutedRti !== rti)
+          changed = true;
+        result[i] = substitutedRti;
+      }
+      return changed ? result : rtiArray;
+    },
+    _substituteNamed(universe, namedArray, typeArguments, depth) {
+      var changed, i, t1, t2, rti, substitutedRti,
+        $length = namedArray.length,
+        result = A._Utils_newArrayOrEmpty($length);
+      for (changed = false, i = 0; i < $length; i += 3) {
+        t1 = namedArray[i];
+        t2 = namedArray[i + 1];
+        rti = namedArray[i + 2];
+        substitutedRti = A._substitute(universe, rti, typeArguments, depth);
+        if (substitutedRti !== rti)
+          changed = true;
+        result.splice(i, 3, t1, t2, substitutedRti);
+      }
+      return changed ? result : namedArray;
+    },
+    _substituteFunctionParameters(universe, functionParameters, typeArguments, depth) {
+      var result,
+        requiredPositional = functionParameters._requiredPositional,
+        substitutedRequiredPositional = A._substituteArray(universe, requiredPositional, typeArguments, depth),
+        optionalPositional = functionParameters._optionalPositional,
+        substitutedOptionalPositional = A._substituteArray(universe, optionalPositional, typeArguments, depth),
+        named = functionParameters._named,
+        substitutedNamed = A._substituteNamed(universe, named, typeArguments, depth);
+      if (substitutedRequiredPositional === requiredPositional && substitutedOptionalPositional === optionalPositional && substitutedNamed === named)
+        return functionParameters;
+      result = new A._FunctionParameters();
+      result._requiredPositional = substitutedRequiredPositional;
+      result._optionalPositional = substitutedOptionalPositional;
+      result._named = substitutedNamed;
+      return result;
+    },
+    _setArrayType(target, rti) {
+      target[init.arrayRti] = rti;
+      return target;
+    },
+    closureFunctionType(closure) {
+      var signature = closure.$signature;
+      if (signature != null) {
+        if (typeof signature == "number")
+          return A.getTypeFromTypesTable(signature);
+        return closure.$signature();
+      }
+      return null;
+    },
+    instanceOrFunctionType(object, testRti) {
+      var rti;
+      if (A.Rti__isUnionOfFunctionType(testRti))
+        if (object instanceof A.Closure) {
+          rti = A.closureFunctionType(object);
+          if (rti != null)
+            return rti;
+        }
+      return A.instanceType(object);
+    },
+    instanceType(object) {
+      if (object instanceof A.Object)
+        return A._instanceType(object);
+      if (Array.isArray(object))
+        return A._arrayInstanceType(object);
+      return A._instanceTypeFromConstructor(J.getInterceptor$(object));
+    },
+    _arrayInstanceType(object) {
+      var rti = object[init.arrayRti],
+        defaultRti = type$.JSArray_dynamic;
+      if (rti == null)
+        return defaultRti;
+      if (rti.constructor !== defaultRti.constructor)
+        return defaultRti;
+      return rti;
+    },
+    _instanceType(object) {
+      var rti = object.$ti;
+      return rti != null ? rti : A._instanceTypeFromConstructor(object);
+    },
+    _instanceTypeFromConstructor(instance) {
+      var $constructor = instance.constructor,
+        probe = $constructor.$ccache;
+      if (probe != null)
+        return probe;
+      return A._instanceTypeFromConstructorMiss(instance, $constructor);
+    },
+    _instanceTypeFromConstructorMiss(instance, $constructor) {
+      var effectiveConstructor = instance instanceof A.Closure ? Object.getPrototypeOf(Object.getPrototypeOf(instance)).constructor : $constructor,
+        rti = A._Universe_findErasedType(init.typeUniverse, effectiveConstructor.name);
+      $constructor.$ccache = rti;
+      return rti;
+    },
+    getTypeFromTypesTable(index) {
+      var rti,
+        table = init.types,
+        type = table[index];
+      if (typeof type == "string") {
+        rti = A._Universe_eval(init.typeUniverse, type, false);
+        table[index] = rti;
+        return rti;
+      }
+      return type;
+    },
+    getRuntimeTypeOfDartObject(object) {
+      return A.createRuntimeType(A._instanceType(object));
+    },
+    _structuralTypeOf(object) {
+      var functionRti = object instanceof A.Closure ? A.closureFunctionType(object) : null;
+      if (functionRti != null)
+        return functionRti;
+      if (type$.TrustedGetRuntimeType._is(object))
+        return J.get$runtimeType$(object)._rti;
+      if (Array.isArray(object))
+        return A._arrayInstanceType(object);
+      return A.instanceType(object);
+    },
+    createRuntimeType(rti) {
+      var t1 = rti._cachedRuntimeType;
+      return t1 == null ? rti._cachedRuntimeType = A._createRuntimeType(rti) : t1;
+    },
+    _createRuntimeType(rti) {
+      var starErasedRti, t1,
+        s = rti._canonicalRecipe,
+        starErasedRecipe = s.replace(/\*/g, "");
+      if (starErasedRecipe === s)
+        return rti._cachedRuntimeType = new A._Type(rti);
+      starErasedRti = A._Universe_eval(init.typeUniverse, starErasedRecipe, true);
+      t1 = starErasedRti._cachedRuntimeType;
+      return t1 == null ? starErasedRti._cachedRuntimeType = A._createRuntimeType(starErasedRti) : t1;
+    },
+    typeLiteral(recipe) {
+      return A.createRuntimeType(A._Universe_eval(init.typeUniverse, recipe, false));
+    },
+    _installSpecializedIsTest(object) {
+      var t1, unstarred, unstarredKind, isFn, $name, predicate, testRti = this;
+      if (testRti === type$.Object)
+        return A._finishIsFn(testRti, object, A._isObject);
+      if (!A.isSoundTopType(testRti))
+        t1 = testRti === type$.legacy_Object;
+      else
+        t1 = true;
+      if (t1)
+        return A._finishIsFn(testRti, object, A._isTop);
+      t1 = testRti._kind;
+      if (t1 === 7)
+        return A._finishIsFn(testRti, object, A._generalNullableIsTestImplementation);
+      if (t1 === 1)
+        return A._finishIsFn(testRti, object, A._isNever);
+      unstarred = t1 === 6 ? testRti._primary : testRti;
+      unstarredKind = unstarred._kind;
+      if (unstarredKind === 8)
+        return A._finishIsFn(testRti, object, A._isFutureOr);
+      if (unstarred === type$.int)
+        isFn = A._isInt;
+      else if (unstarred === type$.double || unstarred === type$.num)
+        isFn = A._isNum;
+      else if (unstarred === type$.String)
+        isFn = A._isString;
+      else
+        isFn = unstarred === type$.bool ? A._isBool : null;
+      if (isFn != null)
+        return A._finishIsFn(testRti, object, isFn);
+      if (unstarredKind === 9) {
+        $name = unstarred._primary;
+        if (unstarred._rest.every(A.isDefinitelyTopType)) {
+          testRti._specializedTestResource = "$is" + $name;
+          if ($name === "List")
+            return A._finishIsFn(testRti, object, A._isListTestViaProperty);
+          return A._finishIsFn(testRti, object, A._isTestViaProperty);
+        }
+      } else if (unstarredKind === 11) {
+        predicate = A.createRecordTypePredicate(unstarred._primary, unstarred._rest);
+        return A._finishIsFn(testRti, object, predicate == null ? A._isNever : predicate);
+      }
+      return A._finishIsFn(testRti, object, A._generalIsTestImplementation);
+    },
+    _finishIsFn(testRti, object, isFn) {
+      testRti._is = isFn;
+      return testRti._is(object);
+    },
+    _installSpecializedAsCheck(object) {
+      var t1, testRti = this,
+        asFn = A._generalAsCheckImplementation;
+      if (!A.isSoundTopType(testRti))
+        t1 = testRti === type$.legacy_Object;
+      else
+        t1 = true;
+      if (t1)
+        asFn = A._asTop;
+      else if (testRti === type$.Object)
+        asFn = A._asObject;
+      else {
+        t1 = A.isNullable(testRti);
+        if (t1)
+          asFn = A._generalNullableAsCheckImplementation;
+      }
+      testRti._as = asFn;
+      return testRti._as(object);
+    },
+    _nullIs(testRti) {
+      var t1,
+        kind = testRti._kind;
+      if (!A.isSoundTopType(testRti))
+        if (!(testRti === type$.legacy_Object))
+          if (!(testRti === type$.legacy_Never))
+            if (kind !== 7)
+              if (!(kind === 6 && A._nullIs(testRti._primary)))
+                t1 = kind === 8 && A._nullIs(testRti._primary) || testRti === type$.Null || testRti === type$.JSNull;
+              else
+                t1 = true;
+            else
+              t1 = true;
+          else
+            t1 = true;
+        else
+          t1 = true;
+      else
+        t1 = true;
+      return t1;
+    },
+    _generalIsTestImplementation(object) {
+      var testRti = this;
+      if (object == null)
+        return A._nullIs(testRti);
+      return A.isSubtype(init.typeUniverse, A.instanceOrFunctionType(object, testRti), testRti);
+    },
+    _generalNullableIsTestImplementation(object) {
+      if (object == null)
+        return true;
+      return this._primary._is(object);
+    },
+    _isTestViaProperty(object) {
+      var tag, testRti = this;
+      if (object == null)
+        return A._nullIs(testRti);
+      tag = testRti._specializedTestResource;
+      if (object instanceof A.Object)
+        return !!object[tag];
+      return !!J.getInterceptor$(object)[tag];
+    },
+    _isListTestViaProperty(object) {
+      var tag, testRti = this;
+      if (object == null)
+        return A._nullIs(testRti);
+      if (typeof object != "object")
+        return false;
+      if (Array.isArray(object))
+        return true;
+      tag = testRti._specializedTestResource;
+      if (object instanceof A.Object)
+        return !!object[tag];
+      return !!J.getInterceptor$(object)[tag];
+    },
+    _generalAsCheckImplementation(object) {
+      var testRti = this;
+      if (object == null) {
+        if (A.isNullable(testRti))
+          return object;
+      } else if (testRti._is(object))
+        return object;
+      A._failedAsCheck(object, testRti);
+    },
+    _generalNullableAsCheckImplementation(object) {
+      var testRti = this;
+      if (object == null)
+        return object;
+      else if (testRti._is(object))
+        return object;
+      A._failedAsCheck(object, testRti);
+    },
+    _failedAsCheck(object, testRti) {
+      throw A.wrapException(A._TypeError$fromMessage(A._Error_compose(object, A._rtiToString(testRti, null))));
+    },
+    _Error_compose(object, checkedTypeDescription) {
+      return A.Error_safeToString(object) + ": type '" + A._rtiToString(A._structuralTypeOf(object), null) + "' is not a subtype of type '" + checkedTypeDescription + "'";
+    },
+    _TypeError$fromMessage(message) {
+      return new A._TypeError("TypeError: " + message);
+    },
+    _TypeError__TypeError$forType(object, type) {
+      return new A._TypeError("TypeError: " + A._Error_compose(object, type));
+    },
+    _isFutureOr(object) {
+      var testRti = this,
+        unstarred = testRti._kind === 6 ? testRti._primary : testRti;
+      return unstarred._primary._is(object) || A.Rti__getFutureFromFutureOr(init.typeUniverse, unstarred)._is(object);
+    },
+    _isObject(object) {
+      return object != null;
+    },
+    _asObject(object) {
+      if (object != null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "Object"));
+    },
+    _isTop(object) {
+      return true;
+    },
+    _asTop(object) {
+      return object;
+    },
+    _isNever(object) {
+      return false;
+    },
+    _isBool(object) {
+      return true === object || false === object;
+    },
+    _asBool(object) {
+      if (true === object)
+        return true;
+      if (false === object)
+        return false;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "bool"));
+    },
+    _asBoolS(object) {
+      if (true === object)
+        return true;
+      if (false === object)
+        return false;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "bool"));
+    },
+    _asBoolQ(object) {
+      if (true === object)
+        return true;
+      if (false === object)
+        return false;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "bool?"));
+    },
+    _asDouble(object) {
+      if (typeof object == "number")
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "double"));
+    },
+    _asDoubleS(object) {
+      if (typeof object == "number")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "double"));
+    },
+    _asDoubleQ(object) {
+      if (typeof object == "number")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "double?"));
+    },
+    _isInt(object) {
+      return typeof object == "number" && Math.floor(object) === object;
+    },
+    _asInt(object) {
+      if (typeof object == "number" && Math.floor(object) === object)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "int"));
+    },
+    _asIntS(object) {
+      if (typeof object == "number" && Math.floor(object) === object)
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "int"));
+    },
+    _asIntQ(object) {
+      if (typeof object == "number" && Math.floor(object) === object)
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "int?"));
+    },
+    _isNum(object) {
+      return typeof object == "number";
+    },
+    _asNum(object) {
+      if (typeof object == "number")
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "num"));
+    },
+    _asNumS(object) {
+      if (typeof object == "number")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "num"));
+    },
+    _asNumQ(object) {
+      if (typeof object == "number")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "num?"));
+    },
+    _isString(object) {
+      return typeof object == "string";
+    },
+    _asString(object) {
+      if (typeof object == "string")
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "String"));
+    },
+    _asStringS(object) {
+      if (typeof object == "string")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "String"));
+    },
+    _asStringQ(object) {
+      if (typeof object == "string")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "String?"));
+    },
+    _rtiArrayToString(array, genericContext) {
+      var s, sep, i;
+      for (s = "", sep = "", i = 0; i < array.length; ++i, sep = ", ")
+        s += sep + A._rtiToString(array[i], genericContext);
+      return s;
+    },
+    _recordRtiToString(recordType, genericContext) {
+      var fieldCount, names, namesIndex, s, comma, i,
+        partialShape = recordType._primary,
+        fields = recordType._rest;
+      if ("" === partialShape)
+        return "(" + A._rtiArrayToString(fields, genericContext) + ")";
+      fieldCount = fields.length;
+      names = partialShape.split(",");
+      namesIndex = names.length - fieldCount;
+      for (s = "(", comma = "", i = 0; i < fieldCount; ++i, comma = ", ") {
+        s += comma;
+        if (namesIndex === 0)
+          s += "{";
+        s += A._rtiToString(fields[i], genericContext);
+        if (namesIndex >= 0)
+          s += " " + names[namesIndex];
+        ++namesIndex;
+      }
+      return s + "})";
+    },
+    _functionRtiToString(functionType, genericContext, bounds) {
+      var boundsLength, outerContextLength, offset, i, t1, t2, typeParametersText, typeSep, t3, t4, boundRti, kind, parameters, requiredPositional, requiredPositionalLength, optionalPositional, optionalPositionalLength, named, namedLength, returnTypeText, argumentsText, sep, _s2_ = ", ";
+      if (bounds != null) {
+        boundsLength = bounds.length;
+        if (genericContext == null) {
+          genericContext = A._setArrayType([], type$.JSArray_String);
+          outerContextLength = null;
+        } else
+          outerContextLength = genericContext.length;
+        offset = genericContext.length;
+        for (i = boundsLength; i > 0; --i)
+          B.JSArray_methods.add$1(genericContext, "T" + (offset + i));
+        for (t1 = type$.nullable_Object, t2 = type$.legacy_Object, typeParametersText = "<", typeSep = "", i = 0; i < boundsLength; ++i, typeSep = _s2_) {
+          t3 = genericContext.length;
+          t4 = t3 - 1 - i;
+          if (!(t4 >= 0))
+            return A.ioore(genericContext, t4);
+          typeParametersText = B.JSString_methods.$add(typeParametersText + typeSep, genericContext[t4]);
+          boundRti = bounds[i];
+          kind = boundRti._kind;
+          if (!(kind === 2 || kind === 3 || kind === 4 || kind === 5 || boundRti === t1))
+            t3 = boundRti === t2;
+          else
+            t3 = true;
+          if (!t3)
+            typeParametersText += " extends " + A._rtiToString(boundRti, genericContext);
+        }
+        typeParametersText += ">";
+      } else {
+        typeParametersText = "";
+        outerContextLength = null;
+      }
+      t1 = functionType._primary;
+      parameters = functionType._rest;
+      requiredPositional = parameters._requiredPositional;
+      requiredPositionalLength = requiredPositional.length;
+      optionalPositional = parameters._optionalPositional;
+      optionalPositionalLength = optionalPositional.length;
+      named = parameters._named;
+      namedLength = named.length;
+      returnTypeText = A._rtiToString(t1, genericContext);
+      for (argumentsText = "", sep = "", i = 0; i < requiredPositionalLength; ++i, sep = _s2_)
+        argumentsText += sep + A._rtiToString(requiredPositional[i], genericContext);
+      if (optionalPositionalLength > 0) {
+        argumentsText += sep + "[";
+        for (sep = "", i = 0; i < optionalPositionalLength; ++i, sep = _s2_)
+          argumentsText += sep + A._rtiToString(optionalPositional[i], genericContext);
+        argumentsText += "]";
+      }
+      if (namedLength > 0) {
+        argumentsText += sep + "{";
+        for (sep = "", i = 0; i < namedLength; i += 3, sep = _s2_) {
+          argumentsText += sep;
+          if (named[i + 1])
+            argumentsText += "required ";
+          argumentsText += A._rtiToString(named[i + 2], genericContext) + " " + named[i];
+        }
+        argumentsText += "}";
+      }
+      if (outerContextLength != null) {
+        genericContext.toString;
+        genericContext.length = outerContextLength;
+      }
+      return typeParametersText + "(" + argumentsText + ") => " + returnTypeText;
+    },
+    _rtiToString(rti, genericContext) {
+      var questionArgument, s, argumentKind, $name, $arguments, t1, t2,
+        kind = rti._kind;
+      if (kind === 5)
+        return "erased";
+      if (kind === 2)
+        return "dynamic";
+      if (kind === 3)
+        return "void";
+      if (kind === 1)
+        return "Never";
+      if (kind === 4)
+        return "any";
+      if (kind === 6)
+        return A._rtiToString(rti._primary, genericContext);
+      if (kind === 7) {
+        questionArgument = rti._primary;
+        s = A._rtiToString(questionArgument, genericContext);
+        argumentKind = questionArgument._kind;
+        return (argumentKind === 12 || argumentKind === 13 ? "(" + s + ")" : s) + "?";
+      }
+      if (kind === 8)
+        return "FutureOr<" + A._rtiToString(rti._primary, genericContext) + ">";
+      if (kind === 9) {
+        $name = A._unminifyOrTag(rti._primary);
+        $arguments = rti._rest;
+        return $arguments.length > 0 ? $name + ("<" + A._rtiArrayToString($arguments, genericContext) + ">") : $name;
+      }
+      if (kind === 11)
+        return A._recordRtiToString(rti, genericContext);
+      if (kind === 12)
+        return A._functionRtiToString(rti, genericContext, null);
+      if (kind === 13)
+        return A._functionRtiToString(rti._primary, genericContext, rti._rest);
+      if (kind === 14) {
+        t1 = rti._primary;
+        t2 = genericContext.length;
+        t1 = t2 - 1 - t1;
+        if (!(t1 >= 0 && t1 < t2))
+          return A.ioore(genericContext, t1);
+        return genericContext[t1];
+      }
+      return "?";
+    },
+    _unminifyOrTag(rawClassName) {
+      var preserved = init.mangledGlobalNames[rawClassName];
+      if (preserved != null)
+        return preserved;
+      return rawClassName;
+    },
+    _Universe_findRule(universe, targetType) {
+      var rule = universe.tR[targetType];
+      for (; typeof rule == "string";)
+        rule = universe.tR[rule];
+      return rule;
+    },
+    _Universe_findErasedType(universe, cls) {
+      var $length, erased, $arguments, i, $interface,
+        t1 = universe.eT,
+        probe = t1[cls];
+      if (probe == null)
+        return A._Universe_eval(universe, cls, false);
+      else if (typeof probe == "number") {
+        $length = probe;
+        erased = A._Universe__lookupTerminalRti(universe, 5, "#");
+        $arguments = A._Utils_newArrayOrEmpty($length);
+        for (i = 0; i < $length; ++i)
+          $arguments[i] = erased;
+        $interface = A._Universe__lookupInterfaceRti(universe, cls, $arguments);
+        t1[cls] = $interface;
+        return $interface;
+      } else
+        return probe;
+    },
+    _Universe_addRules(universe, rules) {
+      return A._Utils_objectAssign(universe.tR, rules);
+    },
+    _Universe_addErasedTypes(universe, types) {
+      return A._Utils_objectAssign(universe.eT, types);
+    },
+    _Universe_eval(universe, recipe, normalize) {
+      var rti,
+        t1 = universe.eC,
+        probe = t1.get(recipe);
+      if (probe != null)
+        return probe;
+      rti = A._Parser_parse(A._Parser_create(universe, null, recipe, normalize));
+      t1.set(recipe, rti);
+      return rti;
+    },
+    _Universe_evalInEnvironment(universe, environment, recipe) {
+      var probe, rti,
+        cache = environment._evalCache;
+      if (cache == null)
+        cache = environment._evalCache = new Map();
+      probe = cache.get(recipe);
+      if (probe != null)
+        return probe;
+      rti = A._Parser_parse(A._Parser_create(universe, environment, recipe, true));
+      cache.set(recipe, rti);
+      return rti;
+    },
+    _Universe_bind(universe, environment, argumentsRti) {
+      var argumentsRecipe, probe, rti,
+        cache = environment._bindCache;
+      if (cache == null)
+        cache = environment._bindCache = new Map();
+      argumentsRecipe = argumentsRti._canonicalRecipe;
+      probe = cache.get(argumentsRecipe);
+      if (probe != null)
+        return probe;
+      rti = A._Universe__lookupBindingRti(universe, environment, argumentsRti._kind === 10 ? argumentsRti._rest : [argumentsRti]);
+      cache.set(argumentsRecipe, rti);
+      return rti;
+    },
+    _Universe__installTypeTests(universe, rti) {
+      rti._as = A._installSpecializedAsCheck;
+      rti._is = A._installSpecializedIsTest;
+      return rti;
+    },
+    _Universe__lookupTerminalRti(universe, kind, key) {
+      var rti, t1,
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = kind;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__lookupStarRti(universe, baseType, normalize) {
+      var t1,
+        key = baseType._canonicalRecipe + "*",
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      t1 = A._Universe__createStarRti(universe, baseType, key, normalize);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__createStarRti(universe, baseType, key, normalize) {
+      var baseKind, t1, rti;
+      if (normalize) {
+        baseKind = baseType._kind;
+        if (!A.isSoundTopType(baseType))
+          t1 = baseType === type$.Null || baseType === type$.JSNull || baseKind === 7 || baseKind === 6;
+        else
+          t1 = true;
+        if (t1)
+          return baseType;
+      }
+      rti = new A.Rti(null, null);
+      rti._kind = 6;
+      rti._primary = baseType;
+      rti._canonicalRecipe = key;
+      return A._Universe__installTypeTests(universe, rti);
+    },
+    _Universe__lookupQuestionRti(universe, baseType, normalize) {
+      var t1,
+        key = baseType._canonicalRecipe + "?",
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      t1 = A._Universe__createQuestionRti(universe, baseType, key, normalize);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__createQuestionRti(universe, baseType, key, normalize) {
+      var baseKind, t1, starArgument, rti;
+      if (normalize) {
+        baseKind = baseType._kind;
+        if (!A.isSoundTopType(baseType))
+          if (!(baseType === type$.Null || baseType === type$.JSNull))
+            if (baseKind !== 7)
+              t1 = baseKind === 8 && A.isNullable(baseType._primary);
+            else
+              t1 = true;
+          else
+            t1 = true;
+        else
+          t1 = true;
+        if (t1)
+          return baseType;
+        else if (baseKind === 1 || baseType === type$.legacy_Never)
+          return type$.Null;
+        else if (baseKind === 6) {
+          starArgument = baseType._primary;
+          if (starArgument._kind === 8 && A.isNullable(starArgument._primary))
+            return starArgument;
+          else
+            return A.Rti__getQuestionFromStar(universe, baseType);
+        }
+      }
+      rti = new A.Rti(null, null);
+      rti._kind = 7;
+      rti._primary = baseType;
+      rti._canonicalRecipe = key;
+      return A._Universe__installTypeTests(universe, rti);
+    },
+    _Universe__lookupFutureOrRti(universe, baseType, normalize) {
+      var t1,
+        key = baseType._canonicalRecipe + "/",
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      t1 = A._Universe__createFutureOrRti(universe, baseType, key, normalize);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__createFutureOrRti(universe, baseType, key, normalize) {
+      var t1, rti;
+      if (normalize) {
+        t1 = baseType._kind;
+        if (A.isSoundTopType(baseType) || baseType === type$.Object || baseType === type$.legacy_Object)
+          return baseType;
+        else if (t1 === 1)
+          return A._Universe__lookupInterfaceRti(universe, "Future", [baseType]);
+        else if (baseType === type$.Null || baseType === type$.JSNull)
+          return type$.nullable_Future_Null;
+      }
+      rti = new A.Rti(null, null);
+      rti._kind = 8;
+      rti._primary = baseType;
+      rti._canonicalRecipe = key;
+      return A._Universe__installTypeTests(universe, rti);
+    },
+    _Universe__lookupGenericFunctionParameterRti(universe, index) {
+      var rti, t1,
+        key = "" + index + "^",
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 14;
+      rti._primary = index;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__canonicalRecipeJoin($arguments) {
+      var s, sep, i,
+        $length = $arguments.length;
+      for (s = "", sep = "", i = 0; i < $length; ++i, sep = ",")
+        s += sep + $arguments[i]._canonicalRecipe;
+      return s;
+    },
+    _Universe__canonicalRecipeJoinNamed($arguments) {
+      var s, sep, i, t1, nameSep,
+        $length = $arguments.length;
+      for (s = "", sep = "", i = 0; i < $length; i += 3, sep = ",") {
+        t1 = $arguments[i];
+        nameSep = $arguments[i + 1] ? "!" : ":";
+        s += sep + t1 + nameSep + $arguments[i + 2]._canonicalRecipe;
+      }
+      return s;
+    },
+    _Universe__lookupInterfaceRti(universe, $name, $arguments) {
+      var probe, rti, t1,
+        s = $name;
+      if ($arguments.length > 0)
+        s += "<" + A._Universe__canonicalRecipeJoin($arguments) + ">";
+      probe = universe.eC.get(s);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 9;
+      rti._primary = $name;
+      rti._rest = $arguments;
+      if ($arguments.length > 0)
+        rti._precomputed1 = $arguments[0];
+      rti._canonicalRecipe = s;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(s, t1);
+      return t1;
+    },
+    _Universe__lookupBindingRti(universe, base, $arguments) {
+      var newBase, newArguments, key, probe, rti, t1;
+      if (base._kind === 10) {
+        newBase = base._primary;
+        newArguments = base._rest.concat($arguments);
+      } else {
+        newArguments = $arguments;
+        newBase = base;
+      }
+      key = newBase._canonicalRecipe + (";<" + A._Universe__canonicalRecipeJoin(newArguments) + ">");
+      probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 10;
+      rti._primary = newBase;
+      rti._rest = newArguments;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__lookupRecordRti(universe, partialShapeTag, fields) {
+      var rti, t1,
+        key = "+" + (partialShapeTag + "(" + A._Universe__canonicalRecipeJoin(fields) + ")"),
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 11;
+      rti._primary = partialShapeTag;
+      rti._rest = fields;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__lookupFunctionRti(universe, returnType, parameters) {
+      var sep, key, probe, rti, t1,
+        s = returnType._canonicalRecipe,
+        requiredPositional = parameters._requiredPositional,
+        requiredPositionalLength = requiredPositional.length,
+        optionalPositional = parameters._optionalPositional,
+        optionalPositionalLength = optionalPositional.length,
+        named = parameters._named,
+        namedLength = named.length,
+        recipe = "(" + A._Universe__canonicalRecipeJoin(requiredPositional);
+      if (optionalPositionalLength > 0) {
+        sep = requiredPositionalLength > 0 ? "," : "";
+        recipe += sep + "[" + A._Universe__canonicalRecipeJoin(optionalPositional) + "]";
+      }
+      if (namedLength > 0) {
+        sep = requiredPositionalLength > 0 ? "," : "";
+        recipe += sep + "{" + A._Universe__canonicalRecipeJoinNamed(named) + "}";
+      }
+      key = s + (recipe + ")");
+      probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 12;
+      rti._primary = returnType;
+      rti._rest = parameters;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__lookupGenericFunctionRti(universe, baseFunctionType, bounds, normalize) {
+      var t1,
+        key = baseFunctionType._canonicalRecipe + ("<" + A._Universe__canonicalRecipeJoin(bounds) + ">"),
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      t1 = A._Universe__createGenericFunctionRti(universe, baseFunctionType, bounds, key, normalize);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__createGenericFunctionRti(universe, baseFunctionType, bounds, key, normalize) {
+      var $length, typeArguments, count, i, bound, substitutedBase, substitutedBounds, rti;
+      if (normalize) {
+        $length = bounds.length;
+        typeArguments = A._Utils_newArrayOrEmpty($length);
+        for (count = 0, i = 0; i < $length; ++i) {
+          bound = bounds[i];
+          if (bound._kind === 1) {
+            typeArguments[i] = bound;
+            ++count;
+          }
+        }
+        if (count > 0) {
+          substitutedBase = A._substitute(universe, baseFunctionType, typeArguments, 0);
+          substitutedBounds = A._substituteArray(universe, bounds, typeArguments, 0);
+          return A._Universe__lookupGenericFunctionRti(universe, substitutedBase, substitutedBounds, bounds !== substitutedBounds);
+        }
+      }
+      rti = new A.Rti(null, null);
+      rti._kind = 13;
+      rti._primary = baseFunctionType;
+      rti._rest = bounds;
+      rti._canonicalRecipe = key;
+      return A._Universe__installTypeTests(universe, rti);
+    },
+    _Parser_create(universe, environment, recipe, normalize) {
+      return {u: universe, e: environment, r: recipe, s: [], p: 0, n: normalize};
+    },
+    _Parser_parse(parser) {
+      var t2, i, ch, t3, array, end, item,
+        source = parser.r,
+        t1 = parser.s;
+      for (t2 = source.length, i = 0; i < t2;) {
+        ch = source.charCodeAt(i);
+        if (ch >= 48 && ch <= 57)
+          i = A._Parser_handleDigit(i + 1, ch, source, t1);
+        else if ((((ch | 32) >>> 0) - 97 & 65535) < 26 || ch === 95 || ch === 36 || ch === 124)
+          i = A._Parser_handleIdentifier(parser, i, source, t1, false);
+        else if (ch === 46)
+          i = A._Parser_handleIdentifier(parser, i, source, t1, true);
+        else {
+          ++i;
+          switch (ch) {
+            case 44:
+              break;
+            case 58:
+              t1.push(false);
+              break;
+            case 33:
+              t1.push(true);
+              break;
+            case 59:
+              t1.push(A._Parser_toType(parser.u, parser.e, t1.pop()));
+              break;
+            case 94:
+              t1.push(A._Universe__lookupGenericFunctionParameterRti(parser.u, t1.pop()));
+              break;
+            case 35:
+              t1.push(A._Universe__lookupTerminalRti(parser.u, 5, "#"));
+              break;
+            case 64:
+              t1.push(A._Universe__lookupTerminalRti(parser.u, 2, "@"));
+              break;
+            case 126:
+              t1.push(A._Universe__lookupTerminalRti(parser.u, 3, "~"));
+              break;
+            case 60:
+              t1.push(parser.p);
+              parser.p = t1.length;
+              break;
+            case 62:
+              A._Parser_handleTypeArguments(parser, t1);
+              break;
+            case 38:
+              A._Parser_handleExtendedOperations(parser, t1);
+              break;
+            case 42:
+              t3 = parser.u;
+              t1.push(A._Universe__lookupStarRti(t3, A._Parser_toType(t3, parser.e, t1.pop()), parser.n));
+              break;
+            case 63:
+              t3 = parser.u;
+              t1.push(A._Universe__lookupQuestionRti(t3, A._Parser_toType(t3, parser.e, t1.pop()), parser.n));
+              break;
+            case 47:
+              t3 = parser.u;
+              t1.push(A._Universe__lookupFutureOrRti(t3, A._Parser_toType(t3, parser.e, t1.pop()), parser.n));
+              break;
+            case 40:
+              t1.push(-3);
+              t1.push(parser.p);
+              parser.p = t1.length;
+              break;
+            case 41:
+              A._Parser_handleArguments(parser, t1);
+              break;
+            case 91:
+              t1.push(parser.p);
+              parser.p = t1.length;
+              break;
+            case 93:
+              array = t1.splice(parser.p);
+              A._Parser_toTypes(parser.u, parser.e, array);
+              parser.p = t1.pop();
+              t1.push(array);
+              t1.push(-1);
+              break;
+            case 123:
+              t1.push(parser.p);
+              parser.p = t1.length;
+              break;
+            case 125:
+              array = t1.splice(parser.p);
+              A._Parser_toTypesNamed(parser.u, parser.e, array);
+              parser.p = t1.pop();
+              t1.push(array);
+              t1.push(-2);
+              break;
+            case 43:
+              end = source.indexOf("(", i);
+              t1.push(source.substring(i, end));
+              t1.push(-4);
+              t1.push(parser.p);
+              parser.p = t1.length;
+              i = end + 1;
+              break;
+            default:
+              throw "Bad character " + ch;
+          }
+        }
+      }
+      item = t1.pop();
+      return A._Parser_toType(parser.u, parser.e, item);
+    },
+    _Parser_handleDigit(i, digit, source, stack) {
+      var t1, ch,
+        value = digit - 48;
+      for (t1 = source.length; i < t1; ++i) {
+        ch = source.charCodeAt(i);
+        if (!(ch >= 48 && ch <= 57))
+          break;
+        value = value * 10 + (ch - 48);
+      }
+      stack.push(value);
+      return i;
+    },
+    _Parser_handleIdentifier(parser, start, source, stack, hasPeriod) {
+      var t1, ch, t2, string, environment, recipe,
+        i = start + 1;
+      for (t1 = source.length; i < t1; ++i) {
+        ch = source.charCodeAt(i);
+        if (ch === 46) {
+          if (hasPeriod)
+            break;
+          hasPeriod = true;
+        } else {
+          if (!((((ch | 32) >>> 0) - 97 & 65535) < 26 || ch === 95 || ch === 36 || ch === 124))
+            t2 = ch >= 48 && ch <= 57;
+          else
+            t2 = true;
+          if (!t2)
+            break;
+        }
+      }
+      string = source.substring(start, i);
+      if (hasPeriod) {
+        t1 = parser.u;
+        environment = parser.e;
+        if (environment._kind === 10)
+          environment = environment._primary;
+        recipe = A._Universe_findRule(t1, environment._primary)[string];
+        if (recipe == null)
+          A.throwExpression('No "' + string + '" in "' + A.Rti__getCanonicalRecipe(environment) + '"');
+        stack.push(A._Universe_evalInEnvironment(t1, environment, recipe));
+      } else
+        stack.push(string);
+      return i;
+    },
+    _Parser_handleTypeArguments(parser, stack) {
+      var base,
+        t1 = parser.u,
+        $arguments = A._Parser_collectArray(parser, stack),
+        head = stack.pop();
+      if (typeof head == "string")
+        stack.push(A._Universe__lookupInterfaceRti(t1, head, $arguments));
+      else {
+        base = A._Parser_toType(t1, parser.e, head);
+        switch (base._kind) {
+          case 12:
+            stack.push(A._Universe__lookupGenericFunctionRti(t1, base, $arguments, parser.n));
+            break;
+          default:
+            stack.push(A._Universe__lookupBindingRti(t1, base, $arguments));
+            break;
+        }
+      }
+    },
+    _Parser_handleArguments(parser, stack) {
+      var optionalPositional, named, requiredPositional, returnType, parameters, _null = null,
+        t1 = parser.u,
+        head = stack.pop();
+      if (typeof head == "number")
+        switch (head) {
+          case -1:
+            optionalPositional = stack.pop();
+            named = _null;
+            break;
+          case -2:
+            named = stack.pop();
+            optionalPositional = _null;
+            break;
+          default:
+            stack.push(head);
+            named = _null;
+            optionalPositional = named;
+            break;
+        }
+      else {
+        stack.push(head);
+        named = _null;
+        optionalPositional = named;
+      }
+      requiredPositional = A._Parser_collectArray(parser, stack);
+      head = stack.pop();
+      switch (head) {
+        case -3:
+          head = stack.pop();
+          if (optionalPositional == null)
+            optionalPositional = t1.sEA;
+          if (named == null)
+            named = t1.sEA;
+          returnType = A._Parser_toType(t1, parser.e, head);
+          parameters = new A._FunctionParameters();
+          parameters._requiredPositional = requiredPositional;
+          parameters._optionalPositional = optionalPositional;
+          parameters._named = named;
+          stack.push(A._Universe__lookupFunctionRti(t1, returnType, parameters));
+          return;
+        case -4:
+          stack.push(A._Universe__lookupRecordRti(t1, stack.pop(), requiredPositional));
+          return;
+        default:
+          throw A.wrapException(A.AssertionError$("Unexpected state under `()`: " + A.S(head)));
+      }
+    },
+    _Parser_handleExtendedOperations(parser, stack) {
+      var $top = stack.pop();
+      if (0 === $top) {
+        stack.push(A._Universe__lookupTerminalRti(parser.u, 1, "0&"));
+        return;
+      }
+      if (1 === $top) {
+        stack.push(A._Universe__lookupTerminalRti(parser.u, 4, "1&"));
+        return;
+      }
+      throw A.wrapException(A.AssertionError$("Unexpected extended operation " + A.S($top)));
+    },
+    _Parser_collectArray(parser, stack) {
+      var array = stack.splice(parser.p);
+      A._Parser_toTypes(parser.u, parser.e, array);
+      parser.p = stack.pop();
+      return array;
+    },
+    _Parser_toType(universe, environment, item) {
+      if (typeof item == "string")
+        return A._Universe__lookupInterfaceRti(universe, item, universe.sEA);
+      else if (typeof item == "number") {
+        environment.toString;
+        return A._Parser_indexToType(universe, environment, item);
+      } else
+        return item;
+    },
+    _Parser_toTypes(universe, environment, items) {
+      var i,
+        $length = items.length;
+      for (i = 0; i < $length; ++i)
+        items[i] = A._Parser_toType(universe, environment, items[i]);
+    },
+    _Parser_toTypesNamed(universe, environment, items) {
+      var i,
+        $length = items.length;
+      for (i = 2; i < $length; i += 3)
+        items[i] = A._Parser_toType(universe, environment, items[i]);
+    },
+    _Parser_indexToType(universe, environment, index) {
+      var typeArguments, len,
+        kind = environment._kind;
+      if (kind === 10) {
+        if (index === 0)
+          return environment._primary;
+        typeArguments = environment._rest;
+        len = typeArguments.length;
+        if (index <= len)
+          return typeArguments[index - 1];
+        index -= len;
+        environment = environment._primary;
+        kind = environment._kind;
+      } else if (index === 0)
+        return environment;
+      if (kind !== 9)
+        throw A.wrapException(A.AssertionError$("Indexed base must be an interface type"));
+      typeArguments = environment._rest;
+      if (index <= typeArguments.length)
+        return typeArguments[index - 1];
+      throw A.wrapException(A.AssertionError$("Bad index " + index + " for " + environment.toString$0(0)));
+    },
+    isSubtype(universe, s, t) {
+      var result,
+        sCache = s._isSubtypeCache;
+      if (sCache == null)
+        sCache = s._isSubtypeCache = new Map();
+      result = sCache.get(t);
+      if (result == null) {
+        result = A._isSubtype(universe, s, null, t, null, false) ? 1 : 0;
+        sCache.set(t, result);
+      }
+      if (0 === result)
+        return false;
+      if (1 === result)
+        return true;
+      return true;
+    },
+    _isSubtype(universe, s, sEnv, t, tEnv, isLegacy) {
+      var t1, sKind, leftTypeVariable, tKind, t2, sBounds, tBounds, sLength, i, sBound, tBound;
+      if (s === t)
+        return true;
+      if (!A.isSoundTopType(t))
+        t1 = t === type$.legacy_Object;
+      else
+        t1 = true;
+      if (t1)
+        return true;
+      sKind = s._kind;
+      if (sKind === 4)
+        return true;
+      if (A.isSoundTopType(s))
+        return false;
+      t1 = s._kind;
+      if (t1 === 1)
+        return true;
+      leftTypeVariable = sKind === 14;
+      if (leftTypeVariable)
+        if (A._isSubtype(universe, sEnv[s._primary], sEnv, t, tEnv, false))
+          return true;
+      tKind = t._kind;
+      t1 = s === type$.Null || s === type$.JSNull;
+      if (t1) {
+        if (tKind === 8)
+          return A._isSubtype(universe, s, sEnv, t._primary, tEnv, false);
+        return t === type$.Null || t === type$.JSNull || tKind === 7 || tKind === 6;
+      }
+      if (t === type$.Object) {
+        if (sKind === 8)
+          return A._isSubtype(universe, s._primary, sEnv, t, tEnv, false);
+        if (sKind === 6)
+          return A._isSubtype(universe, s._primary, sEnv, t, tEnv, false);
+        return sKind !== 7;
+      }
+      if (sKind === 6)
+        return A._isSubtype(universe, s._primary, sEnv, t, tEnv, false);
+      if (tKind === 6) {
+        t1 = A.Rti__getQuestionFromStar(universe, t);
+        return A._isSubtype(universe, s, sEnv, t1, tEnv, false);
+      }
+      if (sKind === 8) {
+        if (!A._isSubtype(universe, s._primary, sEnv, t, tEnv, false))
+          return false;
+        return A._isSubtype(universe, A.Rti__getFutureFromFutureOr(universe, s), sEnv, t, tEnv, false);
+      }
+      if (sKind === 7) {
+        t1 = A._isSubtype(universe, type$.Null, sEnv, t, tEnv, false);
+        return t1 && A._isSubtype(universe, s._primary, sEnv, t, tEnv, false);
+      }
+      if (tKind === 8) {
+        if (A._isSubtype(universe, s, sEnv, t._primary, tEnv, false))
+          return true;
+        return A._isSubtype(universe, s, sEnv, A.Rti__getFutureFromFutureOr(universe, t), tEnv, false);
+      }
+      if (tKind === 7) {
+        t1 = A._isSubtype(universe, s, sEnv, type$.Null, tEnv, false);
+        return t1 || A._isSubtype(universe, s, sEnv, t._primary, tEnv, false);
+      }
+      if (leftTypeVariable)
+        return false;
+      t1 = sKind !== 12;
+      if ((!t1 || sKind === 13) && t === type$.Function)
+        return true;
+      t2 = sKind === 11;
+      if (t2 && t === type$.Record)
+        return true;
+      if (tKind === 13) {
+        if (s === type$.JavaScriptFunction)
+          return true;
+        if (sKind !== 13)
+          return false;
+        sBounds = s._rest;
+        tBounds = t._rest;
+        sLength = sBounds.length;
+        if (sLength !== tBounds.length)
+          return false;
+        sEnv = sEnv == null ? sBounds : sBounds.concat(sEnv);
+        tEnv = tEnv == null ? tBounds : tBounds.concat(tEnv);
+        for (i = 0; i < sLength; ++i) {
+          sBound = sBounds[i];
+          tBound = tBounds[i];
+          if (!A._isSubtype(universe, sBound, sEnv, tBound, tEnv, false) || !A._isSubtype(universe, tBound, tEnv, sBound, sEnv, false))
+            return false;
+        }
+        return A._isFunctionSubtype(universe, s._primary, sEnv, t._primary, tEnv, false);
+      }
+      if (tKind === 12) {
+        if (s === type$.JavaScriptFunction)
+          return true;
+        if (t1)
+          return false;
+        return A._isFunctionSubtype(universe, s, sEnv, t, tEnv, false);
+      }
+      if (sKind === 9) {
+        if (tKind !== 9)
+          return false;
+        return A._isInterfaceSubtype(universe, s, sEnv, t, tEnv, false);
+      }
+      if (t2 && tKind === 11)
+        return A._isRecordSubtype(universe, s, sEnv, t, tEnv, false);
+      return false;
+    },
+    _isFunctionSubtype(universe, s, sEnv, t, tEnv, isLegacy) {
+      var sParameters, tParameters, sRequiredPositional, tRequiredPositional, sRequiredPositionalLength, tRequiredPositionalLength, requiredPositionalDelta, sOptionalPositional, tOptionalPositional, sOptionalPositionalLength, tOptionalPositionalLength, i, t1, sNamed, tNamed, sNamedLength, tNamedLength, sIndex, tIndex, tName, sName, sIsRequired;
+      if (!A._isSubtype(universe, s._primary, sEnv, t._primary, tEnv, false))
+        return false;
+      sParameters = s._rest;
+      tParameters = t._rest;
+      sRequiredPositional = sParameters._requiredPositional;
+      tRequiredPositional = tParameters._requiredPositional;
+      sRequiredPositionalLength = sRequiredPositional.length;
+      tRequiredPositionalLength = tRequiredPositional.length;
+      if (sRequiredPositionalLength > tRequiredPositionalLength)
+        return false;
+      requiredPositionalDelta = tRequiredPositionalLength - sRequiredPositionalLength;
+      sOptionalPositional = sParameters._optionalPositional;
+      tOptionalPositional = tParameters._optionalPositional;
+      sOptionalPositionalLength = sOptionalPositional.length;
+      tOptionalPositionalLength = tOptionalPositional.length;
+      if (sRequiredPositionalLength + sOptionalPositionalLength < tRequiredPositionalLength + tOptionalPositionalLength)
+        return false;
+      for (i = 0; i < sRequiredPositionalLength; ++i) {
+        t1 = sRequiredPositional[i];
+        if (!A._isSubtype(universe, tRequiredPositional[i], tEnv, t1, sEnv, false))
+          return false;
+      }
+      for (i = 0; i < requiredPositionalDelta; ++i) {
+        t1 = sOptionalPositional[i];
+        if (!A._isSubtype(universe, tRequiredPositional[sRequiredPositionalLength + i], tEnv, t1, sEnv, false))
+          return false;
+      }
+      for (i = 0; i < tOptionalPositionalLength; ++i) {
+        t1 = sOptionalPositional[requiredPositionalDelta + i];
+        if (!A._isSubtype(universe, tOptionalPositional[i], tEnv, t1, sEnv, false))
+          return false;
+      }
+      sNamed = sParameters._named;
+      tNamed = tParameters._named;
+      sNamedLength = sNamed.length;
+      tNamedLength = tNamed.length;
+      for (sIndex = 0, tIndex = 0; tIndex < tNamedLength; tIndex += 3) {
+        tName = tNamed[tIndex];
+        for (; true;) {
+          if (sIndex >= sNamedLength)
+            return false;
+          sName = sNamed[sIndex];
+          sIndex += 3;
+          if (tName < sName)
+            return false;
+          sIsRequired = sNamed[sIndex - 2];
+          if (sName < tName) {
+            if (sIsRequired)
+              return false;
+            continue;
+          }
+          t1 = tNamed[tIndex + 1];
+          if (sIsRequired && !t1)
+            return false;
+          t1 = sNamed[sIndex - 1];
+          if (!A._isSubtype(universe, tNamed[tIndex + 2], tEnv, t1, sEnv, false))
+            return false;
+          break;
+        }
+      }
+      for (; sIndex < sNamedLength;) {
+        if (sNamed[sIndex + 1])
+          return false;
+        sIndex += 3;
+      }
+      return true;
+    },
+    _isInterfaceSubtype(universe, s, sEnv, t, tEnv, isLegacy) {
+      var rule, recipes, $length, supertypeArgs, i,
+        sName = s._primary,
+        tName = t._primary;
+      for (; sName !== tName;) {
+        rule = universe.tR[sName];
+        if (rule == null)
+          return false;
+        if (typeof rule == "string") {
+          sName = rule;
+          continue;
+        }
+        recipes = rule[tName];
+        if (recipes == null)
+          return false;
+        $length = recipes.length;
+        supertypeArgs = $length > 0 ? new Array($length) : init.typeUniverse.sEA;
+        for (i = 0; i < $length; ++i)
+          supertypeArgs[i] = A._Universe_evalInEnvironment(universe, s, recipes[i]);
+        return A._areArgumentsSubtypes(universe, supertypeArgs, null, sEnv, t._rest, tEnv, false);
+      }
+      return A._areArgumentsSubtypes(universe, s._rest, null, sEnv, t._rest, tEnv, false);
+    },
+    _areArgumentsSubtypes(universe, sArgs, sVariances, sEnv, tArgs, tEnv, isLegacy) {
+      var i,
+        $length = sArgs.length;
+      for (i = 0; i < $length; ++i)
+        if (!A._isSubtype(universe, sArgs[i], sEnv, tArgs[i], tEnv, false))
+          return false;
+      return true;
+    },
+    _isRecordSubtype(universe, s, sEnv, t, tEnv, isLegacy) {
+      var i,
+        sFields = s._rest,
+        tFields = t._rest,
+        sCount = sFields.length;
+      if (sCount !== tFields.length)
+        return false;
+      if (s._primary !== t._primary)
+        return false;
+      for (i = 0; i < sCount; ++i)
+        if (!A._isSubtype(universe, sFields[i], sEnv, tFields[i], tEnv, false))
+          return false;
+      return true;
+    },
+    isNullable(t) {
+      var t1,
+        kind = t._kind;
+      if (!(t === type$.Null || t === type$.JSNull))
+        if (!A.isSoundTopType(t))
+          if (kind !== 7)
+            if (!(kind === 6 && A.isNullable(t._primary)))
+              t1 = kind === 8 && A.isNullable(t._primary);
+            else
+              t1 = true;
+          else
+            t1 = true;
+        else
+          t1 = true;
+      else
+        t1 = true;
+      return t1;
+    },
+    isDefinitelyTopType(t) {
+      var t1;
+      if (!A.isSoundTopType(t))
+        t1 = t === type$.legacy_Object;
+      else
+        t1 = true;
+      return t1;
+    },
+    isSoundTopType(t) {
+      var kind = t._kind;
+      return kind === 2 || kind === 3 || kind === 4 || kind === 5 || t === type$.nullable_Object;
+    },
+    _Utils_objectAssign(o, other) {
+      var i, key,
+        keys = Object.keys(other),
+        $length = keys.length;
+      for (i = 0; i < $length; ++i) {
+        key = keys[i];
+        o[key] = other[key];
+      }
+    },
+    _Utils_newArrayOrEmpty($length) {
+      return $length > 0 ? new Array($length) : init.typeUniverse.sEA;
+    },
+    Rti: function Rti(t0, t1) {
+      var _ = this;
+      _._as = t0;
+      _._is = t1;
+      _._cachedRuntimeType = _._specializedTestResource = _._isSubtypeCache = _._precomputed1 = null;
+      _._kind = 0;
+      _._canonicalRecipe = _._bindCache = _._evalCache = _._rest = _._primary = null;
+    },
+    _FunctionParameters: function _FunctionParameters() {
+      this._named = this._optionalPositional = this._requiredPositional = null;
+    },
+    _Type: function _Type(t0) {
+      this._rti = t0;
+    },
+    _Error: function _Error() {
+    },
+    _TypeError: function _TypeError(t0) {
+      this.__rti$_message = t0;
+    },
+    _AsyncRun__initializeScheduleImmediate() {
+      var div, span, t1 = {};
+      if (self.scheduleImmediate != null)
+        return A.async__AsyncRun__scheduleImmediateJsOverride$closure();
+      if (self.MutationObserver != null && self.document != null) {
+        div = self.document.createElement("div");
+        span = self.document.createElement("span");
+        t1.storedCallback = null;
+        new self.MutationObserver(A.convertDartClosureToJS(new A._AsyncRun__initializeScheduleImmediate_internalCallback(t1), 1)).observe(div, {childList: true});
+        return new A._AsyncRun__initializeScheduleImmediate_closure(t1, div, span);
+      } else if (self.setImmediate != null)
+        return A.async__AsyncRun__scheduleImmediateWithSetImmediate$closure();
+      return A.async__AsyncRun__scheduleImmediateWithTimer$closure();
+    },
+    _AsyncRun__scheduleImmediateJsOverride(callback) {
+      self.scheduleImmediate(A.convertDartClosureToJS(new A._AsyncRun__scheduleImmediateJsOverride_internalCallback(type$.void_Function._as(callback)), 0));
+    },
+    _AsyncRun__scheduleImmediateWithSetImmediate(callback) {
+      self.setImmediate(A.convertDartClosureToJS(new A._AsyncRun__scheduleImmediateWithSetImmediate_internalCallback(type$.void_Function._as(callback)), 0));
+    },
+    _AsyncRun__scheduleImmediateWithTimer(callback) {
+      A.Timer__createTimer(B.Duration_0, type$.void_Function._as(callback));
+    },
+    Timer__createTimer(duration, callback) {
+      return A._TimerImpl$(duration._duration / 1000 | 0, callback);
+    },
+    _TimerImpl$(milliseconds, callback) {
+      var t1 = new A._TimerImpl();
+      t1._TimerImpl$2(milliseconds, callback);
+      return t1;
+    },
+    _makeAsyncAwaitCompleter($T) {
+      return new A._AsyncAwaitCompleter(new A._Future($.Zone__current, $T._eval$1("_Future<0>")), $T._eval$1("_AsyncAwaitCompleter<0>"));
+    },
+    _asyncStartSync(bodyFunction, completer) {
+      bodyFunction.call$2(0, null);
+      completer.isSync = true;
+      return completer._future;
+    },
+    _asyncAwait(object, bodyFunction) {
+      A._awaitOnObject(object, bodyFunction);
+    },
+    _asyncReturn(object, completer) {
+      completer.complete$1(object);
+    },
+    _asyncRethrow(object, completer) {
+      completer.completeError$2(A.unwrapException(object), A.getTraceFromException(object));
+    },
+    _awaitOnObject(object, bodyFunction) {
+      var t1, future,
+        thenCallback = new A._awaitOnObject_closure(bodyFunction),
+        errorCallback = new A._awaitOnObject_closure0(bodyFunction);
+      if (object instanceof A._Future)
+        object._thenAwait$1$2(thenCallback, errorCallback, type$.dynamic);
+      else {
+        t1 = type$.dynamic;
+        if (object instanceof A._Future)
+          object.then$1$2$onError(thenCallback, errorCallback, t1);
+        else {
+          future = new A._Future($.Zone__current, type$._Future_dynamic);
+          future._state = 8;
+          future._resultOrListeners = object;
+          future._thenAwait$1$2(thenCallback, errorCallback, t1);
+        }
+      }
+    },
+    _wrapJsFunctionForAsync($function) {
+      var $protected = function(fn, ERROR) {
+        return function(errorCode, result) {
+          while (true) {
+            try {
+              fn(errorCode, result);
+              break;
+            } catch (error) {
+              result = error;
+              errorCode = ERROR;
+            }
+          }
+        };
+      }($function, 1);
+      return $.Zone__current.registerBinaryCallback$3$1(new A._wrapJsFunctionForAsync_closure($protected), type$.void, type$.int, type$.dynamic);
+    },
+    AsyncError$(error, stackTrace) {
+      var t1 = A.checkNotNullable(error, "error", type$.Object);
+      return new A.AsyncError(t1, stackTrace == null ? A.AsyncError_defaultStackTrace(error) : stackTrace);
+    },
+    AsyncError_defaultStackTrace(error) {
+      var stackTrace;
+      if (type$.Error._is(error)) {
+        stackTrace = error.get$stackTrace();
+        if (stackTrace != null)
+          return stackTrace;
+      }
+      return B._StringStackTrace_3uE;
+    },
+    Future_Future$sync(computation, $T) {
+      var result, error, stackTrace, future, replacement, t1, exception;
+      try {
+        result = computation.call$0();
+        t1 = $T._eval$1("Future<0>")._is(result) ? result : A._Future$value(result, $T);
+        return t1;
+      } catch (exception) {
+        error = A.unwrapException(exception);
+        stackTrace = A.getTraceFromException(exception);
+        future = new A._Future($.Zone__current, $T._eval$1("_Future<0>"));
+        type$.Object._as(error);
+        type$.nullable_StackTrace._as(stackTrace);
+        replacement = null;
+        if (replacement != null)
+          future._asyncCompleteError$2(replacement.get$error(), replacement.get$stackTrace());
+        else
+          future._asyncCompleteError$2(error, stackTrace);
+        return future;
+      }
+    },
+    Future_Future$value(value, $T) {
+      var t1 = value == null ? $T._as(value) : value,
+        t2 = new A._Future($.Zone__current, $T._eval$1("_Future<0>"));
+      t2._asyncComplete$1(t1);
+      return t2;
+    },
+    Completer_Completer($T) {
+      return new A._AsyncCompleter(new A._Future($.Zone__current, $T._eval$1("_Future<0>")), $T._eval$1("_AsyncCompleter<0>"));
+    },
+    _Future$value(value, $T) {
+      var t1 = new A._Future($.Zone__current, $T._eval$1("_Future<0>"));
+      $T._as(value);
+      t1._state = 8;
+      t1._resultOrListeners = value;
+      return t1;
+    },
+    _Future__chainCoreFutureSync(source, target) {
+      var t1, t2, listeners;
+      for (t1 = type$._Future_dynamic; t2 = source._state, (t2 & 4) !== 0;)
+        source = t1._as(source._resultOrListeners);
+      if ((t2 & 24) !== 0) {
+        listeners = target._removeListeners$0();
+        target._cloneResult$1(source);
+        A._Future__propagateToListeners(target, listeners);
+      } else {
+        listeners = type$.nullable__FutureListener_dynamic_dynamic._as(target._resultOrListeners);
+        target._setChained$1(source);
+        source._prependListeners$1(listeners);
+      }
+    },
+    _Future__chainCoreFutureAsync(source, target) {
+      var t2, t3, listeners, _box_0 = {},
+        t1 = _box_0.source = source;
+      for (t2 = type$._Future_dynamic; t3 = t1._state, (t3 & 4) !== 0; t1 = source) {
+        source = t2._as(t1._resultOrListeners);
+        _box_0.source = source;
+      }
+      if ((t3 & 24) === 0) {
+        listeners = type$.nullable__FutureListener_dynamic_dynamic._as(target._resultOrListeners);
+        target._setChained$1(t1);
+        _box_0.source._prependListeners$1(listeners);
+        return;
+      }
+      if ((t3 & 16) === 0 && target._resultOrListeners == null) {
+        target._cloneResult$1(t1);
+        return;
+      }
+      target._state ^= 2;
+      A._rootScheduleMicrotask(null, null, target._zone, type$.void_Function._as(new A._Future__chainCoreFutureAsync_closure(_box_0, target)));
+    },
+    _Future__propagateToListeners(source, listeners) {
+      var t2, t3, t4, _box_0, t5, t6, hasError, asyncError, nextListener, nextListener0, sourceResult, t7, zone, oldZone, result, current, _box_1 = {},
+        t1 = _box_1.source = source;
+      for (t2 = type$.AsyncError, t3 = type$.nullable__FutureListener_dynamic_dynamic, t4 = type$.Future_dynamic; true;) {
+        _box_0 = {};
+        t5 = t1._state;
+        t6 = (t5 & 16) === 0;
+        hasError = !t6;
+        if (listeners == null) {
+          if (hasError && (t5 & 1) === 0) {
+            asyncError = t2._as(t1._resultOrListeners);
+            A._rootHandleError(asyncError.error, asyncError.stackTrace);
+          }
+          return;
+        }
+        _box_0.listener = listeners;
+        nextListener = listeners._nextListener;
+        for (t1 = listeners; nextListener != null; t1 = nextListener, nextListener = nextListener0) {
+          t1._nextListener = null;
+          A._Future__propagateToListeners(_box_1.source, t1);
+          _box_0.listener = nextListener;
+          nextListener0 = nextListener._nextListener;
+        }
+        t5 = _box_1.source;
+        sourceResult = t5._resultOrListeners;
+        _box_0.listenerHasError = hasError;
+        _box_0.listenerValueOrError = sourceResult;
+        if (t6) {
+          t7 = t1.state;
+          t7 = (t7 & 1) !== 0 || (t7 & 15) === 8;
+        } else
+          t7 = true;
+        if (t7) {
+          zone = t1.result._zone;
+          if (hasError) {
+            t5 = t5._zone === zone;
+            t5 = !(t5 || t5);
+          } else
+            t5 = false;
+          if (t5) {
+            t2._as(sourceResult);
+            A._rootHandleError(sourceResult.error, sourceResult.stackTrace);
+            return;
+          }
+          oldZone = $.Zone__current;
+          if (oldZone !== zone)
+            $.Zone__current = zone;
+          else
+            oldZone = null;
+          t1 = t1.state;
+          if ((t1 & 15) === 8)
+            new A._Future__propagateToListeners_handleWhenCompleteCallback(_box_0, _box_1, hasError).call$0();
+          else if (t6) {
+            if ((t1 & 1) !== 0)
+              new A._Future__propagateToListeners_handleValueCallback(_box_0, sourceResult).call$0();
+          } else if ((t1 & 2) !== 0)
+            new A._Future__propagateToListeners_handleError(_box_1, _box_0).call$0();
+          if (oldZone != null)
+            $.Zone__current = oldZone;
+          t1 = _box_0.listenerValueOrError;
+          if (t1 instanceof A._Future) {
+            t5 = _box_0.listener.$ti;
+            t5 = t5._eval$1("Future<2>")._is(t1) || !t5._rest[1]._is(t1);
+          } else
+            t5 = false;
+          if (t5) {
+            t4._as(t1);
+            result = _box_0.listener.result;
+            if ((t1._state & 24) !== 0) {
+              current = t3._as(result._resultOrListeners);
+              result._resultOrListeners = null;
+              listeners = result._reverseListeners$1(current);
+              result._state = t1._state & 30 | result._state & 1;
+              result._resultOrListeners = t1._resultOrListeners;
+              _box_1.source = t1;
+              continue;
+            } else
+              A._Future__chainCoreFutureSync(t1, result);
+            return;
+          }
+        }
+        result = _box_0.listener.result;
+        current = t3._as(result._resultOrListeners);
+        result._resultOrListeners = null;
+        listeners = result._reverseListeners$1(current);
+        t1 = _box_0.listenerHasError;
+        t5 = _box_0.listenerValueOrError;
+        if (!t1) {
+          result.$ti._precomputed1._as(t5);
+          result._state = 8;
+          result._resultOrListeners = t5;
+        } else {
+          t2._as(t5);
+          result._state = result._state & 1 | 16;
+          result._resultOrListeners = t5;
+        }
+        _box_1.source = result;
+        t1 = result;
+      }
+    },
+    _registerErrorHandler(errorHandler, zone) {
+      var t1;
+      if (type$.dynamic_Function_Object_StackTrace._is(errorHandler))
+        return zone.registerBinaryCallback$3$1(errorHandler, type$.dynamic, type$.Object, type$.StackTrace);
+      t1 = type$.dynamic_Function_Object;
+      if (t1._is(errorHandler))
+        return t1._as(errorHandler);
+      throw A.wrapException(A.ArgumentError$value(errorHandler, "onError", string$.Error_));
+    },
+    _microtaskLoop() {
+      var entry, next;
+      for (entry = $._nextCallback; entry != null; entry = $._nextCallback) {
+        $._lastPriorityCallback = null;
+        next = entry.next;
+        $._nextCallback = next;
+        if (next == null)
+          $._lastCallback = null;
+        entry.callback.call$0();
+      }
+    },
+    _startMicrotaskLoop() {
+      $._isInCallbackLoop = true;
+      try {
+        A._microtaskLoop();
+      } finally {
+        $._lastPriorityCallback = null;
+        $._isInCallbackLoop = false;
+        if ($._nextCallback != null)
+          $.$get$_AsyncRun__scheduleImmediateClosure().call$1(A.async___startMicrotaskLoop$closure());
+      }
+    },
+    _scheduleAsyncCallback(callback) {
+      var newEntry = new A._AsyncCallbackEntry(callback),
+        lastCallback = $._lastCallback;
+      if (lastCallback == null) {
+        $._nextCallback = $._lastCallback = newEntry;
+        if (!$._isInCallbackLoop)
+          $.$get$_AsyncRun__scheduleImmediateClosure().call$1(A.async___startMicrotaskLoop$closure());
+      } else
+        $._lastCallback = lastCallback.next = newEntry;
+    },
+    _schedulePriorityAsyncCallback(callback) {
+      var entry, lastPriorityCallback, next,
+        t1 = $._nextCallback;
+      if (t1 == null) {
+        A._scheduleAsyncCallback(callback);
+        $._lastPriorityCallback = $._lastCallback;
+        return;
+      }
+      entry = new A._AsyncCallbackEntry(callback);
+      lastPriorityCallback = $._lastPriorityCallback;
+      if (lastPriorityCallback == null) {
+        entry.next = t1;
+        $._nextCallback = $._lastPriorityCallback = entry;
+      } else {
+        next = lastPriorityCallback.next;
+        entry.next = next;
+        $._lastPriorityCallback = lastPriorityCallback.next = entry;
+        if (next == null)
+          $._lastCallback = entry;
+      }
+    },
+    scheduleMicrotask(callback) {
+      var _null = null,
+        currentZone = $.Zone__current;
+      if (B.C__RootZone === currentZone) {
+        A._rootScheduleMicrotask(_null, _null, B.C__RootZone, callback);
+        return;
+      }
+      A._rootScheduleMicrotask(_null, _null, currentZone, type$.void_Function._as(currentZone.bindCallbackGuarded$1(callback)));
+    },
+    StreamIterator_StreamIterator(stream, $T) {
+      A.checkNotNullable(stream, "stream", type$.Object);
+      return new A._StreamIterator($T._eval$1("_StreamIterator<0>"));
+    },
+    StreamController_StreamController($T) {
+      var _null = null;
+      return new A._AsyncStreamController(_null, _null, _null, _null, $T._eval$1("_AsyncStreamController<0>"));
+    },
+    _runGuarded(notificationHandler) {
+      return;
+    },
+    _BufferingStreamSubscription__registerDataHandler(zone, handleData, $T) {
+      var t1 = handleData == null ? A.async___nullDataHandler$closure() : handleData;
+      return type$.$env_1_1_void._bind$1($T)._eval$1("1(2)")._as(t1);
+    },
+    _BufferingStreamSubscription__registerErrorHandler(zone, handleError) {
+      if (handleError == null)
+        handleError = A.async___nullErrorHandler$closure();
+      if (type$.void_Function_Object_StackTrace._is(handleError))
+        return zone.registerBinaryCallback$3$1(handleError, type$.dynamic, type$.Object, type$.StackTrace);
+      if (type$.void_Function_Object._is(handleError))
+        return type$.dynamic_Function_Object._as(handleError);
+      throw A.wrapException(A.ArgumentError$("handleError callback must take either an Object (the error), or both an Object (the error) and a StackTrace.", null));
+    },
+    _nullDataHandler(value) {
+    },
+    _nullErrorHandler(error, stackTrace) {
+      A._rootHandleError(type$.Object._as(error), type$.StackTrace._as(stackTrace));
+    },
+    _nullDoneHandler() {
+    },
+    _cancelAndValue(subscription, future, value) {
+      var cancelFuture = subscription.cancel$0(),
+        t1 = $.$get$Future__nullFuture();
+      if (cancelFuture !== t1)
+        cancelFuture.whenComplete$1(new A._cancelAndValue_closure(future, value));
+      else
+        future._complete$1(value);
+    },
+    Timer_Timer(duration, callback) {
+      var t1 = $.Zone__current;
+      if (t1 === B.C__RootZone)
+        return A.Timer__createTimer(duration, type$.void_Function._as(callback));
+      return A.Timer__createTimer(duration, type$.void_Function._as(t1.bindCallbackGuarded$1(callback)));
+    },
+    _rootHandleError(error, stackTrace) {
+      A._schedulePriorityAsyncCallback(new A._rootHandleError_closure(error, stackTrace));
+    },
+    _rootRun($self, $parent, zone, f, $R) {
+      var old,
+        t1 = $.Zone__current;
+      if (t1 === zone)
+        return f.call$0();
+      $.Zone__current = zone;
+      old = t1;
+      try {
+        t1 = f.call$0();
+        return t1;
+      } finally {
+        $.Zone__current = old;
+      }
+    },
+    _rootRunUnary($self, $parent, zone, f, arg, $R, $T) {
+      var old,
+        t1 = $.Zone__current;
+      if (t1 === zone)
+        return f.call$1(arg);
+      $.Zone__current = zone;
+      old = t1;
+      try {
+        t1 = f.call$1(arg);
+        return t1;
+      } finally {
+        $.Zone__current = old;
+      }
+    },
+    _rootRunBinary($self, $parent, zone, f, arg1, arg2, $R, T1, T2) {
+      var old,
+        t1 = $.Zone__current;
+      if (t1 === zone)
+        return f.call$2(arg1, arg2);
+      $.Zone__current = zone;
+      old = t1;
+      try {
+        t1 = f.call$2(arg1, arg2);
+        return t1;
+      } finally {
+        $.Zone__current = old;
+      }
+    },
+    _rootScheduleMicrotask($self, $parent, zone, f) {
+      type$.void_Function._as(f);
+      if (B.C__RootZone !== zone)
+        f = zone.bindCallbackGuarded$1(f);
+      A._scheduleAsyncCallback(f);
+    },
+    _AsyncRun__initializeScheduleImmediate_internalCallback: function _AsyncRun__initializeScheduleImmediate_internalCallback(t0) {
+      this._box_0 = t0;
+    },
+    _AsyncRun__initializeScheduleImmediate_closure: function _AsyncRun__initializeScheduleImmediate_closure(t0, t1, t2) {
+      this._box_0 = t0;
+      this.div = t1;
+      this.span = t2;
+    },
+    _AsyncRun__scheduleImmediateJsOverride_internalCallback: function _AsyncRun__scheduleImmediateJsOverride_internalCallback(t0) {
+      this.callback = t0;
+    },
+    _AsyncRun__scheduleImmediateWithSetImmediate_internalCallback: function _AsyncRun__scheduleImmediateWithSetImmediate_internalCallback(t0) {
+      this.callback = t0;
+    },
+    _TimerImpl: function _TimerImpl() {
+      this._handle = null;
+    },
+    _TimerImpl_internalCallback: function _TimerImpl_internalCallback(t0, t1) {
+      this.$this = t0;
+      this.callback = t1;
+    },
+    _AsyncAwaitCompleter: function _AsyncAwaitCompleter(t0, t1) {
+      this._future = t0;
+      this.isSync = false;
+      this.$ti = t1;
+    },
+    _awaitOnObject_closure: function _awaitOnObject_closure(t0) {
+      this.bodyFunction = t0;
+    },
+    _awaitOnObject_closure0: function _awaitOnObject_closure0(t0) {
+      this.bodyFunction = t0;
+    },
+    _wrapJsFunctionForAsync_closure: function _wrapJsFunctionForAsync_closure(t0) {
+      this.$protected = t0;
+    },
+    AsyncError: function AsyncError(t0, t1) {
+      this.error = t0;
+      this.stackTrace = t1;
+    },
+    _Completer: function _Completer() {
+    },
+    _AsyncCompleter: function _AsyncCompleter(t0, t1) {
+      this.future = t0;
+      this.$ti = t1;
+    },
+    _SyncCompleter: function _SyncCompleter(t0, t1) {
+      this.future = t0;
+      this.$ti = t1;
+    },
+    _FutureListener: function _FutureListener(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._nextListener = null;
+      _.result = t0;
+      _.state = t1;
+      _.callback = t2;
+      _.errorCallback = t3;
+      _.$ti = t4;
+    },
+    _Future: function _Future(t0, t1) {
+      var _ = this;
+      _._state = 0;
+      _._zone = t0;
+      _._resultOrListeners = null;
+      _.$ti = t1;
+    },
+    _Future__addListener_closure: function _Future__addListener_closure(t0, t1) {
+      this.$this = t0;
+      this.listener = t1;
+    },
+    _Future__prependListeners_closure: function _Future__prependListeners_closure(t0, t1) {
+      this._box_0 = t0;
+      this.$this = t1;
+    },
+    _Future__chainForeignFuture_closure: function _Future__chainForeignFuture_closure(t0) {
+      this.$this = t0;
+    },
+    _Future__chainForeignFuture_closure0: function _Future__chainForeignFuture_closure0(t0) {
+      this.$this = t0;
+    },
+    _Future__chainForeignFuture_closure1: function _Future__chainForeignFuture_closure1(t0, t1, t2) {
+      this.$this = t0;
+      this.e = t1;
+      this.s = t2;
+    },
+    _Future__chainCoreFutureAsync_closure: function _Future__chainCoreFutureAsync_closure(t0, t1) {
+      this._box_0 = t0;
+      this.target = t1;
+    },
+    _Future__asyncCompleteWithValue_closure: function _Future__asyncCompleteWithValue_closure(t0, t1) {
+      this.$this = t0;
+      this.value = t1;
+    },
+    _Future__asyncCompleteError_closure: function _Future__asyncCompleteError_closure(t0, t1, t2) {
+      this.$this = t0;
+      this.error = t1;
+      this.stackTrace = t2;
+    },
+    _Future__propagateToListeners_handleWhenCompleteCallback: function _Future__propagateToListeners_handleWhenCompleteCallback(t0, t1, t2) {
+      this._box_0 = t0;
+      this._box_1 = t1;
+      this.hasError = t2;
+    },
+    _Future__propagateToListeners_handleWhenCompleteCallback_closure: function _Future__propagateToListeners_handleWhenCompleteCallback_closure(t0) {
+      this.originalSource = t0;
+    },
+    _Future__propagateToListeners_handleValueCallback: function _Future__propagateToListeners_handleValueCallback(t0, t1) {
+      this._box_0 = t0;
+      this.sourceResult = t1;
+    },
+    _Future__propagateToListeners_handleError: function _Future__propagateToListeners_handleError(t0, t1) {
+      this._box_1 = t0;
+      this._box_0 = t1;
+    },
+    _AsyncCallbackEntry: function _AsyncCallbackEntry(t0) {
+      this.callback = t0;
+      this.next = null;
+    },
+    Stream: function Stream() {
+    },
+    Stream_length_closure: function Stream_length_closure(t0, t1) {
+      this._box_0 = t0;
+      this.$this = t1;
+    },
+    Stream_length_closure0: function Stream_length_closure0(t0, t1) {
+      this._box_0 = t0;
+      this.future = t1;
+    },
+    Stream_first_closure: function Stream_first_closure(t0) {
+      this.future = t0;
+    },
+    Stream_first_closure0: function Stream_first_closure0(t0, t1, t2) {
+      this.$this = t0;
+      this.subscription = t1;
+      this.future = t2;
+    },
+    _StreamController: function _StreamController() {
+    },
+    _StreamController__subscribe_closure: function _StreamController__subscribe_closure(t0) {
+      this.$this = t0;
+    },
+    _StreamController__recordCancel_complete: function _StreamController__recordCancel_complete(t0) {
+      this.$this = t0;
+    },
+    _AsyncStreamControllerDispatch: function _AsyncStreamControllerDispatch() {
+    },
+    _AsyncStreamController: function _AsyncStreamController(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._varData = null;
+      _._state = 0;
+      _._doneFuture = null;
+      _.onListen = t0;
+      _.onPause = t1;
+      _.onResume = t2;
+      _.onCancel = t3;
+      _.$ti = t4;
+    },
+    _ControllerStream: function _ControllerStream(t0, t1) {
+      this._controller = t0;
+      this.$ti = t1;
+    },
+    _ControllerSubscription: function _ControllerSubscription(t0, t1, t2, t3, t4, t5, t6) {
+      var _ = this;
+      _._controller = t0;
+      _._onData = t1;
+      _._onError = t2;
+      _._onDone = t3;
+      _._zone = t4;
+      _._state = t5;
+      _._pending = _._cancelFuture = null;
+      _.$ti = t6;
+    },
+    _StreamSinkWrapper: function _StreamSinkWrapper(t0, t1) {
+      this._async$_target = t0;
+      this.$ti = t1;
+    },
+    _BufferingStreamSubscription: function _BufferingStreamSubscription() {
+    },
+    _BufferingStreamSubscription_asFuture_closure: function _BufferingStreamSubscription_asFuture_closure(t0, t1) {
+      this._box_0 = t0;
+      this.result = t1;
+    },
+    _BufferingStreamSubscription_asFuture_closure0: function _BufferingStreamSubscription_asFuture_closure0(t0, t1) {
+      this.$this = t0;
+      this.result = t1;
+    },
+    _BufferingStreamSubscription_asFuture__closure: function _BufferingStreamSubscription_asFuture__closure(t0, t1, t2) {
+      this.result = t0;
+      this.error = t1;
+      this.stackTrace = t2;
+    },
+    _BufferingStreamSubscription__sendError_sendError: function _BufferingStreamSubscription__sendError_sendError(t0, t1, t2) {
+      this.$this = t0;
+      this.error = t1;
+      this.stackTrace = t2;
+    },
+    _BufferingStreamSubscription__sendDone_sendDone: function _BufferingStreamSubscription__sendDone_sendDone(t0) {
+      this.$this = t0;
+    },
+    _StreamImpl: function _StreamImpl() {
+    },
+    _DelayedEvent: function _DelayedEvent() {
+    },
+    _DelayedData: function _DelayedData(t0, t1) {
+      this.value = t0;
+      this.next = null;
+      this.$ti = t1;
+    },
+    _DelayedError: function _DelayedError(t0, t1) {
+      this.error = t0;
+      this.stackTrace = t1;
+      this.next = null;
+    },
+    _DelayedDone: function _DelayedDone() {
+    },
+    _PendingEvents: function _PendingEvents(t0) {
+      var _ = this;
+      _._state = 0;
+      _.lastPendingEvent = _.firstPendingEvent = null;
+      _.$ti = t0;
+    },
+    _PendingEvents_schedule_closure: function _PendingEvents_schedule_closure(t0, t1) {
+      this.$this = t0;
+      this.dispatch = t1;
+    },
+    _StreamIterator: function _StreamIterator(t0) {
+      this.$ti = t0;
+    },
+    _cancelAndValue_closure: function _cancelAndValue_closure(t0, t1) {
+      this.future = t0;
+      this.value = t1;
+    },
+    _Zone: function _Zone() {
+    },
+    _rootHandleError_closure: function _rootHandleError_closure(t0, t1) {
+      this.error = t0;
+      this.stackTrace = t1;
+    },
+    _RootZone: function _RootZone() {
+    },
+    _RootZone_bindCallbackGuarded_closure: function _RootZone_bindCallbackGuarded_closure(t0, t1) {
+      this.$this = t0;
+      this.f = t1;
+    },
+    _RootZone_bindUnaryCallbackGuarded_closure: function _RootZone_bindUnaryCallbackGuarded_closure(t0, t1, t2) {
+      this.$this = t0;
+      this.f = t1;
+      this.T = t2;
+    },
+    _HashMap__getTableEntry(table, key) {
+      var entry = table[key];
+      return entry === table ? null : entry;
+    },
+    _HashMap__setTableEntry(table, key, value) {
+      if (value == null)
+        table[key] = table;
+      else
+        table[key] = value;
+    },
+    _HashMap__newHashTable() {
+      var table = Object.create(null);
+      A._HashMap__setTableEntry(table, "<non-identifier-key>", table);
+      delete table["<non-identifier-key>"];
+      return table;
+    },
+    LinkedHashMap_LinkedHashMap$_empty($K, $V) {
+      return new A.JsLinkedHashMap($K._eval$1("@<0>")._bind$1($V)._eval$1("JsLinkedHashMap<1,2>"));
+    },
+    MapBase_mapToString(m) {
+      var result, t1 = {};
+      if (A.isToStringVisiting(m))
+        return "{...}";
+      result = new A.StringBuffer("");
+      try {
+        B.JSArray_methods.add$1($.toStringVisiting, m);
+        result._contents += "{";
+        t1.first = true;
+        m.forEach$1(0, new A.MapBase_mapToString_closure(t1, result));
+        result._contents += "}";
+      } finally {
+        if (0 >= $.toStringVisiting.length)
+          return A.ioore($.toStringVisiting, -1);
+        $.toStringVisiting.pop();
+      }
+      t1 = result._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    ListQueue$($E) {
+      return new A.ListQueue(A.List_List$filled(A.ListQueue__calculateCapacity(null), null, false, $E._eval$1("0?")), $E._eval$1("ListQueue<0>"));
+    },
+    ListQueue__calculateCapacity(initialCapacity) {
+      return 8;
+    },
+    _HashMap: function _HashMap() {
+    },
+    _IdentityHashMap: function _IdentityHashMap(t0) {
+      var _ = this;
+      _._collection$_length = 0;
+      _._collection$_keys = _._collection$_rest = _._collection$_nums = _._collection$_strings = null;
+      _.$ti = t0;
+    },
+    _HashMapKeyIterable: function _HashMapKeyIterable(t0, t1) {
+      this._collection$_map = t0;
+      this.$ti = t1;
+    },
+    _HashMapKeyIterator: function _HashMapKeyIterator(t0, t1, t2) {
+      var _ = this;
+      _._collection$_map = t0;
+      _._collection$_keys = t1;
+      _._offset = 0;
+      _._collection$_current = null;
+      _.$ti = t2;
+    },
+    ListBase: function ListBase() {
+    },
+    MapBase: function MapBase() {
+    },
+    MapBase_mapToString_closure: function MapBase_mapToString_closure(t0, t1) {
+      this._box_0 = t0;
+      this.result = t1;
+    },
+    _UnmodifiableMapMixin: function _UnmodifiableMapMixin() {
+    },
+    MapView: function MapView() {
+    },
+    UnmodifiableMapView: function UnmodifiableMapView() {
+    },
+    ListQueue: function ListQueue(t0, t1) {
+      var _ = this;
+      _._table = t0;
+      _._modificationCount = _._tail = _._head = 0;
+      _.$ti = t1;
+    },
+    _ListQueueIterator: function _ListQueueIterator(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._queue = t0;
+      _._end = t1;
+      _._modificationCount = t2;
+      _._position = t3;
+      _._collection$_current = null;
+      _.$ti = t4;
+    },
+    _UnmodifiableMapView_MapView__UnmodifiableMapMixin: function _UnmodifiableMapView_MapView__UnmodifiableMapMixin() {
+    },
+    _parseJson(source, reviver) {
+      var e, exception, t1, parsed = null;
+      try {
+        parsed = JSON.parse(source);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        t1 = A.FormatException$(String(e), null, null);
+        throw A.wrapException(t1);
+      }
+      t1 = A._convertJsonToDartLazy(parsed);
+      return t1;
+    },
+    _convertJsonToDartLazy(object) {
+      var i;
+      if (object == null)
+        return null;
+      if (typeof object != "object")
+        return object;
+      if (Object.getPrototypeOf(object) !== Array.prototype)
+        return new A._JsonMap(object, Object.create(null));
+      for (i = 0; i < object.length; ++i)
+        object[i] = A._convertJsonToDartLazy(object[i]);
+      return object;
+    },
+    JsonUnsupportedObjectError$(unsupportedObject, cause, partialResult) {
+      return new A.JsonUnsupportedObjectError(unsupportedObject, cause);
+    },
+    _defaultToEncodable(object) {
+      return object.toJson$0();
+    },
+    _JsonStringStringifier$(_sink, _toEncodable) {
+      return new A._JsonStringStringifier(_sink, [], A.convert___defaultToEncodable$closure());
+    },
+    _JsonStringStringifier_stringify(object, toEncodable, indent) {
+      var t1,
+        output = new A.StringBuffer(""),
+        stringifier = A._JsonStringStringifier$(output, toEncodable);
+      stringifier.writeObject$1(object);
+      t1 = output._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    _JsonMap: function _JsonMap(t0, t1) {
+      this._original = t0;
+      this._processed = t1;
+      this._data = null;
+    },
+    _JsonMapKeyIterable: function _JsonMapKeyIterable(t0) {
+      this._parent = t0;
+    },
+    Codec: function Codec() {
+    },
+    Converter: function Converter() {
+    },
+    JsonUnsupportedObjectError: function JsonUnsupportedObjectError(t0, t1) {
+      this.unsupportedObject = t0;
+      this.cause = t1;
+    },
+    JsonCyclicError: function JsonCyclicError(t0, t1) {
+      this.unsupportedObject = t0;
+      this.cause = t1;
+    },
+    JsonCodec: function JsonCodec() {
+    },
+    JsonEncoder: function JsonEncoder(t0) {
+      this._toEncodable = t0;
+    },
+    JsonDecoder: function JsonDecoder(t0) {
+      this._reviver = t0;
+    },
+    _JsonStringifier: function _JsonStringifier() {
+    },
+    _JsonStringifier_writeMap_closure: function _JsonStringifier_writeMap_closure(t0, t1) {
+      this._box_0 = t0;
+      this.keyValueList = t1;
+    },
+    _JsonStringStringifier: function _JsonStringStringifier(t0, t1, t2) {
+      this._sink = t0;
+      this._seen = t1;
+      this._toEncodable = t2;
+    },
+    int_parse(source, radix) {
+      var value = A.Primitives_parseInt(source, radix);
+      if (value != null)
+        return value;
+      throw A.wrapException(A.FormatException$(source, null, null));
+    },
+    Error__throw(error, stackTrace) {
+      error = A.wrapException(error);
+      if (error == null)
+        error = type$.Object._as(error);
+      error.stack = stackTrace.toString$0(0);
+      throw error;
+      throw A.wrapException("unreachable");
+    },
+    List_List$filled($length, fill, growable, $E) {
+      var i,
+        result = growable ? J.JSArray_JSArray$growable($length, $E) : J.JSArray_JSArray$fixed($length, $E);
+      if ($length !== 0 && fill != null)
+        for (i = 0; i < result.length; ++i)
+          result[i] = fill;
+      return result;
+    },
+    List_List$of(elements, growable, $E) {
+      var t1 = A.List_List$_of(elements, $E);
+      return t1;
+    },
+    List_List$_of(elements, $E) {
+      var list, t1;
+      if (Array.isArray(elements))
+        return A._setArrayType(elements.slice(0), $E._eval$1("JSArray<0>"));
+      list = A._setArrayType([], $E._eval$1("JSArray<0>"));
+      for (t1 = J.get$iterator$ax(elements); t1.moveNext$0();)
+        B.JSArray_methods.add$1(list, t1.get$current());
+      return list;
+    },
+    StringBuffer__writeAll(string, objects, separator) {
+      var iterator = J.get$iterator$ax(objects);
+      if (!iterator.moveNext$0())
+        return string;
+      if (separator.length === 0) {
+        do
+          string += A.S(iterator.get$current());
+        while (iterator.moveNext$0());
+      } else {
+        string += A.S(iterator.get$current());
+        for (; iterator.moveNext$0();)
+          string = string + separator + A.S(iterator.get$current());
+      }
+      return string;
+    },
+    NoSuchMethodError_NoSuchMethodError$withInvocation(receiver, invocation) {
+      return new A.NoSuchMethodError(receiver, invocation.get$memberName(), invocation.get$positionalArguments(), invocation.get$namedArguments());
+    },
+    StackTrace_current() {
+      return A.getTraceFromException(new Error());
+    },
+    DateTime__fourDigits(n) {
+      var absN = Math.abs(n),
+        sign = n < 0 ? "-" : "";
+      if (absN >= 1000)
+        return "" + n;
+      if (absN >= 100)
+        return sign + "0" + absN;
+      if (absN >= 10)
+        return sign + "00" + absN;
+      return sign + "000" + absN;
+    },
+    DateTime__threeDigits(n) {
+      if (n >= 100)
+        return "" + n;
+      if (n >= 10)
+        return "0" + n;
+      return "00" + n;
+    },
+    DateTime__twoDigits(n) {
+      if (n >= 10)
+        return "" + n;
+      return "0" + n;
+    },
+    Error_safeToString(object) {
+      if (typeof object == "number" || A._isBool(object) || object == null)
+        return J.toString$0$(object);
+      if (typeof object == "string")
+        return JSON.stringify(object);
+      return A.Primitives_safeToString(object);
+    },
+    Error_throwWithStackTrace(error, stackTrace) {
+      A.checkNotNullable(error, "error", type$.Object);
+      A.checkNotNullable(stackTrace, "stackTrace", type$.StackTrace);
+      A.Error__throw(error, stackTrace);
+    },
+    AssertionError$(message) {
+      return new A.AssertionError(message);
+    },
+    ArgumentError$(message, $name) {
+      return new A.ArgumentError(false, null, $name, message);
+    },
+    ArgumentError$value(value, $name, message) {
+      return new A.ArgumentError(true, value, $name, message);
+    },
+    ArgumentError$notNull($name) {
+      return new A.ArgumentError(false, null, $name, "Must not be null");
+    },
+    RangeError$(message) {
+      var _null = null;
+      return new A.RangeError(_null, _null, false, _null, _null, message);
+    },
+    RangeError$value(value, $name) {
+      return new A.RangeError(null, null, true, value, $name, "Value not in range");
+    },
+    RangeError$range(invalidValue, minValue, maxValue, $name, message) {
+      return new A.RangeError(minValue, maxValue, true, invalidValue, $name, "Invalid value");
+    },
+    RangeError_checkValidRange(start, end, $length) {
+      if (0 > start || start > $length)
+        throw A.wrapException(A.RangeError$range(start, 0, $length, "start", null));
+      if (end != null) {
+        if (start > end || end > $length)
+          throw A.wrapException(A.RangeError$range(end, start, $length, "end", null));
+        return end;
+      }
+      return $length;
+    },
+    RangeError_checkNotNegative(value, $name) {
+      if (value < 0)
+        throw A.wrapException(A.RangeError$range(value, 0, null, $name, null));
+      return value;
+    },
+    IndexError$withLength(invalidValue, $length, indexable, message, $name) {
+      return new A.IndexError($length, true, invalidValue, $name, "Index out of range");
+    },
+    UnsupportedError$(message) {
+      return new A.UnsupportedError(message);
+    },
+    UnimplementedError$(message) {
+      return new A.UnimplementedError(message);
+    },
+    StateError$(message) {
+      return new A.StateError(message);
+    },
+    ConcurrentModificationError$(modifiedObject) {
+      return new A.ConcurrentModificationError(modifiedObject);
+    },
+    FormatException$(message, source, offset) {
+      return new A.FormatException(message, source, offset);
+    },
+    Iterable_iterableToShortString(iterable, leftDelimiter, rightDelimiter) {
+      var parts, t1;
+      if (A.isToStringVisiting(iterable)) {
+        if (leftDelimiter === "(" && rightDelimiter === ")")
+          return "(...)";
+        return leftDelimiter + "..." + rightDelimiter;
+      }
+      parts = A._setArrayType([], type$.JSArray_String);
+      B.JSArray_methods.add$1($.toStringVisiting, iterable);
+      try {
+        A._iterablePartsToStrings(iterable, parts);
+      } finally {
+        if (0 >= $.toStringVisiting.length)
+          return A.ioore($.toStringVisiting, -1);
+        $.toStringVisiting.pop();
+      }
+      t1 = A.StringBuffer__writeAll(leftDelimiter, type$.Iterable_dynamic._as(parts), ", ") + rightDelimiter;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    Iterable_iterableToFullString(iterable, leftDelimiter, rightDelimiter) {
+      var buffer, t1;
+      if (A.isToStringVisiting(iterable))
+        return leftDelimiter + "..." + rightDelimiter;
+      buffer = new A.StringBuffer(leftDelimiter);
+      B.JSArray_methods.add$1($.toStringVisiting, iterable);
+      try {
+        t1 = buffer;
+        t1._contents = A.StringBuffer__writeAll(t1._contents, iterable, ", ");
+      } finally {
+        if (0 >= $.toStringVisiting.length)
+          return A.ioore($.toStringVisiting, -1);
+        $.toStringVisiting.pop();
+      }
+      buffer._contents += rightDelimiter;
+      t1 = buffer._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    _iterablePartsToStrings(iterable, parts) {
+      var next, ultimateString, penultimateString, penultimate, ultimate, ultimate0, elision,
+        it = iterable.get$iterator(iterable),
+        $length = 0, count = 0;
+      while (true) {
+        if (!($length < 80 || count < 3))
+          break;
+        if (!it.moveNext$0())
+          return;
+        next = A.S(it.get$current());
+        B.JSArray_methods.add$1(parts, next);
+        $length += next.length + 2;
+        ++count;
+      }
+      if (!it.moveNext$0()) {
+        if (count <= 5)
+          return;
+        if (0 >= parts.length)
+          return A.ioore(parts, -1);
+        ultimateString = parts.pop();
+        if (0 >= parts.length)
+          return A.ioore(parts, -1);
+        penultimateString = parts.pop();
+      } else {
+        penultimate = it.get$current();
+        ++count;
+        if (!it.moveNext$0()) {
+          if (count <= 4) {
+            B.JSArray_methods.add$1(parts, A.S(penultimate));
+            return;
+          }
+          ultimateString = A.S(penultimate);
+          if (0 >= parts.length)
+            return A.ioore(parts, -1);
+          penultimateString = parts.pop();
+          $length += ultimateString.length + 2;
+        } else {
+          ultimate = it.get$current();
+          ++count;
+          for (; it.moveNext$0(); penultimate = ultimate, ultimate = ultimate0) {
+            ultimate0 = it.get$current();
+            ++count;
+            if (count > 100) {
+              while (true) {
+                if (!($length > 75 && count > 3))
+                  break;
+                if (0 >= parts.length)
+                  return A.ioore(parts, -1);
+                $length -= parts.pop().length + 2;
+                --count;
+              }
+              B.JSArray_methods.add$1(parts, "...");
+              return;
+            }
+          }
+          penultimateString = A.S(penultimate);
+          ultimateString = A.S(ultimate);
+          $length += ultimateString.length + penultimateString.length + 4;
+        }
+      }
+      if (count > parts.length + 2) {
+        $length += 5;
+        elision = "...";
+      } else
+        elision = null;
+      while (true) {
+        if (!($length > 80 && parts.length > 3))
+          break;
+        if (0 >= parts.length)
+          return A.ioore(parts, -1);
+        $length -= parts.pop().length + 2;
+        if (elision == null) {
+          $length += 5;
+          elision = "...";
+        }
+      }
+      if (elision != null)
+        B.JSArray_methods.add$1(parts, elision);
+      B.JSArray_methods.add$1(parts, penultimateString);
+      B.JSArray_methods.add$1(parts, ultimateString);
+    },
+    NoSuchMethodError_toString_closure: function NoSuchMethodError_toString_closure(t0, t1) {
+      this._box_0 = t0;
+      this.sb = t1;
+    },
+    DateTime: function DateTime(t0, t1) {
+      this._value = t0;
+      this.isUtc = t1;
+    },
+    Duration: function Duration(t0) {
+      this._duration = t0;
+    },
+    Error: function Error() {
+    },
+    AssertionError: function AssertionError(t0) {
+      this.message = t0;
+    },
+    TypeError: function TypeError() {
+    },
+    ArgumentError: function ArgumentError(t0, t1, t2, t3) {
+      var _ = this;
+      _._hasValue = t0;
+      _.invalidValue = t1;
+      _.name = t2;
+      _.message = t3;
+    },
+    RangeError: function RangeError(t0, t1, t2, t3, t4, t5) {
+      var _ = this;
+      _.start = t0;
+      _.end = t1;
+      _._hasValue = t2;
+      _.invalidValue = t3;
+      _.name = t4;
+      _.message = t5;
+    },
+    IndexError: function IndexError(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _.length = t0;
+      _._hasValue = t1;
+      _.invalidValue = t2;
+      _.name = t3;
+      _.message = t4;
+    },
+    NoSuchMethodError: function NoSuchMethodError(t0, t1, t2, t3) {
+      var _ = this;
+      _._core$_receiver = t0;
+      _._core$_memberName = t1;
+      _._core$_arguments = t2;
+      _._namedArguments = t3;
+    },
+    UnsupportedError: function UnsupportedError(t0) {
+      this.message = t0;
+    },
+    UnimplementedError: function UnimplementedError(t0) {
+      this.message = t0;
+    },
+    StateError: function StateError(t0) {
+      this.message = t0;
+    },
+    ConcurrentModificationError: function ConcurrentModificationError(t0) {
+      this.modifiedObject = t0;
+    },
+    OutOfMemoryError: function OutOfMemoryError() {
+    },
+    StackOverflowError: function StackOverflowError() {
+    },
+    _Exception: function _Exception(t0) {
+      this.message = t0;
+    },
+    FormatException: function FormatException(t0, t1, t2) {
+      this.message = t0;
+      this.source = t1;
+      this.offset = t2;
+    },
+    Iterable: function Iterable() {
+    },
+    Null: function Null() {
+    },
+    Object: function Object() {
+    },
+    _StringStackTrace: function _StringStackTrace(t0) {
+      this._stackTrace = t0;
+    },
+    StringBuffer: function StringBuffer(t0) {
+      this._contents = t0;
+    },
+    _convertDartFunctionFast(f) {
+      var ret,
+        existing = f.$dart_jsFunction;
+      if (existing != null)
+        return existing;
+      ret = function(_call, f) {
+        return function() {
+          return _call(f, Array.prototype.slice.apply(arguments));
+        };
+      }(A._callDartFunctionFast, f);
+      ret[$.$get$DART_CLOSURE_PROPERTY_NAME()] = f;
+      f.$dart_jsFunction = ret;
+      return ret;
+    },
+    _callDartFunctionFast(callback, $arguments) {
+      type$.List_dynamic._as($arguments);
+      type$.Function._as(callback);
+      return A.Primitives_applyFunction(callback, $arguments, null);
+    },
+    allowInterop(f, $F) {
+      if (typeof f == "function")
+        return f;
+      else
+        return $F._as(A._convertDartFunctionFast(f));
+    },
+    promiseToFuture(jsPromise, $T) {
+      var t1 = new A._Future($.Zone__current, $T._eval$1("_Future<0>")),
+        completer = new A._AsyncCompleter(t1, $T._eval$1("_AsyncCompleter<0>"));
+      jsPromise.then(A.convertDartClosureToJS(new A.promiseToFuture_closure(completer, $T), 1), A.convertDartClosureToJS(new A.promiseToFuture_closure0(completer), 1));
+      return t1;
+    },
+    _noDartifyRequired(o) {
+      return o == null || typeof o === "boolean" || typeof o === "number" || typeof o === "string" || o instanceof Int8Array || o instanceof Uint8Array || o instanceof Uint8ClampedArray || o instanceof Int16Array || o instanceof Uint16Array || o instanceof Int32Array || o instanceof Uint32Array || o instanceof Float32Array || o instanceof Float64Array || o instanceof ArrayBuffer || o instanceof DataView;
+    },
+    dartify(o) {
+      if (A._noDartifyRequired(o))
+        return o;
+      return new A.dartify_convert(new A._IdentityHashMap(type$._IdentityHashMap_of_nullable_Object_and_nullable_Object)).call$1(o);
+    },
+    promiseToFuture_closure: function promiseToFuture_closure(t0, t1) {
+      this.completer = t0;
+      this.T = t1;
+    },
+    promiseToFuture_closure0: function promiseToFuture_closure0(t0) {
+      this.completer = t0;
+    },
+    dartify_convert: function dartify_convert(t0) {
+      this._convertedObjects = t0;
+    },
+    NullRejectionException: function NullRejectionException(t0) {
+      this.isUndefined = t0;
+    },
+    _JSRandom: function _JSRandom() {
+    },
+    AsyncMemoizer: function AsyncMemoizer(t0, t1) {
+      this._completer = t0;
+      this.$ti = t1;
+    },
+    Level: function Level(t0, t1) {
+      this.name = t0;
+      this.value = t1;
+    },
+    LogRecord: function LogRecord(t0, t1, t2) {
+      this.level = t0;
+      this.message = t1;
+      this.loggerName = t2;
+    },
+    Logger_Logger($name) {
+      return $.Logger__loggers.putIfAbsent$2($name, new A.Logger_Logger_closure($name));
+    },
+    Logger: function Logger(t0, t1, t2) {
+      var _ = this;
+      _.name = t0;
+      _.parent = t1;
+      _._level = null;
+      _._children = t2;
+    },
+    Logger_Logger_closure: function Logger_Logger_closure(t0) {
+      this.name = t0;
+    },
+    Pool: function Pool(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._requestedResources = t0;
+      _._onReleaseCallbacks = t1;
+      _._onReleaseCompleters = t2;
+      _._maxAllocatedResources = t3;
+      _._allocatedResources = 0;
+      _._timer = null;
+      _._closeMemo = t4;
+    },
+    Pool__runOnRelease_closure: function Pool__runOnRelease_closure(t0) {
+      this.$this = t0;
+    },
+    Pool__runOnRelease_closure0: function Pool__runOnRelease_closure0(t0) {
+      this.$this = t0;
+    },
+    PoolResource: function PoolResource(t0) {
+      this._pool = t0;
+      this._released = false;
+    },
+    SseClient$(serverUrl) {
+      var t3, t4, t5,
+        t1 = type$.String,
+        t2 = A.StreamController_StreamController(t1);
+      t1 = A.StreamController_StreamController(t1);
+      t3 = A.Logger_Logger("SseClient");
+      t4 = $.Zone__current;
+      t5 = A.generateUuidV4();
+      t1 = new A.SseClient(t5, t2, t1, t3, new A._AsyncCompleter(new A._Future(t4, type$._Future_void), type$._AsyncCompleter_void));
+      t1.SseClient$2$debugKey(serverUrl, null);
+      return t1;
+    },
+    SseClient: function SseClient(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._clientId = t0;
+      _._incomingController = t1;
+      _._outgoingController = t2;
+      _._logger = t3;
+      _._onConnected = t4;
+      _._lastMessageId = -1;
+      _.__SseClient__serverUrl_A = _.__SseClient__eventSource_A = $;
+      _._errorTimer = null;
+    },
+    SseClient_closure: function SseClient_closure(t0) {
+      this.$this = t0;
+    },
+    SseClient_closure0: function SseClient_closure0(t0) {
+      this.$this = t0;
+    },
+    SseClient_closure1: function SseClient_closure1(t0) {
+      this.$this = t0;
+    },
+    SseClient__closure: function SseClient__closure(t0, t1) {
+      this.$this = t0;
+      this.error = t1;
+    },
+    SseClient__onOutgoingMessage_closure: function SseClient__onOutgoingMessage_closure(t0, t1, t2) {
+      this._box_0 = t0;
+      this.$this = t1;
+      this.message = t2;
+    },
+    generateUuidV4() {
+      var t1 = new A.generateUuidV4_printDigits(),
+        t2 = new A.generateUuidV4_bitsDigits(t1, new A.generateUuidV4_generateBits(B.C__JSRandom)),
+        t3 = B.C__JSRandom.nextInt$1(4);
+      return A.S(t2.call$2(16, 4)) + A.S(t2.call$2(16, 4)) + "-" + A.S(t2.call$2(16, 4)) + "-4" + A.S(t2.call$2(12, 3)) + "-" + A.S(t1.call$2(8 + t3, 1)) + A.S(t2.call$2(12, 3)) + "-" + A.S(t2.call$2(16, 4)) + A.S(t2.call$2(16, 4)) + A.S(t2.call$2(16, 4));
+    },
+    generateUuidV4_generateBits: function generateUuidV4_generateBits(t0) {
+      this.random = t0;
+    },
+    generateUuidV4_printDigits: function generateUuidV4_printDigits() {
+    },
+    generateUuidV4_bitsDigits: function generateUuidV4_bitsDigits(t0, t1) {
+      this.printDigits = t0;
+      this.generateBits = t1;
+    },
+    StreamChannelMixin: function StreamChannelMixin() {
+    },
+    _EventStreamSubscription$(_target, _eventType, onData, _useCapture, $T) {
+      var t1;
+      if (onData == null)
+        t1 = null;
+      else {
+        t1 = A._wrapZone(new A._EventStreamSubscription_closure(onData), type$.JSObject);
+        t1 = t1 == null ? null : type$.JavaScriptFunction._as(A.allowInterop(t1, type$.Function));
+      }
+      t1 = new A._EventStreamSubscription(_target, _eventType, t1, false, $T._eval$1("_EventStreamSubscription<0>"));
+      t1._tryResume$0();
+      return t1;
+    },
+    _wrapZone(callback, $T) {
+      var t1 = $.Zone__current;
+      if (t1 === B.C__RootZone)
+        return callback;
+      return t1.bindUnaryCallbackGuarded$1$1(callback, $T);
+    },
+    EventStreamProvider: function EventStreamProvider(t0, t1) {
+      this._eventType = t0;
+      this.$ti = t1;
+    },
+    _EventStream: function _EventStream(t0, t1, t2, t3) {
+      var _ = this;
+      _._target = t0;
+      _._eventType = t1;
+      _._useCapture = t2;
+      _.$ti = t3;
+    },
+    _ElementEventStreamImpl: function _ElementEventStreamImpl(t0, t1, t2, t3) {
+      var _ = this;
+      _._target = t0;
+      _._eventType = t1;
+      _._useCapture = t2;
+      _.$ti = t3;
+    },
+    _EventStreamSubscription: function _EventStreamSubscription(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._target = t0;
+      _._eventType = t1;
+      _._streams$_onData = t2;
+      _._useCapture = t3;
+      _.$ti = t4;
+    },
+    _EventStreamSubscription_closure: function _EventStreamSubscription_closure(t0) {
+      this.onData = t0;
+    },
+    _EventStreamSubscription_onData_closure: function _EventStreamSubscription_onData_closure(t0) {
+      this.handleData = t0;
+    },
+    main() {
+      var t2,
+        channel = A.SseClient$("/test"),
+        t1 = type$.nullable_JSObject._as(type$.JSObject._as(self.document).querySelector("button"));
+      t1.toString;
+      t2 = type$._ElementEventStreamImpl_JSObject;
+      A._EventStreamSubscription$(t1, "click", t2._eval$1("~(1)?")._as(new A.main_closure(channel)), false, t2._precomputed1);
+      t2 = channel._incomingController;
+      new A._ControllerStream(t2, A._instanceType(t2)._eval$1("_ControllerStream<1>")).listen$1(new A.main_closure0(channel));
+    },
+    main_closure: function main_closure(t0) {
+      this.channel = t0;
+    },
+    main_closure0: function main_closure0(t0) {
+      this.channel = t0;
+    },
+    throwLateFieldNI(fieldName) {
+      A.throwExpressionWithWrapper(new A.LateError("Field '" + fieldName + "' has not been initialized."), new Error());
+    },
+    throwLateFieldADI(fieldName) {
+      A.throwExpressionWithWrapper(new A.LateError("Field '" + fieldName + "' has been assigned during initialization."), new Error());
+    }
+  },
+  B = {};
+  var holders = [A, J, B];
+  var $ = {};
+  A.JS_CONST.prototype = {};
+  J.Interceptor.prototype = {
+    $eq(receiver, other) {
+      return receiver === other;
+    },
+    get$hashCode(receiver) {
+      return A.Primitives_objectHashCode(receiver);
+    },
+    toString$0(receiver) {
+      return "Instance of '" + A.Primitives_objectTypeName(receiver) + "'";
+    },
+    noSuchMethod$1(receiver, invocation) {
+      throw A.wrapException(A.NoSuchMethodError_NoSuchMethodError$withInvocation(receiver, type$.Invocation._as(invocation)));
+    },
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(A._instanceTypeFromConstructor(this));
+    }
+  };
+  J.JSBool.prototype = {
+    toString$0(receiver) {
+      return String(receiver);
+    },
+    get$hashCode(receiver) {
+      return receiver ? 519018 : 218159;
+    },
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.bool);
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isbool: 1
+  };
+  J.JSNull.prototype = {
+    $eq(receiver, other) {
+      return null == other;
+    },
+    toString$0(receiver) {
+      return "null";
+    },
+    get$hashCode(receiver) {
+      return 0;
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isNull: 1
+  };
+  J.JavaScriptObject.prototype = {$isJSObject: 1};
+  J.LegacyJavaScriptObject.prototype = {
+    get$hashCode(receiver) {
+      return 0;
+    },
+    toString$0(receiver) {
+      return String(receiver);
+    }
+  };
+  J.PlainJavaScriptObject.prototype = {};
+  J.UnknownJavaScriptObject.prototype = {};
+  J.JavaScriptFunction.prototype = {
+    toString$0(receiver) {
+      var dartClosure = receiver[$.$get$DART_CLOSURE_PROPERTY_NAME()];
+      if (dartClosure == null)
+        return this.super$LegacyJavaScriptObject$toString(receiver);
+      return "JavaScript function for " + J.toString$0$(dartClosure);
+    },
+    $isFunction: 1
+  };
+  J.JavaScriptBigInt.prototype = {
+    get$hashCode(receiver) {
+      return 0;
+    },
+    toString$0(receiver) {
+      return String(receiver);
+    }
+  };
+  J.JavaScriptSymbol.prototype = {
+    get$hashCode(receiver) {
+      return 0;
+    },
+    toString$0(receiver) {
+      return String(receiver);
+    }
+  };
+  J.JSArray.prototype = {
+    add$1(receiver, value) {
+      A._arrayInstanceType(receiver)._precomputed1._as(value);
+      if (!!receiver.fixed$length)
+        A.throwExpression(A.UnsupportedError$("add"));
+      receiver.push(value);
+    },
+    addAll$1(receiver, collection) {
+      var t1;
+      A._arrayInstanceType(receiver)._eval$1("Iterable<1>")._as(collection);
+      if (!!receiver.fixed$length)
+        A.throwExpression(A.UnsupportedError$("addAll"));
+      if (Array.isArray(collection)) {
+        this._addAllFromArray$1(receiver, collection);
+        return;
+      }
+      for (t1 = J.get$iterator$ax(collection); t1.moveNext$0();)
+        receiver.push(t1.get$current());
+    },
+    _addAllFromArray$1(receiver, array) {
+      var len, i;
+      type$.JSArray_dynamic._as(array);
+      len = array.length;
+      if (len === 0)
+        return;
+      if (receiver === array)
+        throw A.wrapException(A.ConcurrentModificationError$(receiver));
+      for (i = 0; i < len; ++i)
+        receiver.push(array[i]);
+    },
+    get$last(receiver) {
+      var t1 = receiver.length;
+      if (t1 > 0)
+        return receiver[t1 - 1];
+      throw A.wrapException(A.IterableElementError_noElement());
+    },
+    setRange$4(receiver, start, end, iterable, skipCount) {
+      var $length, otherList, t1, i;
+      A._arrayInstanceType(receiver)._eval$1("Iterable<1>")._as(iterable);
+      if (!!receiver.immutable$list)
+        A.throwExpression(A.UnsupportedError$("setRange"));
+      A.RangeError_checkValidRange(start, end, receiver.length);
+      $length = end - start;
+      if ($length === 0)
+        return;
+      A.RangeError_checkNotNegative(skipCount, "skipCount");
+      otherList = iterable;
+      t1 = J.getInterceptor$asx(otherList);
+      if (skipCount + $length > t1.get$length(otherList))
+        throw A.wrapException(A.IterableElementError_tooFew());
+      if (skipCount < start)
+        for (i = $length - 1; i >= 0; --i)
+          receiver[start + i] = t1.$index(otherList, skipCount + i);
+      else
+        for (i = 0; i < $length; ++i)
+          receiver[start + i] = t1.$index(otherList, skipCount + i);
+    },
+    get$isNotEmpty(receiver) {
+      return receiver.length !== 0;
+    },
+    toString$0(receiver) {
+      return A.Iterable_iterableToFullString(receiver, "[", "]");
+    },
+    get$iterator(receiver) {
+      return new J.ArrayIterator(receiver, receiver.length, A._arrayInstanceType(receiver)._eval$1("ArrayIterator<1>"));
+    },
+    get$hashCode(receiver) {
+      return A.Primitives_objectHashCode(receiver);
+    },
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $index(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        throw A.wrapException(A.diagnoseIndexError(receiver, index));
+      return receiver[index];
+    },
+    $indexSet(receiver, index, value) {
+      A._arrayInstanceType(receiver)._precomputed1._as(value);
+      if (!!receiver.immutable$list)
+        A.throwExpression(A.UnsupportedError$("indexed set"));
+      if (!(index >= 0 && index < receiver.length))
+        throw A.wrapException(A.diagnoseIndexError(receiver, index));
+      receiver[index] = value;
+    },
+    $isIterable: 1,
+    $isList: 1
+  };
+  J.JSUnmodifiableArray.prototype = {};
+  J.ArrayIterator.prototype = {
+    get$current() {
+      var t1 = this._current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    moveNext$0() {
+      var t2, _this = this,
+        t1 = _this._iterable,
+        $length = t1.length;
+      if (_this._length !== $length) {
+        t1 = A.throwConcurrentModificationError(t1);
+        throw A.wrapException(t1);
+      }
+      t2 = _this._index;
+      if (t2 >= $length) {
+        _this.set$_current(null);
+        return false;
+      }
+      _this.set$_current(t1[t2]);
+      ++_this._index;
+      return true;
+    },
+    set$_current(_current) {
+      this._current = this.$ti._eval$1("1?")._as(_current);
+    }
+  };
+  J.JSNumber.prototype = {
+    toRadixString$1(receiver, radix) {
+      var result, t1, t2, match, exponent;
+      if (radix < 2 || radix > 36)
+        throw A.wrapException(A.RangeError$range(radix, 2, 36, "radix", null));
+      result = receiver.toString(radix);
+      t1 = result.length;
+      t2 = t1 - 1;
+      if (!(t2 >= 0))
+        return A.ioore(result, t2);
+      if (result.charCodeAt(t2) !== 41)
+        return result;
+      match = /^([\da-z]+)(?:\.([\da-z]+))?\(e\+(\d+)\)$/.exec(result);
+      if (match == null)
+        A.throwExpression(A.UnsupportedError$("Unexpected toString result: " + result));
+      t1 = match.length;
+      if (1 >= t1)
+        return A.ioore(match, 1);
+      result = match[1];
+      if (3 >= t1)
+        return A.ioore(match, 3);
+      exponent = +match[3];
+      t1 = match[2];
+      if (t1 != null) {
+        result += t1;
+        exponent -= t1.length;
+      }
+      return result + B.JSString_methods.$mul("0", exponent);
+    },
+    toString$0(receiver) {
+      if (receiver === 0 && 1 / receiver < 0)
+        return "-0.0";
+      else
+        return "" + receiver;
+    },
+    get$hashCode(receiver) {
+      var absolute, floorLog2, factor, scaled,
+        intValue = receiver | 0;
+      if (receiver === intValue)
+        return intValue & 536870911;
+      absolute = Math.abs(receiver);
+      floorLog2 = Math.log(absolute) / 0.6931471805599453 | 0;
+      factor = Math.pow(2, floorLog2);
+      scaled = absolute < 1 ? absolute / factor : factor / absolute;
+      return ((scaled * 9007199254740992 | 0) + (scaled * 3542243181176521 | 0)) * 599197 + floorLog2 * 1259 & 536870911;
+    },
+    _tdivFast$1(receiver, other) {
+      return (receiver | 0) === receiver ? receiver / other | 0 : this._tdivSlow$1(receiver, other);
+    },
+    _tdivSlow$1(receiver, other) {
+      var quotient = receiver / other;
+      if (quotient >= -2147483648 && quotient <= 2147483647)
+        return quotient | 0;
+      if (quotient > 0) {
+        if (quotient !== 1 / 0)
+          return Math.floor(quotient);
+      } else if (quotient > -1 / 0)
+        return Math.ceil(quotient);
+      throw A.wrapException(A.UnsupportedError$("Result of truncating division is " + A.S(quotient) + ": " + A.S(receiver) + " ~/ " + other));
+    },
+    _shlPositive$1(receiver, other) {
+      return other > 31 ? 0 : receiver << other >>> 0;
+    },
+    _shrOtherPositive$1(receiver, other) {
+      var t1;
+      if (receiver > 0)
+        t1 = this._shrBothPositive$1(receiver, other);
+      else {
+        t1 = other > 31 ? 31 : other;
+        t1 = receiver >> t1 >>> 0;
+      }
+      return t1;
+    },
+    _shrBothPositive$1(receiver, other) {
+      return other > 31 ? 0 : receiver >>> other;
+    },
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.num);
+    },
+    $isdouble: 1,
+    $isnum: 1
+  };
+  J.JSInt.prototype = {
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.int);
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isint: 1
+  };
+  J.JSNumNotInt.prototype = {
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.double);
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  J.JSString.prototype = {
+    matchAsPrefix$2(receiver, string, start) {
+      var t1, t2, i, t3, _null = null;
+      if (start < 0 || start > string.length)
+        throw A.wrapException(A.RangeError$range(start, 0, string.length, _null, _null));
+      t1 = receiver.length;
+      t2 = string.length;
+      if (start + t1 > t2)
+        return _null;
+      for (i = 0; i < t1; ++i) {
+        t3 = start + i;
+        if (!(t3 >= 0 && t3 < t2))
+          return A.ioore(string, t3);
+        if (string.charCodeAt(t3) !== receiver.charCodeAt(i))
+          return _null;
+      }
+      return new A.StringMatch(start, receiver);
+    },
+    $add(receiver, other) {
+      return receiver + other;
+    },
+    endsWith$1(receiver, other) {
+      var otherLength = other.length,
+        t1 = receiver.length;
+      if (otherLength > t1)
+        return false;
+      return other === this.substring$1(receiver, t1 - otherLength);
+    },
+    startsWith$2(receiver, pattern, index) {
+      var endIndex;
+      if (index < 0 || index > receiver.length)
+        throw A.wrapException(A.RangeError$range(index, 0, receiver.length, null, null));
+      if (typeof pattern == "string") {
+        endIndex = index + pattern.length;
+        if (endIndex > receiver.length)
+          return false;
+        return pattern === receiver.substring(index, endIndex);
+      }
+      return J.matchAsPrefix$2$s(pattern, receiver, index) != null;
+    },
+    startsWith$1(receiver, pattern) {
+      return this.startsWith$2(receiver, pattern, 0);
+    },
+    substring$2(receiver, start, end) {
+      return receiver.substring(start, A.RangeError_checkValidRange(start, end, receiver.length));
+    },
+    substring$1(receiver, start) {
+      return this.substring$2(receiver, start, null);
+    },
+    $mul(receiver, times) {
+      var s, result;
+      if (0 >= times)
+        return "";
+      if (times === 1 || receiver.length === 0)
+        return receiver;
+      if (times !== times >>> 0)
+        throw A.wrapException(B.C_OutOfMemoryError);
+      for (s = receiver, result = ""; true;) {
+        if ((times & 1) === 1)
+          result = s + result;
+        times = times >>> 1;
+        if (times === 0)
+          break;
+        s += s;
+      }
+      return result;
+    },
+    padLeft$2(receiver, width, padding) {
+      var delta = width - receiver.length;
+      if (delta <= 0)
+        return receiver;
+      return this.$mul(padding, delta) + receiver;
+    },
+    lastIndexOf$2(receiver, pattern, start) {
+      var t1, t2;
+      if (start == null)
+        start = receiver.length;
+      else if (start < 0 || start > receiver.length)
+        throw A.wrapException(A.RangeError$range(start, 0, receiver.length, null, null));
+      t1 = pattern.length;
+      t2 = receiver.length;
+      if (start + t1 > t2)
+        start = t2 - t1;
+      return receiver.lastIndexOf(pattern, start);
+    },
+    lastIndexOf$1(receiver, pattern) {
+      return this.lastIndexOf$2(receiver, pattern, null);
+    },
+    toString$0(receiver) {
+      return receiver;
+    },
+    get$hashCode(receiver) {
+      var t1, hash, i;
+      for (t1 = receiver.length, hash = 0, i = 0; i < t1; ++i) {
+        hash = hash + receiver.charCodeAt(i) & 536870911;
+        hash = hash + ((hash & 524287) << 10) & 536870911;
+        hash ^= hash >> 6;
+      }
+      hash = hash + ((hash & 67108863) << 3) & 536870911;
+      hash ^= hash >> 11;
+      return hash + ((hash & 16383) << 15) & 536870911;
+    },
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.String);
+    },
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isPattern: 1,
+    $isString: 1
+  };
+  A.LateError.prototype = {
+    toString$0(_) {
+      return "LateInitializationError: " + this._message;
+    }
+  };
+  A.nullFuture_closure.prototype = {
+    call$0() {
+      return A.Future_Future$value(null, type$.Null);
+    },
+    $signature: 7
+  };
+  A.EfficientLengthIterable.prototype = {};
+  A.ListIterable.prototype = {
+    get$iterator(_) {
+      var _this = this;
+      return new A.ListIterator(_this, _this.get$length(_this), A._instanceType(_this)._eval$1("ListIterator<ListIterable.E>"));
+    },
+    get$isEmpty(_) {
+      return this.get$length(this) === 0;
+    }
+  };
+  A.ListIterator.prototype = {
+    get$current() {
+      var t1 = this.__internal$_current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    moveNext$0() {
+      var t3, _this = this,
+        t1 = _this.__internal$_iterable,
+        t2 = J.getInterceptor$asx(t1),
+        $length = t2.get$length(t1);
+      if (_this.__internal$_length !== $length)
+        throw A.wrapException(A.ConcurrentModificationError$(t1));
+      t3 = _this.__internal$_index;
+      if (t3 >= $length) {
+        _this.set$__internal$_current(null);
+        return false;
+      }
+      _this.set$__internal$_current(t2.elementAt$1(t1, t3));
+      ++_this.__internal$_index;
+      return true;
+    },
+    set$__internal$_current(_current) {
+      this.__internal$_current = this.$ti._eval$1("1?")._as(_current);
+    }
+  };
+  A.FixedLengthListMixin.prototype = {};
+  A.Symbol.prototype = {
+    get$hashCode(_) {
+      var hash = this._hashCode;
+      if (hash != null)
+        return hash;
+      hash = 664597 * B.JSString_methods.get$hashCode(this._name) & 536870911;
+      this._hashCode = hash;
+      return hash;
+    },
+    toString$0(_) {
+      return 'Symbol("' + this._name + '")';
+    },
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      return other instanceof A.Symbol && this._name === other._name;
+    },
+    $isSymbol0: 1
+  };
+  A.ConstantMapView.prototype = {};
+  A.ConstantMap.prototype = {
+    get$isEmpty(_) {
+      return this.get$length(this) === 0;
+    },
+    toString$0(_) {
+      return A.MapBase_mapToString(this);
+    },
+    $isMap: 1
+  };
+  A.ConstantStringMap.prototype = {
+    get$length(_) {
+      return this._values.length;
+    },
+    get$_keys() {
+      var keys = this.$keys;
+      if (keys == null) {
+        keys = Object.keys(this._jsIndex);
+        this.$keys = keys;
+      }
+      return keys;
+    },
+    forEach$1(_, f) {
+      var keys, values, t1, i;
+      this.$ti._eval$1("~(1,2)")._as(f);
+      keys = this.get$_keys();
+      values = this._values;
+      for (t1 = keys.length, i = 0; i < t1; ++i)
+        f.call$2(keys[i], values[i]);
+    }
+  };
+  A.JSInvocationMirror.prototype = {
+    get$memberName() {
+      var t1 = this._memberName;
+      if (t1 instanceof A.Symbol)
+        return t1;
+      return this._memberName = new A.Symbol(A._asString(t1));
+    },
+    get$positionalArguments() {
+      var t1, t2, argumentCount, list, index, _this = this;
+      if (_this.__js_helper$_kind === 1)
+        return B.List_empty;
+      t1 = _this._arguments;
+      t2 = J.getInterceptor$asx(t1);
+      argumentCount = t2.get$length(t1) - J.get$length$asx(_this._namedArgumentNames) - _this._typeArgumentCount;
+      if (argumentCount === 0)
+        return B.List_empty;
+      list = [];
+      for (index = 0; index < argumentCount; ++index)
+        list.push(t2.$index(t1, index));
+      return J.JSArray_markUnmodifiableList(list);
+    },
+    get$namedArguments() {
+      var t1, t2, namedArgumentCount, t3, t4, namedArgumentsStartIndex, map, i, _this = this;
+      if (_this.__js_helper$_kind !== 0)
+        return B.Map_empty;
+      t1 = _this._namedArgumentNames;
+      t2 = J.getInterceptor$asx(t1);
+      namedArgumentCount = t2.get$length(t1);
+      t3 = _this._arguments;
+      t4 = J.getInterceptor$asx(t3);
+      namedArgumentsStartIndex = t4.get$length(t3) - namedArgumentCount - _this._typeArgumentCount;
+      if (namedArgumentCount === 0)
+        return B.Map_empty;
+      map = new A.JsLinkedHashMap(type$.JsLinkedHashMap_Symbol_dynamic);
+      for (i = 0; i < namedArgumentCount; ++i)
+        map.$indexSet(0, new A.Symbol(A._asString(t2.$index(t1, i))), t4.$index(t3, namedArgumentsStartIndex + i));
+      return new A.ConstantMapView(map, type$.ConstantMapView_Symbol_dynamic);
+    },
+    $isInvocation: 1
+  };
+  A.Primitives_functionNoSuchMethod_closure.prototype = {
+    call$2($name, argument) {
+      var t1;
+      A._asString($name);
+      t1 = this._box_0;
+      t1.names = t1.names + "$" + $name;
+      B.JSArray_methods.add$1(this.namedArgumentList, $name);
+      B.JSArray_methods.add$1(this.$arguments, argument);
+      ++t1.argumentCount;
+    },
+    $signature: 12
+  };
+  A.TypeErrorDecoder.prototype = {
+    matchTypeError$1(message) {
+      var result, t1, _this = this,
+        match = new RegExp(_this._pattern).exec(message);
+      if (match == null)
+        return null;
+      result = Object.create(null);
+      t1 = _this._arguments;
+      if (t1 !== -1)
+        result.arguments = match[t1 + 1];
+      t1 = _this._argumentsExpr;
+      if (t1 !== -1)
+        result.argumentsExpr = match[t1 + 1];
+      t1 = _this._expr;
+      if (t1 !== -1)
+        result.expr = match[t1 + 1];
+      t1 = _this._method;
+      if (t1 !== -1)
+        result.method = match[t1 + 1];
+      t1 = _this._receiver;
+      if (t1 !== -1)
+        result.receiver = match[t1 + 1];
+      return result;
+    }
+  };
+  A.NullError.prototype = {
+    toString$0(_) {
+      return "Null check operator used on a null value";
+    }
+  };
+  A.JsNoSuchMethodError.prototype = {
+    toString$0(_) {
+      var t2, _this = this,
+        _s38_ = "NoSuchMethodError: method not found: '",
+        t1 = _this._method;
+      if (t1 == null)
+        return "NoSuchMethodError: " + _this.__js_helper$_message;
+      t2 = _this._receiver;
+      if (t2 == null)
+        return _s38_ + t1 + "' (" + _this.__js_helper$_message + ")";
+      return _s38_ + t1 + "' on '" + t2 + "' (" + _this.__js_helper$_message + ")";
+    }
+  };
+  A.UnknownJsTypeError.prototype = {
+    toString$0(_) {
+      var t1 = this.__js_helper$_message;
+      return t1.length === 0 ? "Error" : "Error: " + t1;
+    }
+  };
+  A.NullThrownFromJavaScriptException.prototype = {
+    toString$0(_) {
+      return "Throw of null ('" + (this._irritant === null ? "null" : "undefined") + "' from JavaScript)";
+    }
+  };
+  A.ExceptionAndStackTrace.prototype = {};
+  A._StackTrace.prototype = {
+    toString$0(_) {
+      var trace,
+        t1 = this._trace;
+      if (t1 != null)
+        return t1;
+      t1 = this._exception;
+      trace = t1 !== null && typeof t1 === "object" ? t1.stack : null;
+      return this._trace = trace == null ? "" : trace;
+    },
+    $isStackTrace: 1
+  };
+  A.Closure.prototype = {
+    toString$0(_) {
+      var $constructor = this.constructor,
+        $name = $constructor == null ? null : $constructor.name;
+      return "Closure '" + A.unminifyOrTag($name == null ? "unknown" : $name) + "'";
+    },
+    $isFunction: 1,
+    get$$call() {
+      return this;
+    },
+    "call*": "call$1",
+    $requiredArgCount: 1,
+    $defaultValues: null
+  };
+  A.Closure0Args.prototype = {"call*": "call$0", $requiredArgCount: 0};
+  A.Closure2Args.prototype = {"call*": "call$2", $requiredArgCount: 2};
+  A.TearOffClosure.prototype = {};
+  A.StaticClosure.prototype = {
+    toString$0(_) {
+      var $name = this.$static_name;
+      if ($name == null)
+        return "Closure of unknown static method";
+      return "Closure '" + A.unminifyOrTag($name) + "'";
+    }
+  };
+  A.BoundClosure.prototype = {
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      if (this === other)
+        return true;
+      if (!(other instanceof A.BoundClosure))
+        return false;
+      return this.$_target === other.$_target && this._receiver === other._receiver;
+    },
+    get$hashCode(_) {
+      return (A.objectHashCode(this._receiver) ^ A.Primitives_objectHashCode(this.$_target)) >>> 0;
+    },
+    toString$0(_) {
+      return "Closure '" + this.$_name + "' of " + ("Instance of '" + A.Primitives_objectTypeName(this._receiver) + "'");
+    }
+  };
+  A._CyclicInitializationError.prototype = {
+    toString$0(_) {
+      return "Reading static variable '" + this.variableName + "' during its initialization";
+    }
+  };
+  A.RuntimeError.prototype = {
+    toString$0(_) {
+      return "RuntimeError: " + this.message;
+    }
+  };
+  A._Required.prototype = {};
+  A.JsLinkedHashMap.prototype = {
+    get$length(_) {
+      return this.__js_helper$_length;
+    },
+    get$isEmpty(_) {
+      return this.__js_helper$_length === 0;
+    },
+    get$keys() {
+      return new A.LinkedHashMapKeyIterable(this, A._instanceType(this)._eval$1("LinkedHashMapKeyIterable<1>"));
+    },
+    containsKey$1(key) {
+      var strings = this._strings;
+      if (strings == null)
+        return false;
+      return strings[key] != null;
+    },
+    $index(_, key) {
+      var strings, cell, t1, nums, _null = null;
+      if (typeof key == "string") {
+        strings = this._strings;
+        if (strings == null)
+          return _null;
+        cell = strings[key];
+        t1 = cell == null ? _null : cell.hashMapCellValue;
+        return t1;
+      } else if (typeof key == "number" && (key & 0x3fffffff) === key) {
+        nums = this._nums;
+        if (nums == null)
+          return _null;
+        cell = nums[key];
+        t1 = cell == null ? _null : cell.hashMapCellValue;
+        return t1;
+      } else
+        return this.internalGet$1(key);
+    },
+    internalGet$1(key) {
+      var bucket, index,
+        rest = this.__js_helper$_rest;
+      if (rest == null)
+        return null;
+      bucket = rest[this.internalComputeHashCode$1(key)];
+      index = this.internalFindBucketIndex$2(bucket, key);
+      if (index < 0)
+        return null;
+      return bucket[index].hashMapCellValue;
+    },
+    $indexSet(_, key, value) {
+      var strings, nums, rest, hash, bucket, index, _this = this,
+        t1 = A._instanceType(_this);
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      if (typeof key == "string") {
+        strings = _this._strings;
+        _this._addHashTableEntry$3(strings == null ? _this._strings = _this._newHashTable$0() : strings, key, value);
+      } else if (typeof key == "number" && (key & 0x3fffffff) === key) {
+        nums = _this._nums;
+        _this._addHashTableEntry$3(nums == null ? _this._nums = _this._newHashTable$0() : nums, key, value);
+      } else {
+        rest = _this.__js_helper$_rest;
+        if (rest == null)
+          rest = _this.__js_helper$_rest = _this._newHashTable$0();
+        hash = _this.internalComputeHashCode$1(key);
+        bucket = rest[hash];
+        if (bucket == null)
+          rest[hash] = [_this._newLinkedCell$2(key, value)];
+        else {
+          index = _this.internalFindBucketIndex$2(bucket, key);
+          if (index >= 0)
+            bucket[index].hashMapCellValue = value;
+          else
+            bucket.push(_this._newLinkedCell$2(key, value));
+        }
+      }
+    },
+    putIfAbsent$2(key, ifAbsent) {
+      var t2, value, _this = this,
+        t1 = A._instanceType(_this);
+      t1._precomputed1._as(key);
+      t1._eval$1("2()")._as(ifAbsent);
+      if (_this.containsKey$1(key)) {
+        t2 = _this.$index(0, key);
+        return t2 == null ? t1._rest[1]._as(t2) : t2;
+      }
+      value = ifAbsent.call$0();
+      _this.$indexSet(0, key, value);
+      return value;
+    },
+    forEach$1(_, action) {
+      var cell, modifications, _this = this;
+      A._instanceType(_this)._eval$1("~(1,2)")._as(action);
+      cell = _this._first;
+      modifications = _this._modifications;
+      for (; cell != null;) {
+        action.call$2(cell.hashMapCellKey, cell.hashMapCellValue);
+        if (modifications !== _this._modifications)
+          throw A.wrapException(A.ConcurrentModificationError$(_this));
+        cell = cell._next;
+      }
+    },
+    _addHashTableEntry$3(table, key, value) {
+      var cell,
+        t1 = A._instanceType(this);
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      cell = table[key];
+      if (cell == null)
+        table[key] = this._newLinkedCell$2(key, value);
+      else
+        cell.hashMapCellValue = value;
+    },
+    _newLinkedCell$2(key, value) {
+      var _this = this,
+        t1 = A._instanceType(_this),
+        cell = new A.LinkedHashMapCell(t1._precomputed1._as(key), t1._rest[1]._as(value));
+      if (_this._first == null)
+        _this._first = _this._last = cell;
+      else
+        _this._last = _this._last._next = cell;
+      ++_this.__js_helper$_length;
+      _this._modifications = _this._modifications + 1 & 1073741823;
+      return cell;
+    },
+    internalComputeHashCode$1(key) {
+      return J.get$hashCode$(key) & 1073741823;
+    },
+    internalFindBucketIndex$2(bucket, key) {
+      var $length, i;
+      if (bucket == null)
+        return -1;
+      $length = bucket.length;
+      for (i = 0; i < $length; ++i)
+        if (J.$eq$(bucket[i].hashMapCellKey, key))
+          return i;
+      return -1;
+    },
+    toString$0(_) {
+      return A.MapBase_mapToString(this);
+    },
+    _newHashTable$0() {
+      var table = Object.create(null);
+      table["<non-identifier-key>"] = table;
+      delete table["<non-identifier-key>"];
+      return table;
+    }
+  };
+  A.LinkedHashMapCell.prototype = {};
+  A.LinkedHashMapKeyIterable.prototype = {
+    get$length(_) {
+      return this._map.__js_helper$_length;
+    },
+    get$isEmpty(_) {
+      return this._map.__js_helper$_length === 0;
+    },
+    get$iterator(_) {
+      var t1 = this._map,
+        t2 = new A.LinkedHashMapKeyIterator(t1, t1._modifications, this.$ti._eval$1("LinkedHashMapKeyIterator<1>"));
+      t2._cell = t1._first;
+      return t2;
+    }
+  };
+  A.LinkedHashMapKeyIterator.prototype = {
+    get$current() {
+      return this.__js_helper$_current;
+    },
+    moveNext$0() {
+      var cell, _this = this,
+        t1 = _this._map;
+      if (_this._modifications !== t1._modifications)
+        throw A.wrapException(A.ConcurrentModificationError$(t1));
+      cell = _this._cell;
+      if (cell == null) {
+        _this.set$__js_helper$_current(null);
+        return false;
+      } else {
+        _this.set$__js_helper$_current(cell.hashMapCellKey);
+        _this._cell = cell._next;
+        return true;
+      }
+    },
+    set$__js_helper$_current(_current) {
+      this.__js_helper$_current = this.$ti._eval$1("1?")._as(_current);
+    }
+  };
+  A.initHooks_closure.prototype = {
+    call$1(o) {
+      return this.getTag(o);
+    },
+    $signature: 8
+  };
+  A.initHooks_closure0.prototype = {
+    call$2(o, tag) {
+      return this.getUnknownTag(o, tag);
+    },
+    $signature: 13
+  };
+  A.initHooks_closure1.prototype = {
+    call$1(tag) {
+      return this.prototypeForTag(A._asString(tag));
+    },
+    $signature: 14
+  };
+  A.StringMatch.prototype = {};
+  A.NativeByteBuffer.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_ByteBuffer_RkP;
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeTypedData.prototype = {};
+  A.NativeByteData.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_ByteData_zNC;
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeTypedArray.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $isJavaScriptIndexingBehavior: 1
+  };
+  A.NativeTypedArrayOfDouble.prototype = {
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.NativeTypedArrayOfInt.prototype = {$isIterable: 1, $isList: 1};
+  A.NativeFloat32List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Float32List_LB7;
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeFloat64List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Float64List_LB7;
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeInt16List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Int16List_uXf;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeInt32List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Int32List_O50;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeInt8List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Int8List_ekJ;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeUint16List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Uint16List_2bx;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeUint32List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Uint32List_2bx;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeUint8ClampedList.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Uint8ClampedList_Jik;
+    },
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A.NativeUint8List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Uint8List_WLA;
+    },
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin.prototype = {};
+  A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin.prototype = {};
+  A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin.prototype = {};
+  A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin.prototype = {};
+  A.Rti.prototype = {
+    _eval$1(recipe) {
+      return A._Universe_evalInEnvironment(init.typeUniverse, this, recipe);
+    },
+    _bind$1(typeOrTuple) {
+      return A._Universe_bind(init.typeUniverse, this, typeOrTuple);
+    }
+  };
+  A._FunctionParameters.prototype = {};
+  A._Type.prototype = {
+    toString$0(_) {
+      return A._rtiToString(this._rti, null);
+    }
+  };
+  A._Error.prototype = {
+    toString$0(_) {
+      return this.__rti$_message;
+    }
+  };
+  A._TypeError.prototype = {$isTypeError: 1};
+  A._AsyncRun__initializeScheduleImmediate_internalCallback.prototype = {
+    call$1(_) {
+      var t1 = this._box_0,
+        f = t1.storedCallback;
+      t1.storedCallback = null;
+      f.call$0();
+    },
+    $signature: 4
+  };
+  A._AsyncRun__initializeScheduleImmediate_closure.prototype = {
+    call$1(callback) {
+      var t1, t2;
+      this._box_0.storedCallback = type$.void_Function._as(callback);
+      t1 = this.div;
+      t2 = this.span;
+      t1.firstChild ? t1.removeChild(t2) : t1.appendChild(t2);
+    },
+    $signature: 15
+  };
+  A._AsyncRun__scheduleImmediateJsOverride_internalCallback.prototype = {
+    call$0() {
+      this.callback.call$0();
+    },
+    $signature: 2
+  };
+  A._AsyncRun__scheduleImmediateWithSetImmediate_internalCallback.prototype = {
+    call$0() {
+      this.callback.call$0();
+    },
+    $signature: 2
+  };
+  A._TimerImpl.prototype = {
+    _TimerImpl$2(milliseconds, callback) {
+      if (self.setTimeout != null)
+        this._handle = self.setTimeout(A.convertDartClosureToJS(new A._TimerImpl_internalCallback(this, callback), 0), milliseconds);
+      else
+        throw A.wrapException(A.UnsupportedError$("`setTimeout()` not found."));
+    },
+    cancel$0() {
+      if (self.setTimeout != null) {
+        var t1 = this._handle;
+        if (t1 == null)
+          return;
+        self.clearTimeout(t1);
+        this._handle = null;
+      } else
+        throw A.wrapException(A.UnsupportedError$("Canceling a timer."));
+    },
+    $isTimer: 1
+  };
+  A._TimerImpl_internalCallback.prototype = {
+    call$0() {
+      this.$this._handle = null;
+      this.callback.call$0();
+    },
+    $signature: 0
+  };
+  A._AsyncAwaitCompleter.prototype = {
+    complete$1(value) {
+      var t2, _this = this,
+        t1 = _this.$ti;
+      t1._eval$1("1/?")._as(value);
+      if (value == null)
+        value = t1._precomputed1._as(value);
+      if (!_this.isSync)
+        _this._future._asyncComplete$1(value);
+      else {
+        t2 = _this._future;
+        if (t1._eval$1("Future<1>")._is(value))
+          t2._chainFuture$1(value);
+        else
+          t2._completeWithValue$1(value);
+      }
+    },
+    completeError$2(e, st) {
+      var t1 = this._future;
+      if (this.isSync)
+        t1._completeError$2(e, st);
+      else
+        t1._asyncCompleteError$2(e, st);
+    },
+    $isCompleter: 1
+  };
+  A._awaitOnObject_closure.prototype = {
+    call$1(result) {
+      return this.bodyFunction.call$2(0, result);
+    },
+    $signature: 3
+  };
+  A._awaitOnObject_closure0.prototype = {
+    call$2(error, stackTrace) {
+      this.bodyFunction.call$2(1, new A.ExceptionAndStackTrace(error, type$.StackTrace._as(stackTrace)));
+    },
+    $signature: 16
+  };
+  A._wrapJsFunctionForAsync_closure.prototype = {
+    call$2(errorCode, result) {
+      this.$protected(A._asInt(errorCode), result);
+    },
+    $signature: 17
+  };
+  A.AsyncError.prototype = {
+    toString$0(_) {
+      return A.S(this.error);
+    },
+    $isError: 1,
+    get$stackTrace() {
+      return this.stackTrace;
+    }
+  };
+  A._Completer.prototype = {
+    completeError$2(error, stackTrace) {
+      A.checkNotNullable(error, "error", type$.Object);
+      if ((this.future._state & 30) !== 0)
+        throw A.wrapException(A.StateError$("Future already completed"));
+      if (stackTrace == null)
+        stackTrace = A.AsyncError_defaultStackTrace(error);
+      this._completeError$2(error, stackTrace);
+    },
+    completeError$1(error) {
+      return this.completeError$2(error, null);
+    },
+    $isCompleter: 1
+  };
+  A._AsyncCompleter.prototype = {
+    complete$1(value) {
+      var t2,
+        t1 = this.$ti;
+      t1._eval$1("1/?")._as(value);
+      t2 = this.future;
+      if ((t2._state & 30) !== 0)
+        throw A.wrapException(A.StateError$("Future already completed"));
+      t2._asyncComplete$1(t1._eval$1("1/")._as(value));
+    },
+    complete$0() {
+      return this.complete$1(null);
+    },
+    _completeError$2(error, stackTrace) {
+      this.future._asyncCompleteError$2(error, stackTrace);
+    }
+  };
+  A._SyncCompleter.prototype = {
+    complete$1(value) {
+      var t2,
+        t1 = this.$ti;
+      t1._eval$1("1/?")._as(value);
+      t2 = this.future;
+      if ((t2._state & 30) !== 0)
+        throw A.wrapException(A.StateError$("Future already completed"));
+      t2._complete$1(t1._eval$1("1/")._as(value));
+    },
+    _completeError$2(error, stackTrace) {
+      this.future._completeError$2(error, stackTrace);
+    }
+  };
+  A._FutureListener.prototype = {
+    matchesErrorTest$1(asyncError) {
+      if ((this.state & 15) !== 6)
+        return true;
+      return this.result._zone.runUnary$2$2(type$.bool_Function_Object._as(this.callback), asyncError.error, type$.bool, type$.Object);
+    },
+    handleError$1(asyncError) {
+      var exception, _this = this,
+        errorCallback = _this.errorCallback,
+        result = null,
+        t1 = type$.dynamic,
+        t2 = type$.Object,
+        t3 = asyncError.error,
+        t4 = _this.result._zone;
+      if (type$.dynamic_Function_Object_StackTrace._is(errorCallback))
+        result = t4.runBinary$3$3(errorCallback, t3, asyncError.stackTrace, t1, t2, type$.StackTrace);
+      else
+        result = t4.runUnary$2$2(type$.dynamic_Function_Object._as(errorCallback), t3, t1, t2);
+      try {
+        t1 = _this.$ti._eval$1("2/")._as(result);
+        return t1;
+      } catch (exception) {
+        if (type$.TypeError._is(A.unwrapException(exception))) {
+          if ((_this.state & 1) !== 0)
+            throw A.wrapException(A.ArgumentError$("The error handler of Future.then must return a value of the returned future's type", "onError"));
+          throw A.wrapException(A.ArgumentError$("The error handler of Future.catchError must return a value of the future's type", "onError"));
+        } else
+          throw exception;
+      }
+    }
+  };
+  A._Future.prototype = {
+    _setChained$1(source) {
+      this._state = this._state & 1 | 4;
+      this._resultOrListeners = source;
+    },
+    then$1$2$onError(f, onError, $R) {
+      var currentZone, result, t2,
+        t1 = this.$ti;
+      t1._bind$1($R)._eval$1("1/(2)")._as(f);
+      currentZone = $.Zone__current;
+      if (currentZone === B.C__RootZone) {
+        if (onError != null && !type$.dynamic_Function_Object_StackTrace._is(onError) && !type$.dynamic_Function_Object._is(onError))
+          throw A.wrapException(A.ArgumentError$value(onError, "onError", string$.Error_));
+      } else {
+        $R._eval$1("@<0/>")._bind$1(t1._precomputed1)._eval$1("1(2)")._as(f);
+        if (onError != null)
+          onError = A._registerErrorHandler(onError, currentZone);
+      }
+      result = new A._Future(currentZone, $R._eval$1("_Future<0>"));
+      t2 = onError == null ? 1 : 3;
+      this._addListener$1(new A._FutureListener(result, t2, f, onError, t1._eval$1("@<1>")._bind$1($R)._eval$1("_FutureListener<1,2>")));
+      return result;
+    },
+    then$1$1(f, $R) {
+      return this.then$1$2$onError(f, null, $R);
+    },
+    _thenAwait$1$2(f, onError, $E) {
+      var result,
+        t1 = this.$ti;
+      t1._bind$1($E)._eval$1("1/(2)")._as(f);
+      result = new A._Future($.Zone__current, $E._eval$1("_Future<0>"));
+      this._addListener$1(new A._FutureListener(result, 19, f, onError, t1._eval$1("@<1>")._bind$1($E)._eval$1("_FutureListener<1,2>")));
+      return result;
+    },
+    whenComplete$1(action) {
+      var t1, result;
+      type$.dynamic_Function._as(action);
+      t1 = this.$ti;
+      result = new A._Future($.Zone__current, t1);
+      this._addListener$1(new A._FutureListener(result, 8, action, null, t1._eval$1("@<1>")._bind$1(t1._precomputed1)._eval$1("_FutureListener<1,2>")));
+      return result;
+    },
+    _setErrorObject$1(error) {
+      this._state = this._state & 1 | 16;
+      this._resultOrListeners = error;
+    },
+    _cloneResult$1(source) {
+      this._state = source._state & 30 | this._state & 1;
+      this._resultOrListeners = source._resultOrListeners;
+    },
+    _addListener$1(listener) {
+      var source, _this = this,
+        t1 = _this._state;
+      if (t1 <= 3) {
+        listener._nextListener = type$.nullable__FutureListener_dynamic_dynamic._as(_this._resultOrListeners);
+        _this._resultOrListeners = listener;
+      } else {
+        if ((t1 & 4) !== 0) {
+          source = type$._Future_dynamic._as(_this._resultOrListeners);
+          if ((source._state & 24) === 0) {
+            source._addListener$1(listener);
+            return;
+          }
+          _this._cloneResult$1(source);
+        }
+        A._rootScheduleMicrotask(null, null, _this._zone, type$.void_Function._as(new A._Future__addListener_closure(_this, listener)));
+      }
+    },
+    _prependListeners$1(listeners) {
+      var t1, existingListeners, next, cursor, next0, source, _this = this, _box_0 = {};
+      _box_0.listeners = listeners;
+      if (listeners == null)
+        return;
+      t1 = _this._state;
+      if (t1 <= 3) {
+        existingListeners = type$.nullable__FutureListener_dynamic_dynamic._as(_this._resultOrListeners);
+        _this._resultOrListeners = listeners;
+        if (existingListeners != null) {
+          next = listeners._nextListener;
+          for (cursor = listeners; next != null; cursor = next, next = next0)
+            next0 = next._nextListener;
+          cursor._nextListener = existingListeners;
+        }
+      } else {
+        if ((t1 & 4) !== 0) {
+          source = type$._Future_dynamic._as(_this._resultOrListeners);
+          if ((source._state & 24) === 0) {
+            source._prependListeners$1(listeners);
+            return;
+          }
+          _this._cloneResult$1(source);
+        }
+        _box_0.listeners = _this._reverseListeners$1(listeners);
+        A._rootScheduleMicrotask(null, null, _this._zone, type$.void_Function._as(new A._Future__prependListeners_closure(_box_0, _this)));
+      }
+    },
+    _removeListeners$0() {
+      var current = type$.nullable__FutureListener_dynamic_dynamic._as(this._resultOrListeners);
+      this._resultOrListeners = null;
+      return this._reverseListeners$1(current);
+    },
+    _reverseListeners$1(listeners) {
+      var current, prev, next;
+      for (current = listeners, prev = null; current != null; prev = current, current = next) {
+        next = current._nextListener;
+        current._nextListener = prev;
+      }
+      return prev;
+    },
+    _chainForeignFuture$1(source) {
+      var e, s, exception, _this = this;
+      _this._state ^= 2;
+      try {
+        source.then$1$2$onError(new A._Future__chainForeignFuture_closure(_this), new A._Future__chainForeignFuture_closure0(_this), type$.Null);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        A.scheduleMicrotask(new A._Future__chainForeignFuture_closure1(_this, e, s));
+      }
+    },
+    _complete$1(value) {
+      var listeners, _this = this,
+        t1 = _this.$ti;
+      t1._eval$1("1/")._as(value);
+      if (t1._eval$1("Future<1>")._is(value))
+        if (t1._is(value))
+          A._Future__chainCoreFutureSync(value, _this);
+        else
+          _this._chainForeignFuture$1(value);
+      else {
+        listeners = _this._removeListeners$0();
+        t1._precomputed1._as(value);
+        _this._state = 8;
+        _this._resultOrListeners = value;
+        A._Future__propagateToListeners(_this, listeners);
+      }
+    },
+    _completeWithValue$1(value) {
+      var listeners, _this = this;
+      _this.$ti._precomputed1._as(value);
+      listeners = _this._removeListeners$0();
+      _this._state = 8;
+      _this._resultOrListeners = value;
+      A._Future__propagateToListeners(_this, listeners);
+    },
+    _completeError$2(error, stackTrace) {
+      var listeners;
+      type$.Object._as(error);
+      type$.StackTrace._as(stackTrace);
+      listeners = this._removeListeners$0();
+      this._setErrorObject$1(A.AsyncError$(error, stackTrace));
+      A._Future__propagateToListeners(this, listeners);
+    },
+    _asyncComplete$1(value) {
+      var t1 = this.$ti;
+      t1._eval$1("1/")._as(value);
+      if (t1._eval$1("Future<1>")._is(value)) {
+        this._chainFuture$1(value);
+        return;
+      }
+      this._asyncCompleteWithValue$1(value);
+    },
+    _asyncCompleteWithValue$1(value) {
+      var _this = this;
+      _this.$ti._precomputed1._as(value);
+      _this._state ^= 2;
+      A._rootScheduleMicrotask(null, null, _this._zone, type$.void_Function._as(new A._Future__asyncCompleteWithValue_closure(_this, value)));
+    },
+    _chainFuture$1(value) {
+      var t1 = this.$ti;
+      t1._eval$1("Future<1>")._as(value);
+      if (t1._is(value)) {
+        A._Future__chainCoreFutureAsync(value, this);
+        return;
+      }
+      this._chainForeignFuture$1(value);
+    },
+    _asyncCompleteError$2(error, stackTrace) {
+      type$.StackTrace._as(stackTrace);
+      this._state ^= 2;
+      A._rootScheduleMicrotask(null, null, this._zone, type$.void_Function._as(new A._Future__asyncCompleteError_closure(this, error, stackTrace)));
+    },
+    $isFuture: 1
+  };
+  A._Future__addListener_closure.prototype = {
+    call$0() {
+      A._Future__propagateToListeners(this.$this, this.listener);
+    },
+    $signature: 0
+  };
+  A._Future__prependListeners_closure.prototype = {
+    call$0() {
+      A._Future__propagateToListeners(this.$this, this._box_0.listeners);
+    },
+    $signature: 0
+  };
+  A._Future__chainForeignFuture_closure.prototype = {
+    call$1(value) {
+      var error, stackTrace, exception,
+        t1 = this.$this;
+      t1._state ^= 2;
+      try {
+        t1._completeWithValue$1(t1.$ti._precomputed1._as(value));
+      } catch (exception) {
+        error = A.unwrapException(exception);
+        stackTrace = A.getTraceFromException(exception);
+        t1._completeError$2(error, stackTrace);
+      }
+    },
+    $signature: 4
+  };
+  A._Future__chainForeignFuture_closure0.prototype = {
+    call$2(error, stackTrace) {
+      this.$this._completeError$2(type$.Object._as(error), type$.StackTrace._as(stackTrace));
+    },
+    $signature: 5
+  };
+  A._Future__chainForeignFuture_closure1.prototype = {
+    call$0() {
+      this.$this._completeError$2(this.e, this.s);
+    },
+    $signature: 0
+  };
+  A._Future__chainCoreFutureAsync_closure.prototype = {
+    call$0() {
+      A._Future__chainCoreFutureSync(this._box_0.source, this.target);
+    },
+    $signature: 0
+  };
+  A._Future__asyncCompleteWithValue_closure.prototype = {
+    call$0() {
+      this.$this._completeWithValue$1(this.value);
+    },
+    $signature: 0
+  };
+  A._Future__asyncCompleteError_closure.prototype = {
+    call$0() {
+      this.$this._completeError$2(this.error, this.stackTrace);
+    },
+    $signature: 0
+  };
+  A._Future__propagateToListeners_handleWhenCompleteCallback.prototype = {
+    call$0() {
+      var e, s, t1, exception, t2, originalSource, _this = this, completeResult = null;
+      try {
+        t1 = _this._box_0.listener;
+        completeResult = t1.result._zone.run$1$1(type$.dynamic_Function._as(t1.callback), type$.dynamic);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        t1 = _this.hasError && type$.AsyncError._as(_this._box_1.source._resultOrListeners).error === e;
+        t2 = _this._box_0;
+        if (t1)
+          t2.listenerValueOrError = type$.AsyncError._as(_this._box_1.source._resultOrListeners);
+        else
+          t2.listenerValueOrError = A.AsyncError$(e, s);
+        t2.listenerHasError = true;
+        return;
+      }
+      if (completeResult instanceof A._Future && (completeResult._state & 24) !== 0) {
+        if ((completeResult._state & 16) !== 0) {
+          t1 = _this._box_0;
+          t1.listenerValueOrError = type$.AsyncError._as(completeResult._resultOrListeners);
+          t1.listenerHasError = true;
+        }
+        return;
+      }
+      if (completeResult instanceof A._Future) {
+        originalSource = _this._box_1.source;
+        t1 = _this._box_0;
+        t1.listenerValueOrError = completeResult.then$1$1(new A._Future__propagateToListeners_handleWhenCompleteCallback_closure(originalSource), type$.dynamic);
+        t1.listenerHasError = false;
+      }
+    },
+    $signature: 0
+  };
+  A._Future__propagateToListeners_handleWhenCompleteCallback_closure.prototype = {
+    call$1(_) {
+      return this.originalSource;
+    },
+    $signature: 18
+  };
+  A._Future__propagateToListeners_handleValueCallback.prototype = {
+    call$0() {
+      var e, s, t1, t2, t3, t4, t5, exception;
+      try {
+        t1 = this._box_0;
+        t2 = t1.listener;
+        t3 = t2.$ti;
+        t4 = t3._precomputed1;
+        t5 = t4._as(this.sourceResult);
+        t1.listenerValueOrError = t2.result._zone.runUnary$2$2(t3._eval$1("2/(1)")._as(t2.callback), t5, t3._eval$1("2/"), t4);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        t1 = this._box_0;
+        t1.listenerValueOrError = A.AsyncError$(e, s);
+        t1.listenerHasError = true;
+      }
+    },
+    $signature: 0
+  };
+  A._Future__propagateToListeners_handleError.prototype = {
+    call$0() {
+      var asyncError, e, s, t1, exception, t2, _this = this;
+      try {
+        asyncError = type$.AsyncError._as(_this._box_1.source._resultOrListeners);
+        t1 = _this._box_0;
+        if (t1.listener.matchesErrorTest$1(asyncError) && t1.listener.errorCallback != null) {
+          t1.listenerValueOrError = t1.listener.handleError$1(asyncError);
+          t1.listenerHasError = false;
+        }
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        t1 = type$.AsyncError._as(_this._box_1.source._resultOrListeners);
+        t2 = _this._box_0;
+        if (t1.error === e)
+          t2.listenerValueOrError = t1;
+        else
+          t2.listenerValueOrError = A.AsyncError$(e, s);
+        t2.listenerHasError = true;
+      }
+    },
+    $signature: 0
+  };
+  A._AsyncCallbackEntry.prototype = {};
+  A.Stream.prototype = {
+    get$length(_) {
+      var t1 = {},
+        future = new A._Future($.Zone__current, type$._Future_int);
+      t1.count = 0;
+      this.listen$4$cancelOnError$onDone$onError(new A.Stream_length_closure(t1, this), true, new A.Stream_length_closure0(t1, future), future.get$_completeError());
+      return future;
+    },
+    get$first(_) {
+      var future = new A._Future($.Zone__current, A._instanceType(this)._eval$1("_Future<1>")),
+        subscription = this.listen$4$cancelOnError$onDone$onError(null, true, new A.Stream_first_closure(future), future.get$_completeError());
+      subscription.onData$1(new A.Stream_first_closure0(this, subscription, future));
+      return future;
+    }
+  };
+  A.Stream_length_closure.prototype = {
+    call$1(_) {
+      A._instanceType(this.$this)._precomputed1._as(_);
+      ++this._box_0.count;
+    },
+    $signature() {
+      return A._instanceType(this.$this)._eval$1("~(1)");
+    }
+  };
+  A.Stream_length_closure0.prototype = {
+    call$0() {
+      this.future._complete$1(this._box_0.count);
+    },
+    $signature: 0
+  };
+  A.Stream_first_closure.prototype = {
+    call$0() {
+      var e, s, t1, exception, stackTrace;
+      try {
+        t1 = A.IterableElementError_noElement();
+        throw A.wrapException(t1);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        t1 = e;
+        stackTrace = s;
+        if (stackTrace == null)
+          stackTrace = A.AsyncError_defaultStackTrace(t1);
+        this.future._completeError$2(t1, stackTrace);
+      }
+    },
+    $signature: 0
+  };
+  A.Stream_first_closure0.prototype = {
+    call$1(value) {
+      A._cancelAndValue(this.subscription, this.future, A._instanceType(this.$this)._precomputed1._as(value));
+    },
+    $signature() {
+      return A._instanceType(this.$this)._eval$1("~(1)");
+    }
+  };
+  A._StreamController.prototype = {
+    get$_pendingEvents() {
+      var t1, _this = this;
+      if ((_this._state & 8) === 0)
+        return A._instanceType(_this)._eval$1("_PendingEvents<1>?")._as(_this._varData);
+      t1 = A._instanceType(_this);
+      return t1._eval$1("_PendingEvents<1>?")._as(t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData).get$_varData());
+    },
+    _ensurePendingEvents$0() {
+      var events, t1, _this = this;
+      if ((_this._state & 8) === 0) {
+        events = _this._varData;
+        if (events == null)
+          events = _this._varData = new A._PendingEvents(A._instanceType(_this)._eval$1("_PendingEvents<1>"));
+        return A._instanceType(_this)._eval$1("_PendingEvents<1>")._as(events);
+      }
+      t1 = A._instanceType(_this);
+      events = t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData).get$_varData();
+      return t1._eval$1("_PendingEvents<1>")._as(events);
+    },
+    get$_subscription() {
+      var varData = this._varData;
+      if ((this._state & 8) !== 0)
+        varData = type$._StreamControllerAddStreamState_nullable_Object._as(varData).get$_varData();
+      return A._instanceType(this)._eval$1("_ControllerSubscription<1>")._as(varData);
+    },
+    _badEventState$0() {
+      if ((this._state & 4) !== 0)
+        return new A.StateError("Cannot add event after closing");
+      return new A.StateError("Cannot add event while adding a stream");
+    },
+    _ensureDoneFuture$0() {
+      var t1 = this._doneFuture;
+      if (t1 == null)
+        t1 = this._doneFuture = (this._state & 2) !== 0 ? $.$get$Future__nullFuture() : new A._Future($.Zone__current, type$._Future_void);
+      return t1;
+    },
+    add$1(_, value) {
+      var t2, _this = this,
+        t1 = A._instanceType(_this);
+      t1._precomputed1._as(value);
+      t2 = _this._state;
+      if (t2 >= 4)
+        throw A.wrapException(_this._badEventState$0());
+      if ((t2 & 1) !== 0)
+        _this._sendData$1(value);
+      else if ((t2 & 3) === 0)
+        _this._ensurePendingEvents$0().add$1(0, new A._DelayedData(value, t1._eval$1("_DelayedData<1>")));
+    },
+    close$0() {
+      var _this = this,
+        t1 = _this._state;
+      if ((t1 & 4) !== 0)
+        return _this._ensureDoneFuture$0();
+      if (t1 >= 4)
+        throw A.wrapException(_this._badEventState$0());
+      t1 = _this._state = t1 | 4;
+      if ((t1 & 1) !== 0)
+        _this._sendDone$0();
+      else if ((t1 & 3) === 0)
+        _this._ensurePendingEvents$0().add$1(0, B.C__DelayedDone);
+      return _this._ensureDoneFuture$0();
+    },
+    _subscribe$4(onData, onError, onDone, cancelOnError) {
+      var t2, t3, t4, t5, t6, t7, t8, subscription, pendingEvents, addState, _this = this,
+        t1 = A._instanceType(_this);
+      t1._eval$1("~(1)?")._as(onData);
+      type$.nullable_void_Function._as(onDone);
+      if ((_this._state & 3) !== 0)
+        throw A.wrapException(A.StateError$("Stream has already been listened to."));
+      t2 = $.Zone__current;
+      t3 = cancelOnError ? 1 : 0;
+      t4 = onError != null ? 32 : 0;
+      t5 = A._BufferingStreamSubscription__registerDataHandler(t2, onData, t1._precomputed1);
+      t6 = A._BufferingStreamSubscription__registerErrorHandler(t2, onError);
+      t7 = onDone == null ? A.async___nullDoneHandler$closure() : onDone;
+      t8 = type$.void_Function;
+      subscription = new A._ControllerSubscription(_this, t5, t6, t8._as(t7), t2, t3 | t4, t1._eval$1("_ControllerSubscription<1>"));
+      pendingEvents = _this.get$_pendingEvents();
+      t4 = _this._state |= 1;
+      if ((t4 & 8) !== 0) {
+        addState = t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData);
+        addState.set$_varData(subscription);
+        addState.resume$0();
+      } else
+        _this._varData = subscription;
+      subscription._setPendingEvents$1(pendingEvents);
+      t1 = t8._as(new A._StreamController__subscribe_closure(_this));
+      t2 = subscription._state;
+      subscription._state = t2 | 64;
+      t1.call$0();
+      subscription._state &= 4294967231;
+      subscription._checkState$1((t2 & 4) !== 0);
+      return subscription;
+    },
+    _recordCancel$1(subscription) {
+      var result, onCancel, cancelResult, e, s, exception, result0, _this = this,
+        t1 = A._instanceType(_this);
+      t1._eval$1("StreamSubscription<1>")._as(subscription);
+      result = null;
+      if ((_this._state & 8) !== 0)
+        result = t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData).cancel$0();
+      _this._varData = null;
+      _this._state = _this._state & 4294967286 | 2;
+      onCancel = _this.onCancel;
+      if (onCancel != null)
+        if (result == null)
+          try {
+            cancelResult = onCancel.call$0();
+            if (cancelResult instanceof A._Future)
+              result = cancelResult;
+          } catch (exception) {
+            e = A.unwrapException(exception);
+            s = A.getTraceFromException(exception);
+            result0 = new A._Future($.Zone__current, type$._Future_void);
+            result0._asyncCompleteError$2(e, s);
+            result = result0;
+          }
+        else
+          result = result.whenComplete$1(onCancel);
+      t1 = new A._StreamController__recordCancel_complete(_this);
+      if (result != null)
+        result = result.whenComplete$1(t1);
+      else
+        t1.call$0();
+      return result;
+    },
+    $isStreamController: 1,
+    $is_StreamControllerLifecycle: 1,
+    $is_EventDispatch: 1
+  };
+  A._StreamController__subscribe_closure.prototype = {
+    call$0() {
+      A._runGuarded(this.$this.onListen);
+    },
+    $signature: 0
+  };
+  A._StreamController__recordCancel_complete.prototype = {
+    call$0() {
+      var doneFuture = this.$this._doneFuture;
+      if (doneFuture != null && (doneFuture._state & 30) === 0)
+        doneFuture._asyncComplete$1(null);
+    },
+    $signature: 0
+  };
+  A._AsyncStreamControllerDispatch.prototype = {
+    _sendData$1(data) {
+      var t1 = this.$ti;
+      t1._precomputed1._as(data);
+      this.get$_subscription()._addPending$1(new A._DelayedData(data, t1._eval$1("_DelayedData<1>")));
+    },
+    _sendError$2(error, stackTrace) {
+      this.get$_subscription()._addPending$1(new A._DelayedError(error, stackTrace));
+    },
+    _sendDone$0() {
+      this.get$_subscription()._addPending$1(B.C__DelayedDone);
+    }
+  };
+  A._AsyncStreamController.prototype = {};
+  A._ControllerStream.prototype = {
+    get$hashCode(_) {
+      return (A.Primitives_objectHashCode(this._controller) ^ 892482866) >>> 0;
+    },
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      if (this === other)
+        return true;
+      return other instanceof A._ControllerStream && other._controller === this._controller;
+    }
+  };
+  A._ControllerSubscription.prototype = {
+    _onCancel$0() {
+      return this._controller._recordCancel$1(this);
+    },
+    _onPause$0() {
+      var t1 = this._controller,
+        t2 = A._instanceType(t1);
+      t2._eval$1("StreamSubscription<1>")._as(this);
+      if ((t1._state & 8) !== 0)
+        t2._eval$1("_StreamControllerAddStreamState<1>")._as(t1._varData).pause$0();
+      A._runGuarded(t1.onPause);
+    },
+    _onResume$0() {
+      var t1 = this._controller,
+        t2 = A._instanceType(t1);
+      t2._eval$1("StreamSubscription<1>")._as(this);
+      if ((t1._state & 8) !== 0)
+        t2._eval$1("_StreamControllerAddStreamState<1>")._as(t1._varData).resume$0();
+      A._runGuarded(t1.onResume);
+    }
+  };
+  A._StreamSinkWrapper.prototype = {};
+  A._BufferingStreamSubscription.prototype = {
+    _setPendingEvents$1(pendingEvents) {
+      var _this = this;
+      A._instanceType(_this)._eval$1("_PendingEvents<1>?")._as(pendingEvents);
+      if (pendingEvents == null)
+        return;
+      _this.set$_pending(pendingEvents);
+      if (pendingEvents.lastPendingEvent != null) {
+        _this._state |= 128;
+        pendingEvents.schedule$1(_this);
+      }
+    },
+    onData$1(handleData) {
+      var t1 = A._instanceType(this);
+      this.set$_onData(A._BufferingStreamSubscription__registerDataHandler(this._zone, t1._eval$1("~(1)?")._as(handleData), t1._precomputed1));
+    },
+    cancel$0() {
+      var t1 = this._state &= 4294967279;
+      if ((t1 & 8) === 0)
+        this._cancel$0();
+      t1 = this._cancelFuture;
+      return t1 == null ? $.$get$Future__nullFuture() : t1;
+    },
+    asFuture$1$1(futureValue, $E) {
+      var result, _this = this, t1 = {};
+      t1.resultValue = null;
+      if (!$E._is(null))
+        throw A.wrapException(A.ArgumentError$notNull("futureValue"));
+      $E._as(futureValue);
+      t1.resultValue = futureValue;
+      result = new A._Future($.Zone__current, $E._eval$1("_Future<0>"));
+      _this.set$_onDone(new A._BufferingStreamSubscription_asFuture_closure(t1, result));
+      _this._state |= 32;
+      _this._onError = new A._BufferingStreamSubscription_asFuture_closure0(_this, result);
+      return result;
+    },
+    _cancel$0() {
+      var t2, _this = this,
+        t1 = _this._state |= 8;
+      if ((t1 & 128) !== 0) {
+        t2 = _this._pending;
+        if (t2._state === 1)
+          t2._state = 3;
+      }
+      if ((t1 & 64) === 0)
+        _this.set$_pending(null);
+      _this._cancelFuture = _this._onCancel$0();
+    },
+    _onPause$0() {
+    },
+    _onResume$0() {
+    },
+    _onCancel$0() {
+      return null;
+    },
+    _addPending$1($event) {
+      var t1, _this = this,
+        pending = _this._pending;
+      if (pending == null) {
+        pending = new A._PendingEvents(A._instanceType(_this)._eval$1("_PendingEvents<1>"));
+        _this.set$_pending(pending);
+      }
+      pending.add$1(0, $event);
+      t1 = _this._state;
+      if ((t1 & 128) === 0) {
+        t1 |= 128;
+        _this._state = t1;
+        if (t1 < 256)
+          pending.schedule$1(_this);
+      }
+    },
+    _sendData$1(data) {
+      var t2, _this = this,
+        t1 = A._instanceType(_this)._precomputed1;
+      t1._as(data);
+      t2 = _this._state;
+      _this._state = t2 | 64;
+      _this._zone.runUnaryGuarded$1$2(_this._onData, data, t1);
+      _this._state &= 4294967231;
+      _this._checkState$1((t2 & 4) !== 0);
+    },
+    _sendError$2(error, stackTrace) {
+      var cancelFuture, _this = this,
+        t1 = _this._state,
+        t2 = new A._BufferingStreamSubscription__sendError_sendError(_this, error, stackTrace);
+      if ((t1 & 1) !== 0) {
+        _this._state = t1 | 16;
+        _this._cancel$0();
+        cancelFuture = _this._cancelFuture;
+        if (cancelFuture != null && cancelFuture !== $.$get$Future__nullFuture())
+          cancelFuture.whenComplete$1(t2);
+        else
+          t2.call$0();
+      } else {
+        t2.call$0();
+        _this._checkState$1((t1 & 4) !== 0);
+      }
+    },
+    _sendDone$0() {
+      var cancelFuture, _this = this,
+        t1 = new A._BufferingStreamSubscription__sendDone_sendDone(_this);
+      _this._cancel$0();
+      _this._state |= 16;
+      cancelFuture = _this._cancelFuture;
+      if (cancelFuture != null && cancelFuture !== $.$get$Future__nullFuture())
+        cancelFuture.whenComplete$1(t1);
+      else
+        t1.call$0();
+    },
+    _checkState$1(wasInputPaused) {
+      var t2, isInputPaused, _this = this,
+        t1 = _this._state;
+      if ((t1 & 128) !== 0 && _this._pending.lastPendingEvent == null) {
+        t1 = _this._state = t1 & 4294967167;
+        if ((t1 & 4) !== 0)
+          if (t1 < 256) {
+            t2 = _this._pending;
+            t2 = t2 == null ? null : t2.lastPendingEvent == null;
+            t2 = t2 !== false;
+          } else
+            t2 = false;
+        else
+          t2 = false;
+        if (t2) {
+          t1 &= 4294967291;
+          _this._state = t1;
+        }
+      }
+      for (; true; wasInputPaused = isInputPaused) {
+        if ((t1 & 8) !== 0) {
+          _this.set$_pending(null);
+          return;
+        }
+        isInputPaused = (t1 & 4) !== 0;
+        if (wasInputPaused === isInputPaused)
+          break;
+        _this._state = t1 ^ 64;
+        if (isInputPaused)
+          _this._onPause$0();
+        else
+          _this._onResume$0();
+        t1 = _this._state &= 4294967231;
+      }
+      if ((t1 & 128) !== 0 && t1 < 256)
+        _this._pending.schedule$1(_this);
+    },
+    set$_onData(_onData) {
+      this._onData = A._instanceType(this)._eval$1("~(1)")._as(_onData);
+    },
+    set$_onDone(_onDone) {
+      this._onDone = type$.void_Function._as(_onDone);
+    },
+    set$_pending(_pending) {
+      this._pending = A._instanceType(this)._eval$1("_PendingEvents<1>?")._as(_pending);
+    },
+    $isStreamSubscription: 1,
+    $is_EventDispatch: 1
+  };
+  A._BufferingStreamSubscription_asFuture_closure.prototype = {
+    call$0() {
+      this.result._complete$1(this._box_0.resultValue);
+    },
+    $signature: 0
+  };
+  A._BufferingStreamSubscription_asFuture_closure0.prototype = {
+    call$2(error, stackTrace) {
+      var cancelFuture = this.$this.cancel$0(),
+        t1 = this.result;
+      if (cancelFuture !== $.$get$Future__nullFuture())
+        cancelFuture.whenComplete$1(new A._BufferingStreamSubscription_asFuture__closure(t1, error, stackTrace));
+      else
+        t1._completeError$2(error, stackTrace);
+    },
+    $signature: 5
+  };
+  A._BufferingStreamSubscription_asFuture__closure.prototype = {
+    call$0() {
+      this.result._completeError$2(this.error, this.stackTrace);
+    },
+    $signature: 2
+  };
+  A._BufferingStreamSubscription__sendError_sendError.prototype = {
+    call$0() {
+      var onError, t3, t4,
+        t1 = this.$this,
+        t2 = t1._state;
+      if ((t2 & 8) !== 0 && (t2 & 16) === 0)
+        return;
+      t1._state = t2 | 64;
+      onError = t1._onError;
+      t2 = this.error;
+      t3 = type$.Object;
+      t4 = t1._zone;
+      if (type$.void_Function_Object_StackTrace._is(onError))
+        t4.runBinaryGuarded$2$3(onError, t2, this.stackTrace, t3, type$.StackTrace);
+      else
+        t4.runUnaryGuarded$1$2(type$.void_Function_Object._as(onError), t2, t3);
+      t1._state &= 4294967231;
+    },
+    $signature: 0
+  };
+  A._BufferingStreamSubscription__sendDone_sendDone.prototype = {
+    call$0() {
+      var t1 = this.$this,
+        t2 = t1._state;
+      if ((t2 & 16) === 0)
+        return;
+      t1._state = t2 | 74;
+      t1._zone.runGuarded$1(t1._onDone);
+      t1._state &= 4294967231;
+    },
+    $signature: 0
+  };
+  A._StreamImpl.prototype = {
+    listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, onError) {
+      var t1 = this.$ti;
+      t1._eval$1("~(1)?")._as(onData);
+      type$.nullable_void_Function._as(onDone);
+      return this._controller._subscribe$4(t1._eval$1("~(1)?")._as(onData), onError, onDone, cancelOnError === true);
+    },
+    listen$1(onData) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, null, null, null);
+    },
+    listen$2$cancelOnError(onData, cancelOnError) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, cancelOnError, null, null);
+    },
+    listen$2$onDone(onData, onDone) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, null, onDone, null);
+    }
+  };
+  A._DelayedEvent.prototype = {
+    set$next(next) {
+      this.next = type$.nullable__DelayedEvent_dynamic._as(next);
+    },
+    get$next() {
+      return this.next;
+    }
+  };
+  A._DelayedData.prototype = {
+    perform$1(dispatch) {
+      this.$ti._eval$1("_EventDispatch<1>")._as(dispatch)._sendData$1(this.value);
+    }
+  };
+  A._DelayedError.prototype = {
+    perform$1(dispatch) {
+      dispatch._sendError$2(this.error, this.stackTrace);
+    }
+  };
+  A._DelayedDone.prototype = {
+    perform$1(dispatch) {
+      dispatch._sendDone$0();
+    },
+    get$next() {
+      return null;
+    },
+    set$next(_) {
+      throw A.wrapException(A.StateError$("No events after a done."));
+    },
+    $is_DelayedEvent: 1
+  };
+  A._PendingEvents.prototype = {
+    schedule$1(dispatch) {
+      var t1, _this = this;
+      _this.$ti._eval$1("_EventDispatch<1>")._as(dispatch);
+      t1 = _this._state;
+      if (t1 === 1)
+        return;
+      if (t1 >= 1) {
+        _this._state = 1;
+        return;
+      }
+      A.scheduleMicrotask(new A._PendingEvents_schedule_closure(_this, dispatch));
+      _this._state = 1;
+    },
+    add$1(_, $event) {
+      var _this = this,
+        lastEvent = _this.lastPendingEvent;
+      if (lastEvent == null)
+        _this.firstPendingEvent = _this.lastPendingEvent = $event;
+      else {
+        lastEvent.set$next($event);
+        _this.lastPendingEvent = $event;
+      }
+    }
+  };
+  A._PendingEvents_schedule_closure.prototype = {
+    call$0() {
+      var t2, $event, nextEvent,
+        t1 = this.$this,
+        oldState = t1._state;
+      t1._state = 0;
+      if (oldState === 3)
+        return;
+      t2 = t1.$ti._eval$1("_EventDispatch<1>")._as(this.dispatch);
+      $event = t1.firstPendingEvent;
+      nextEvent = $event.get$next();
+      t1.firstPendingEvent = nextEvent;
+      if (nextEvent == null)
+        t1.lastPendingEvent = null;
+      $event.perform$1(t2);
+    },
+    $signature: 0
+  };
+  A._StreamIterator.prototype = {};
+  A._cancelAndValue_closure.prototype = {
+    call$0() {
+      return this.future._complete$1(this.value);
+    },
+    $signature: 0
+  };
+  A._Zone.prototype = {$isZone: 1};
+  A._rootHandleError_closure.prototype = {
+    call$0() {
+      A.Error_throwWithStackTrace(this.error, this.stackTrace);
+    },
+    $signature: 0
+  };
+  A._RootZone.prototype = {
+    runGuarded$1(f) {
+      var e, s, exception;
+      type$.void_Function._as(f);
+      try {
+        if (B.C__RootZone === $.Zone__current) {
+          f.call$0();
+          return;
+        }
+        A._rootRun(null, null, this, f, type$.void);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        A._rootHandleError(type$.Object._as(e), type$.StackTrace._as(s));
+      }
+    },
+    runUnaryGuarded$1$2(f, arg, $T) {
+      var e, s, exception;
+      $T._eval$1("~(0)")._as(f);
+      $T._as(arg);
+      try {
+        if (B.C__RootZone === $.Zone__current) {
+          f.call$1(arg);
+          return;
+        }
+        A._rootRunUnary(null, null, this, f, arg, type$.void, $T);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        A._rootHandleError(type$.Object._as(e), type$.StackTrace._as(s));
+      }
+    },
+    runBinaryGuarded$2$3(f, arg1, arg2, T1, T2) {
+      var e, s, exception;
+      T1._eval$1("@<0>")._bind$1(T2)._eval$1("~(1,2)")._as(f);
+      T1._as(arg1);
+      T2._as(arg2);
+      try {
+        if (B.C__RootZone === $.Zone__current) {
+          f.call$2(arg1, arg2);
+          return;
+        }
+        A._rootRunBinary(null, null, this, f, arg1, arg2, type$.void, T1, T2);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        A._rootHandleError(type$.Object._as(e), type$.StackTrace._as(s));
+      }
+    },
+    bindCallbackGuarded$1(f) {
+      return new A._RootZone_bindCallbackGuarded_closure(this, type$.void_Function._as(f));
+    },
+    bindUnaryCallbackGuarded$1$1(f, $T) {
+      return new A._RootZone_bindUnaryCallbackGuarded_closure(this, $T._eval$1("~(0)")._as(f), $T);
+    },
+    run$1$1(f, $R) {
+      $R._eval$1("0()")._as(f);
+      if ($.Zone__current === B.C__RootZone)
+        return f.call$0();
+      return A._rootRun(null, null, this, f, $R);
+    },
+    runUnary$2$2(f, arg, $R, $T) {
+      $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f);
+      $T._as(arg);
+      if ($.Zone__current === B.C__RootZone)
+        return f.call$1(arg);
+      return A._rootRunUnary(null, null, this, f, arg, $R, $T);
+    },
+    runBinary$3$3(f, arg1, arg2, $R, T1, T2) {
+      $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f);
+      T1._as(arg1);
+      T2._as(arg2);
+      if ($.Zone__current === B.C__RootZone)
+        return f.call$2(arg1, arg2);
+      return A._rootRunBinary(null, null, this, f, arg1, arg2, $R, T1, T2);
+    },
+    registerBinaryCallback$3$1(f, $R, T1, T2) {
+      return $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f);
+    }
+  };
+  A._RootZone_bindCallbackGuarded_closure.prototype = {
+    call$0() {
+      return this.$this.runGuarded$1(this.f);
+    },
+    $signature: 0
+  };
+  A._RootZone_bindUnaryCallbackGuarded_closure.prototype = {
+    call$1(arg) {
+      var t1 = this.T;
+      return this.$this.runUnaryGuarded$1$2(this.f, t1._as(arg), t1);
+    },
+    $signature() {
+      return this.T._eval$1("~(0)");
+    }
+  };
+  A._HashMap.prototype = {
+    get$length(_) {
+      return this._collection$_length;
+    },
+    get$isEmpty(_) {
+      return this._collection$_length === 0;
+    },
+    get$keys() {
+      return new A._HashMapKeyIterable(this, this.$ti._eval$1("_HashMapKeyIterable<1>"));
+    },
+    containsKey$1(key) {
+      var strings, nums;
+      if (typeof key == "string" && key !== "__proto__") {
+        strings = this._collection$_strings;
+        return strings == null ? false : strings[key] != null;
+      } else if (typeof key == "number" && (key & 1073741823) === key) {
+        nums = this._collection$_nums;
+        return nums == null ? false : nums[key] != null;
+      } else
+        return this._containsKey$1(key);
+    },
+    _containsKey$1(key) {
+      var rest = this._collection$_rest;
+      if (rest == null)
+        return false;
+      return this._findBucketIndex$2(this._getBucket$2(rest, key), key) >= 0;
+    },
+    $index(_, key) {
+      var strings, t1, nums;
+      if (typeof key == "string" && key !== "__proto__") {
+        strings = this._collection$_strings;
+        t1 = strings == null ? null : A._HashMap__getTableEntry(strings, key);
+        return t1;
+      } else if (typeof key == "number" && (key & 1073741823) === key) {
+        nums = this._collection$_nums;
+        t1 = nums == null ? null : A._HashMap__getTableEntry(nums, key);
+        return t1;
+      } else
+        return this._get$1(key);
+    },
+    _get$1(key) {
+      var bucket, index,
+        rest = this._collection$_rest;
+      if (rest == null)
+        return null;
+      bucket = this._getBucket$2(rest, key);
+      index = this._findBucketIndex$2(bucket, key);
+      return index < 0 ? null : bucket[index + 1];
+    },
+    $indexSet(_, key, value) {
+      var strings, nums, rest, hash, bucket, index, _this = this,
+        t1 = _this.$ti;
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      if (typeof key == "string" && key !== "__proto__") {
+        strings = _this._collection$_strings;
+        _this._collection$_addHashTableEntry$3(strings == null ? _this._collection$_strings = A._HashMap__newHashTable() : strings, key, value);
+      } else if (typeof key == "number" && (key & 1073741823) === key) {
+        nums = _this._collection$_nums;
+        _this._collection$_addHashTableEntry$3(nums == null ? _this._collection$_nums = A._HashMap__newHashTable() : nums, key, value);
+      } else {
+        rest = _this._collection$_rest;
+        if (rest == null)
+          rest = _this._collection$_rest = A._HashMap__newHashTable();
+        hash = A.objectHashCode(key) & 1073741823;
+        bucket = rest[hash];
+        if (bucket == null) {
+          A._HashMap__setTableEntry(rest, hash, [key, value]);
+          ++_this._collection$_length;
+          _this._collection$_keys = null;
+        } else {
+          index = _this._findBucketIndex$2(bucket, key);
+          if (index >= 0)
+            bucket[index + 1] = value;
+          else {
+            bucket.push(key, value);
+            ++_this._collection$_length;
+            _this._collection$_keys = null;
+          }
+        }
+      }
+    },
+    forEach$1(_, action) {
+      var keys, $length, t2, i, key, t3, _this = this,
+        t1 = _this.$ti;
+      t1._eval$1("~(1,2)")._as(action);
+      keys = _this._computeKeys$0();
+      for ($length = keys.length, t2 = t1._precomputed1, t1 = t1._rest[1], i = 0; i < $length; ++i) {
+        key = keys[i];
+        t2._as(key);
+        t3 = _this.$index(0, key);
+        action.call$2(key, t3 == null ? t1._as(t3) : t3);
+        if (keys !== _this._collection$_keys)
+          throw A.wrapException(A.ConcurrentModificationError$(_this));
+      }
+    },
+    _computeKeys$0() {
+      var strings, names, entries, index, i, nums, rest, bucket, $length, i0, _this = this,
+        result = _this._collection$_keys;
+      if (result != null)
+        return result;
+      result = A.List_List$filled(_this._collection$_length, null, false, type$.dynamic);
+      strings = _this._collection$_strings;
+      if (strings != null) {
+        names = Object.getOwnPropertyNames(strings);
+        entries = names.length;
+        for (index = 0, i = 0; i < entries; ++i) {
+          result[index] = names[i];
+          ++index;
+        }
+      } else
+        index = 0;
+      nums = _this._collection$_nums;
+      if (nums != null) {
+        names = Object.getOwnPropertyNames(nums);
+        entries = names.length;
+        for (i = 0; i < entries; ++i) {
+          result[index] = +names[i];
+          ++index;
+        }
+      }
+      rest = _this._collection$_rest;
+      if (rest != null) {
+        names = Object.getOwnPropertyNames(rest);
+        entries = names.length;
+        for (i = 0; i < entries; ++i) {
+          bucket = rest[names[i]];
+          $length = bucket.length;
+          for (i0 = 0; i0 < $length; i0 += 2) {
+            result[index] = bucket[i0];
+            ++index;
+          }
+        }
+      }
+      return _this._collection$_keys = result;
+    },
+    _collection$_addHashTableEntry$3(table, key, value) {
+      var t1 = this.$ti;
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      if (table[key] == null) {
+        ++this._collection$_length;
+        this._collection$_keys = null;
+      }
+      A._HashMap__setTableEntry(table, key, value);
+    },
+    _getBucket$2(table, key) {
+      return table[A.objectHashCode(key) & 1073741823];
+    }
+  };
+  A._IdentityHashMap.prototype = {
+    _findBucketIndex$2(bucket, key) {
+      var $length, i, t1;
+      if (bucket == null)
+        return -1;
+      $length = bucket.length;
+      for (i = 0; i < $length; i += 2) {
+        t1 = bucket[i];
+        if (t1 == null ? key == null : t1 === key)
+          return i;
+      }
+      return -1;
+    }
+  };
+  A._HashMapKeyIterable.prototype = {
+    get$length(_) {
+      return this._collection$_map._collection$_length;
+    },
+    get$isEmpty(_) {
+      return this._collection$_map._collection$_length === 0;
+    },
+    get$iterator(_) {
+      var t1 = this._collection$_map;
+      return new A._HashMapKeyIterator(t1, t1._computeKeys$0(), this.$ti._eval$1("_HashMapKeyIterator<1>"));
+    }
+  };
+  A._HashMapKeyIterator.prototype = {
+    get$current() {
+      var t1 = this._collection$_current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    moveNext$0() {
+      var _this = this,
+        keys = _this._collection$_keys,
+        offset = _this._offset,
+        t1 = _this._collection$_map;
+      if (keys !== t1._collection$_keys)
+        throw A.wrapException(A.ConcurrentModificationError$(t1));
+      else if (offset >= keys.length) {
+        _this.set$_collection$_current(null);
+        return false;
+      } else {
+        _this.set$_collection$_current(keys[offset]);
+        _this._offset = offset + 1;
+        return true;
+      }
+    },
+    set$_collection$_current(_current) {
+      this._collection$_current = this.$ti._eval$1("1?")._as(_current);
+    }
+  };
+  A.ListBase.prototype = {
+    get$iterator(receiver) {
+      return new A.ListIterator(receiver, this.get$length(receiver), A.instanceType(receiver)._eval$1("ListIterator<ListBase.E>"));
+    },
+    elementAt$1(receiver, index) {
+      return this.$index(receiver, index);
+    },
+    get$isNotEmpty(receiver) {
+      return this.get$length(receiver) !== 0;
+    },
+    toString$0(receiver) {
+      return A.Iterable_iterableToFullString(receiver, "[", "]");
+    }
+  };
+  A.MapBase.prototype = {
+    forEach$1(_, action) {
+      var t2, key, t3,
+        t1 = A._instanceType(this);
+      t1._eval$1("~(MapBase.K,MapBase.V)")._as(action);
+      for (t2 = this.get$keys(), t2 = t2.get$iterator(t2), t1 = t1._eval$1("MapBase.V"); t2.moveNext$0();) {
+        key = t2.get$current();
+        t3 = this.$index(0, key);
+        action.call$2(key, t3 == null ? t1._as(t3) : t3);
+      }
+    },
+    get$length(_) {
+      var t1 = this.get$keys();
+      return t1.get$length(t1);
+    },
+    get$isEmpty(_) {
+      var t1 = this.get$keys();
+      return t1.get$isEmpty(t1);
+    },
+    toString$0(_) {
+      return A.MapBase_mapToString(this);
+    },
+    $isMap: 1
+  };
+  A.MapBase_mapToString_closure.prototype = {
+    call$2(k, v) {
+      var t2,
+        t1 = this._box_0;
+      if (!t1.first)
+        this.result._contents += ", ";
+      t1.first = false;
+      t1 = this.result;
+      t2 = A.S(k);
+      t2 = t1._contents += t2;
+      t1._contents = t2 + ": ";
+      t2 = A.S(v);
+      t1._contents += t2;
+    },
+    $signature: 10
+  };
+  A._UnmodifiableMapMixin.prototype = {};
+  A.MapView.prototype = {
+    forEach$1(_, action) {
+      this._collection$_map.forEach$1(0, A._instanceType(this)._eval$1("~(1,2)")._as(action));
+    },
+    get$isEmpty(_) {
+      return this._collection$_map.__js_helper$_length === 0;
+    },
+    get$length(_) {
+      return this._collection$_map.__js_helper$_length;
+    },
+    toString$0(_) {
+      return A.MapBase_mapToString(this._collection$_map);
+    },
+    $isMap: 1
+  };
+  A.UnmodifiableMapView.prototype = {};
+  A.ListQueue.prototype = {
+    get$iterator(_) {
+      var _this = this;
+      return new A._ListQueueIterator(_this, _this._tail, _this._modificationCount, _this._head, _this.$ti._eval$1("_ListQueueIterator<1>"));
+    },
+    get$isEmpty(_) {
+      return this._head === this._tail;
+    },
+    get$length(_) {
+      return (this._tail - this._head & this._table.length - 1) >>> 0;
+    },
+    elementAt$1(_, index) {
+      var t2, t3, _this = this,
+        t1 = _this.get$length(0);
+      if (0 > index || index >= t1)
+        A.throwExpression(A.IndexError$withLength(index, t1, _this, null, "index"));
+      t1 = _this._table;
+      t2 = t1.length;
+      t3 = (_this._head + index & t2 - 1) >>> 0;
+      if (!(t3 >= 0 && t3 < t2))
+        return A.ioore(t1, t3);
+      t3 = t1[t3];
+      return t3 == null ? _this.$ti._precomputed1._as(t3) : t3;
+    },
+    toString$0(_) {
+      return A.Iterable_iterableToFullString(this, "{", "}");
+    },
+    removeFirst$0() {
+      var t2, result, _this = this,
+        t1 = _this._head;
+      if (t1 === _this._tail)
+        throw A.wrapException(A.IterableElementError_noElement());
+      ++_this._modificationCount;
+      t2 = _this._table;
+      if (!(t1 < t2.length))
+        return A.ioore(t2, t1);
+      result = t2[t1];
+      if (result == null)
+        result = _this.$ti._precomputed1._as(result);
+      B.JSArray_methods.$indexSet(t2, t1, null);
+      _this._head = (_this._head + 1 & _this._table.length - 1) >>> 0;
+      return result;
+    },
+    _add$1(element) {
+      var t2, t3, newTable, split, _this = this,
+        t1 = _this.$ti;
+      t1._precomputed1._as(element);
+      B.JSArray_methods.$indexSet(_this._table, _this._tail, element);
+      t2 = _this._tail;
+      t3 = _this._table.length;
+      t2 = (t2 + 1 & t3 - 1) >>> 0;
+      _this._tail = t2;
+      if (_this._head === t2) {
+        newTable = A.List_List$filled(t3 * 2, null, false, t1._eval$1("1?"));
+        t1 = _this._table;
+        t2 = _this._head;
+        split = t1.length - t2;
+        B.JSArray_methods.setRange$4(newTable, 0, split, t1, t2);
+        B.JSArray_methods.setRange$4(newTable, split, split + _this._head, _this._table, 0);
+        _this._head = 0;
+        _this._tail = _this._table.length;
+        _this.set$_table(newTable);
+      }
+      ++_this._modificationCount;
+    },
+    set$_table(_table) {
+      this._table = this.$ti._eval$1("List<1?>")._as(_table);
+    },
+    $isQueue: 1
+  };
+  A._ListQueueIterator.prototype = {
+    get$current() {
+      var t1 = this._collection$_current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    moveNext$0() {
+      var t2, t3, _this = this,
+        t1 = _this._queue;
+      if (_this._modificationCount !== t1._modificationCount)
+        A.throwExpression(A.ConcurrentModificationError$(t1));
+      t2 = _this._position;
+      if (t2 === _this._end) {
+        _this.set$_collection$_current(null);
+        return false;
+      }
+      t3 = t1._table;
+      if (!(t2 < t3.length))
+        return A.ioore(t3, t2);
+      _this.set$_collection$_current(t3[t2]);
+      _this._position = (_this._position + 1 & t1._table.length - 1) >>> 0;
+      return true;
+    },
+    set$_collection$_current(_current) {
+      this._collection$_current = this.$ti._eval$1("1?")._as(_current);
+    }
+  };
+  A._UnmodifiableMapView_MapView__UnmodifiableMapMixin.prototype = {};
+  A._JsonMap.prototype = {
+    $index(_, key) {
+      var result,
+        t1 = this._processed;
+      if (t1 == null)
+        return this._data.$index(0, key);
+      else if (typeof key != "string")
+        return null;
+      else {
+        result = t1[key];
+        return typeof result == "undefined" ? this._process$1(key) : result;
+      }
+    },
+    get$length(_) {
+      return this._processed == null ? this._data.__js_helper$_length : this._convert$_computeKeys$0().length;
+    },
+    get$isEmpty(_) {
+      return this.get$length(0) === 0;
+    },
+    get$keys() {
+      if (this._processed == null) {
+        var t1 = this._data;
+        return new A.LinkedHashMapKeyIterable(t1, A._instanceType(t1)._eval$1("LinkedHashMapKeyIterable<1>"));
+      }
+      return new A._JsonMapKeyIterable(this);
+    },
+    forEach$1(_, f) {
+      var keys, i, key, value, _this = this;
+      type$.void_Function_String_dynamic._as(f);
+      if (_this._processed == null)
+        return _this._data.forEach$1(0, f);
+      keys = _this._convert$_computeKeys$0();
+      for (i = 0; i < keys.length; ++i) {
+        key = keys[i];
+        value = _this._processed[key];
+        if (typeof value == "undefined") {
+          value = A._convertJsonToDartLazy(_this._original[key]);
+          _this._processed[key] = value;
+        }
+        f.call$2(key, value);
+        if (keys !== _this._data)
+          throw A.wrapException(A.ConcurrentModificationError$(_this));
+      }
+    },
+    _convert$_computeKeys$0() {
+      var keys = type$.nullable_List_dynamic._as(this._data);
+      if (keys == null)
+        keys = this._data = A._setArrayType(Object.keys(this._original), type$.JSArray_String);
+      return keys;
+    },
+    _process$1(key) {
+      var result;
+      if (!Object.prototype.hasOwnProperty.call(this._original, key))
+        return null;
+      result = A._convertJsonToDartLazy(this._original[key]);
+      return this._processed[key] = result;
+    }
+  };
+  A._JsonMapKeyIterable.prototype = {
+    get$length(_) {
+      return this._parent.get$length(0);
+    },
+    elementAt$1(_, index) {
+      var t1 = this._parent;
+      if (t1._processed == null)
+        t1 = t1.get$keys().elementAt$1(0, index);
+      else {
+        t1 = t1._convert$_computeKeys$0();
+        if (!(index >= 0 && index < t1.length))
+          return A.ioore(t1, index);
+        t1 = t1[index];
+      }
+      return t1;
+    },
+    get$iterator(_) {
+      var t1 = this._parent;
+      if (t1._processed == null) {
+        t1 = t1.get$keys();
+        t1 = t1.get$iterator(t1);
+      } else {
+        t1 = t1._convert$_computeKeys$0();
+        t1 = new J.ArrayIterator(t1, t1.length, A._arrayInstanceType(t1)._eval$1("ArrayIterator<1>"));
+      }
+      return t1;
+    }
+  };
+  A.Codec.prototype = {};
+  A.Converter.prototype = {};
+  A.JsonUnsupportedObjectError.prototype = {
+    toString$0(_) {
+      var safeString = A.Error_safeToString(this.unsupportedObject);
+      return (this.cause != null ? "Converting object to an encodable object failed:" : "Converting object did not return an encodable object:") + " " + safeString;
+    }
+  };
+  A.JsonCyclicError.prototype = {
+    toString$0(_) {
+      return "Cyclic error in JSON stringify";
+    }
+  };
+  A.JsonCodec.prototype = {
+    decode$2$reviver(source, reviver) {
+      var t1 = A._parseJson(source, this.get$decoder()._reviver);
+      return t1;
+    },
+    encode$2$toEncodable(value, toEncodable) {
+      var t1 = A._JsonStringStringifier_stringify(value, this.get$encoder()._toEncodable, null);
+      return t1;
+    },
+    get$encoder() {
+      return B.JsonEncoder_null;
+    },
+    get$decoder() {
+      return B.JsonDecoder_null;
+    }
+  };
+  A.JsonEncoder.prototype = {};
+  A.JsonDecoder.prototype = {};
+  A._JsonStringifier.prototype = {
+    writeStringContent$1(s) {
+      var offset, i, charCode, t1, t2, _this = this,
+        $length = s.length;
+      for (offset = 0, i = 0; i < $length; ++i) {
+        charCode = s.charCodeAt(i);
+        if (charCode > 92) {
+          if (charCode >= 55296) {
+            t1 = charCode & 64512;
+            if (t1 === 55296) {
+              t2 = i + 1;
+              t2 = !(t2 < $length && (s.charCodeAt(t2) & 64512) === 56320);
+            } else
+              t2 = false;
+            if (!t2)
+              if (t1 === 56320) {
+                t1 = i - 1;
+                t1 = !(t1 >= 0 && (s.charCodeAt(t1) & 64512) === 55296);
+              } else
+                t1 = false;
+            else
+              t1 = true;
+            if (t1) {
+              if (i > offset)
+                _this.writeStringSlice$3(s, offset, i);
+              offset = i + 1;
+              _this.writeCharCode$1(92);
+              _this.writeCharCode$1(117);
+              _this.writeCharCode$1(100);
+              t1 = charCode >>> 8 & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+              t1 = charCode >>> 4 & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+              t1 = charCode & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+            }
+          }
+          continue;
+        }
+        if (charCode < 32) {
+          if (i > offset)
+            _this.writeStringSlice$3(s, offset, i);
+          offset = i + 1;
+          _this.writeCharCode$1(92);
+          switch (charCode) {
+            case 8:
+              _this.writeCharCode$1(98);
+              break;
+            case 9:
+              _this.writeCharCode$1(116);
+              break;
+            case 10:
+              _this.writeCharCode$1(110);
+              break;
+            case 12:
+              _this.writeCharCode$1(102);
+              break;
+            case 13:
+              _this.writeCharCode$1(114);
+              break;
+            default:
+              _this.writeCharCode$1(117);
+              _this.writeCharCode$1(48);
+              _this.writeCharCode$1(48);
+              t1 = charCode >>> 4 & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+              t1 = charCode & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+              break;
+          }
+        } else if (charCode === 34 || charCode === 92) {
+          if (i > offset)
+            _this.writeStringSlice$3(s, offset, i);
+          offset = i + 1;
+          _this.writeCharCode$1(92);
+          _this.writeCharCode$1(charCode);
+        }
+      }
+      if (offset === 0)
+        _this.writeString$1(s);
+      else if (offset < $length)
+        _this.writeStringSlice$3(s, offset, $length);
+    },
+    _checkCycle$1(object) {
+      var t1, t2, i, t3;
+      for (t1 = this._seen, t2 = t1.length, i = 0; i < t2; ++i) {
+        t3 = t1[i];
+        if (object == null ? t3 == null : object === t3)
+          throw A.wrapException(new A.JsonCyclicError(object, null));
+      }
+      B.JSArray_methods.add$1(t1, object);
+    },
+    writeObject$1(object) {
+      var customJson, e, t1, exception, _this = this;
+      if (_this.writeJsonValue$1(object))
+        return;
+      _this._checkCycle$1(object);
+      try {
+        customJson = _this._toEncodable.call$1(object);
+        if (!_this.writeJsonValue$1(customJson)) {
+          t1 = A.JsonUnsupportedObjectError$(object, null, _this.get$_partialResult());
+          throw A.wrapException(t1);
+        }
+        t1 = _this._seen;
+        if (0 >= t1.length)
+          return A.ioore(t1, -1);
+        t1.pop();
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        t1 = A.JsonUnsupportedObjectError$(object, e, _this.get$_partialResult());
+        throw A.wrapException(t1);
+      }
+    },
+    writeJsonValue$1(object) {
+      var t1, success, _this = this;
+      if (typeof object == "number") {
+        if (!isFinite(object))
+          return false;
+        _this.writeNumber$1(object);
+        return true;
+      } else if (object === true) {
+        _this.writeString$1("true");
+        return true;
+      } else if (object === false) {
+        _this.writeString$1("false");
+        return true;
+      } else if (object == null) {
+        _this.writeString$1("null");
+        return true;
+      } else if (typeof object == "string") {
+        _this.writeString$1('"');
+        _this.writeStringContent$1(object);
+        _this.writeString$1('"');
+        return true;
+      } else if (type$.List_dynamic._is(object)) {
+        _this._checkCycle$1(object);
+        _this.writeList$1(object);
+        t1 = _this._seen;
+        if (0 >= t1.length)
+          return A.ioore(t1, -1);
+        t1.pop();
+        return true;
+      } else if (type$.Map_dynamic_dynamic._is(object)) {
+        _this._checkCycle$1(object);
+        success = _this.writeMap$1(object);
+        t1 = _this._seen;
+        if (0 >= t1.length)
+          return A.ioore(t1, -1);
+        t1.pop();
+        return success;
+      } else
+        return false;
+    },
+    writeList$1(list) {
+      var t1, i, _this = this;
+      _this.writeString$1("[");
+      t1 = J.getInterceptor$asx(list);
+      if (t1.get$isNotEmpty(list)) {
+        _this.writeObject$1(t1.$index(list, 0));
+        for (i = 1; i < t1.get$length(list); ++i) {
+          _this.writeString$1(",");
+          _this.writeObject$1(t1.$index(list, i));
+        }
+      }
+      _this.writeString$1("]");
+    },
+    writeMap$1(map) {
+      var t1, keyValueList, i, separator, t2, _this = this, _box_0 = {};
+      if (map.get$isEmpty(map)) {
+        _this.writeString$1("{}");
+        return true;
+      }
+      t1 = map.get$length(map) * 2;
+      keyValueList = A.List_List$filled(t1, null, false, type$.nullable_Object);
+      i = _box_0.i = 0;
+      _box_0.allStringKeys = true;
+      map.forEach$1(0, new A._JsonStringifier_writeMap_closure(_box_0, keyValueList));
+      if (!_box_0.allStringKeys)
+        return false;
+      _this.writeString$1("{");
+      for (separator = '"'; i < t1; i += 2, separator = ',"') {
+        _this.writeString$1(separator);
+        _this.writeStringContent$1(A._asString(keyValueList[i]));
+        _this.writeString$1('":');
+        t2 = i + 1;
+        if (!(t2 < t1))
+          return A.ioore(keyValueList, t2);
+        _this.writeObject$1(keyValueList[t2]);
+      }
+      _this.writeString$1("}");
+      return true;
+    }
+  };
+  A._JsonStringifier_writeMap_closure.prototype = {
+    call$2(key, value) {
+      var t1, t2;
+      if (typeof key != "string")
+        this._box_0.allStringKeys = false;
+      t1 = this.keyValueList;
+      t2 = this._box_0;
+      B.JSArray_methods.$indexSet(t1, t2.i++, key);
+      B.JSArray_methods.$indexSet(t1, t2.i++, value);
+    },
+    $signature: 10
+  };
+  A._JsonStringStringifier.prototype = {
+    get$_partialResult() {
+      var t1 = this._sink._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    writeNumber$1(number) {
+      var t1 = this._sink,
+        t2 = B.JSNumber_methods.toString$0(number);
+      t1._contents += t2;
+    },
+    writeString$1(string) {
+      this._sink._contents += string;
+    },
+    writeStringSlice$3(string, start, end) {
+      this._sink._contents += B.JSString_methods.substring$2(string, start, end);
+    },
+    writeCharCode$1(charCode) {
+      var t1 = this._sink,
+        t2 = A.Primitives_stringFromCharCode(charCode);
+      t1._contents += t2;
+    }
+  };
+  A.NoSuchMethodError_toString_closure.prototype = {
+    call$2(key, value) {
+      var t1, t2, t3;
+      type$.Symbol._as(key);
+      t1 = this.sb;
+      t2 = this._box_0;
+      t3 = t1._contents += t2.comma;
+      t3 += key._name;
+      t1._contents = t3;
+      t1._contents = t3 + ": ";
+      t3 = A.Error_safeToString(value);
+      t1._contents += t3;
+      t2.comma = ", ";
+    },
+    $signature: 19
+  };
+  A.DateTime.prototype = {
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      return other instanceof A.DateTime && this._value === other._value && this.isUtc === other.isUtc;
+    },
+    get$hashCode(_) {
+      var t1 = this._value;
+      return (t1 ^ B.JSInt_methods._shrOtherPositive$1(t1, 30)) & 1073741823;
+    },
+    toString$0(_) {
+      var _this = this,
+        y = A.DateTime__fourDigits(A.Primitives_getYear(_this)),
+        m = A.DateTime__twoDigits(A.Primitives_getMonth(_this)),
+        d = A.DateTime__twoDigits(A.Primitives_getDay(_this)),
+        h = A.DateTime__twoDigits(A.Primitives_getHours(_this)),
+        min = A.DateTime__twoDigits(A.Primitives_getMinutes(_this)),
+        sec = A.DateTime__twoDigits(A.Primitives_getSeconds(_this)),
+        ms = A.DateTime__threeDigits(A.Primitives_getMilliseconds(_this)),
+        t1 = y + "-" + m;
+      if (_this.isUtc)
+        return t1 + "-" + d + " " + h + ":" + min + ":" + sec + "." + ms + "Z";
+      else
+        return t1 + "-" + d + " " + h + ":" + min + ":" + sec + "." + ms;
+    }
+  };
+  A.Duration.prototype = {
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      return other instanceof A.Duration && this._duration === other._duration;
+    },
+    get$hashCode(_) {
+      return B.JSInt_methods.get$hashCode(this._duration);
+    },
+    toString$0(_) {
+      var minutesPadding, seconds, secondsPadding,
+        microseconds = this._duration,
+        microseconds0 = microseconds % 3600000000,
+        minutes = B.JSInt_methods._tdivFast$1(microseconds0, 60000000);
+      microseconds0 %= 60000000;
+      minutesPadding = minutes < 10 ? "0" : "";
+      seconds = B.JSInt_methods._tdivFast$1(microseconds0, 1000000);
+      secondsPadding = seconds < 10 ? "0" : "";
+      return "" + (microseconds / 3600000000 | 0) + ":" + minutesPadding + minutes + ":" + secondsPadding + seconds + "." + B.JSString_methods.padLeft$2(B.JSInt_methods.toString$0(microseconds0 % 1000000), 6, "0");
+    }
+  };
+  A.Error.prototype = {
+    get$stackTrace() {
+      return A.getTraceFromException(this.$thrownJsError);
+    }
+  };
+  A.AssertionError.prototype = {
+    toString$0(_) {
+      var t1 = this.message;
+      if (t1 != null)
+        return "Assertion failed: " + A.Error_safeToString(t1);
+      return "Assertion failed";
+    }
+  };
+  A.TypeError.prototype = {};
+  A.ArgumentError.prototype = {
+    get$_errorName() {
+      return "Invalid argument" + (!this._hasValue ? "(s)" : "");
+    },
+    get$_errorExplanation() {
+      return "";
+    },
+    toString$0(_) {
+      var _this = this,
+        $name = _this.name,
+        nameString = $name == null ? "" : " (" + $name + ")",
+        message = _this.message,
+        messageString = message == null ? "" : ": " + A.S(message),
+        prefix = _this.get$_errorName() + nameString + messageString;
+      if (!_this._hasValue)
+        return prefix;
+      return prefix + _this.get$_errorExplanation() + ": " + A.Error_safeToString(_this.get$invalidValue());
+    },
+    get$invalidValue() {
+      return this.invalidValue;
+    }
+  };
+  A.RangeError.prototype = {
+    get$invalidValue() {
+      return A._asNumQ(this.invalidValue);
+    },
+    get$_errorName() {
+      return "RangeError";
+    },
+    get$_errorExplanation() {
+      var explanation,
+        start = this.start,
+        end = this.end;
+      if (start == null)
+        explanation = end != null ? ": Not less than or equal to " + A.S(end) : "";
+      else if (end == null)
+        explanation = ": Not greater than or equal to " + A.S(start);
+      else if (end > start)
+        explanation = ": Not in inclusive range " + A.S(start) + ".." + A.S(end);
+      else
+        explanation = end < start ? ": Valid value range is empty" : ": Only valid value is " + A.S(start);
+      return explanation;
+    }
+  };
+  A.IndexError.prototype = {
+    get$invalidValue() {
+      return A._asInt(this.invalidValue);
+    },
+    get$_errorName() {
+      return "RangeError";
+    },
+    get$_errorExplanation() {
+      if (A._asInt(this.invalidValue) < 0)
+        return ": index must not be negative";
+      var t1 = this.length;
+      if (t1 === 0)
+        return ": no indices are valid";
+      return ": index should be less than " + t1;
+    },
+    get$length(receiver) {
+      return this.length;
+    }
+  };
+  A.NoSuchMethodError.prototype = {
+    toString$0(_) {
+      var $arguments, t1, _i, t2, t3, argument, receiverText, actualParameters, _this = this, _box_0 = {},
+        sb = new A.StringBuffer("");
+      _box_0.comma = "";
+      $arguments = _this._core$_arguments;
+      for (t1 = $arguments.length, _i = 0, t2 = "", t3 = ""; _i < t1; ++_i, t3 = ", ") {
+        argument = $arguments[_i];
+        sb._contents = t2 + t3;
+        t2 = A.Error_safeToString(argument);
+        t2 = sb._contents += t2;
+        _box_0.comma = ", ";
+      }
+      _this._namedArguments.forEach$1(0, new A.NoSuchMethodError_toString_closure(_box_0, sb));
+      receiverText = A.Error_safeToString(_this._core$_receiver);
+      actualParameters = sb.toString$0(0);
+      return "NoSuchMethodError: method not found: '" + _this._core$_memberName._name + "'\nReceiver: " + receiverText + "\nArguments: [" + actualParameters + "]";
+    }
+  };
+  A.UnsupportedError.prototype = {
+    toString$0(_) {
+      return "Unsupported operation: " + this.message;
+    }
+  };
+  A.UnimplementedError.prototype = {
+    toString$0(_) {
+      return "UnimplementedError: " + this.message;
+    }
+  };
+  A.StateError.prototype = {
+    toString$0(_) {
+      return "Bad state: " + this.message;
+    }
+  };
+  A.ConcurrentModificationError.prototype = {
+    toString$0(_) {
+      var t1 = this.modifiedObject;
+      if (t1 == null)
+        return "Concurrent modification during iteration.";
+      return "Concurrent modification during iteration: " + A.Error_safeToString(t1) + ".";
+    }
+  };
+  A.OutOfMemoryError.prototype = {
+    toString$0(_) {
+      return "Out of Memory";
+    },
+    get$stackTrace() {
+      return null;
+    },
+    $isError: 1
+  };
+  A.StackOverflowError.prototype = {
+    toString$0(_) {
+      return "Stack Overflow";
+    },
+    get$stackTrace() {
+      return null;
+    },
+    $isError: 1
+  };
+  A._Exception.prototype = {
+    toString$0(_) {
+      return "Exception: " + this.message;
+    }
+  };
+  A.FormatException.prototype = {
+    toString$0(_) {
+      var t1, lineEnd, lineNum, lineStart, previousCharWasCR, i, char, end, start, prefix, postfix,
+        message = this.message,
+        report = "" !== message ? "FormatException: " + message : "FormatException",
+        offset = this.offset,
+        source = this.source;
+      if (typeof source == "string") {
+        if (offset != null)
+          t1 = offset < 0 || offset > source.length;
+        else
+          t1 = false;
+        if (t1)
+          offset = null;
+        if (offset == null) {
+          if (source.length > 78)
+            source = B.JSString_methods.substring$2(source, 0, 75) + "...";
+          return report + "\n" + source;
+        }
+        for (lineEnd = source.length, lineNum = 1, lineStart = 0, previousCharWasCR = false, i = 0; i < offset; ++i) {
+          if (!(i < lineEnd))
+            return A.ioore(source, i);
+          char = source.charCodeAt(i);
+          if (char === 10) {
+            if (lineStart !== i || !previousCharWasCR)
+              ++lineNum;
+            lineStart = i + 1;
+            previousCharWasCR = false;
+          } else if (char === 13) {
+            ++lineNum;
+            lineStart = i + 1;
+            previousCharWasCR = true;
+          }
+        }
+        report = lineNum > 1 ? report + (" (at line " + lineNum + ", character " + (offset - lineStart + 1) + ")\n") : report + (" (at character " + (offset + 1) + ")\n");
+        for (i = offset; i < lineEnd; ++i) {
+          if (!(i >= 0))
+            return A.ioore(source, i);
+          char = source.charCodeAt(i);
+          if (char === 10 || char === 13) {
+            lineEnd = i;
+            break;
+          }
+        }
+        if (lineEnd - lineStart > 78)
+          if (offset - lineStart < 75) {
+            end = lineStart + 75;
+            start = lineStart;
+            prefix = "";
+            postfix = "...";
+          } else {
+            if (lineEnd - offset < 75) {
+              start = lineEnd - 75;
+              end = lineEnd;
+              postfix = "";
+            } else {
+              start = offset - 36;
+              end = offset + 36;
+              postfix = "...";
+            }
+            prefix = "...";
+          }
+        else {
+          end = lineEnd;
+          start = lineStart;
+          prefix = "";
+          postfix = "";
+        }
+        return report + prefix + B.JSString_methods.substring$2(source, start, end) + postfix + "\n" + B.JSString_methods.$mul(" ", offset - start + prefix.length) + "^\n";
+      } else
+        return offset != null ? report + (" (at offset " + A.S(offset) + ")") : report;
+    }
+  };
+  A.Iterable.prototype = {
+    get$length(_) {
+      var count,
+        it = this.get$iterator(this);
+      for (count = 0; it.moveNext$0();)
+        ++count;
+      return count;
+    },
+    elementAt$1(_, index) {
+      var iterator, skipCount;
+      A.RangeError_checkNotNegative(index, "index");
+      iterator = this.get$iterator(this);
+      for (skipCount = index; iterator.moveNext$0();) {
+        if (skipCount === 0)
+          return iterator.get$current();
+        --skipCount;
+      }
+      throw A.wrapException(A.IndexError$withLength(index, index - skipCount, this, null, "index"));
+    },
+    toString$0(_) {
+      return A.Iterable_iterableToShortString(this, "(", ")");
+    }
+  };
+  A.Null.prototype = {
+    get$hashCode(_) {
+      return A.Object.prototype.get$hashCode.call(this, 0);
+    },
+    toString$0(_) {
+      return "null";
+    }
+  };
+  A.Object.prototype = {$isObject: 1,
+    $eq(_, other) {
+      return this === other;
+    },
+    get$hashCode(_) {
+      return A.Primitives_objectHashCode(this);
+    },
+    toString$0(_) {
+      return "Instance of '" + A.Primitives_objectTypeName(this) + "'";
+    },
+    noSuchMethod$1(_, invocation) {
+      throw A.wrapException(A.NoSuchMethodError_NoSuchMethodError$withInvocation(this, type$.Invocation._as(invocation)));
+    },
+    get$runtimeType(_) {
+      return A.getRuntimeTypeOfDartObject(this);
+    },
+    toString() {
+      return this.toString$0(this);
+    }
+  };
+  A._StringStackTrace.prototype = {
+    toString$0(_) {
+      return this._stackTrace;
+    },
+    $isStackTrace: 1
+  };
+  A.StringBuffer.prototype = {
+    get$length(_) {
+      return this._contents.length;
+    },
+    toString$0(_) {
+      var t1 = this._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    $isStringSink: 1
+  };
+  A.promiseToFuture_closure.prototype = {
+    call$1(r) {
+      return this.completer.complete$1(this.T._eval$1("0/?")._as(r));
+    },
+    $signature: 3
+  };
+  A.promiseToFuture_closure0.prototype = {
+    call$1(e) {
+      if (e == null)
+        return this.completer.completeError$1(new A.NullRejectionException(e === undefined));
+      return this.completer.completeError$1(e);
+    },
+    $signature: 3
+  };
+  A.dartify_convert.prototype = {
+    call$1(o) {
+      var t1, millisSinceEpoch, proto, t2, dartObject, originalKeys, dartKeys, i, jsKey, dartKey, l, $length;
+      if (A._noDartifyRequired(o))
+        return o;
+      t1 = this._convertedObjects;
+      o.toString;
+      if (t1.containsKey$1(o))
+        return t1.$index(0, o);
+      if (o instanceof Date) {
+        millisSinceEpoch = o.getTime();
+        if (Math.abs(millisSinceEpoch) > 864e13)
+          A.throwExpression(A.ArgumentError$("DateTime is outside valid range: " + millisSinceEpoch, null));
+        A.checkNotNullable(true, "isUtc", type$.bool);
+        return new A.DateTime(millisSinceEpoch, true);
+      }
+      if (o instanceof RegExp)
+        throw A.wrapException(A.ArgumentError$("structured clone of RegExp", null));
+      if (typeof Promise != "undefined" && o instanceof Promise)
+        return A.promiseToFuture(o, type$.nullable_Object);
+      proto = Object.getPrototypeOf(o);
+      if (proto === Object.prototype || proto === null) {
+        t2 = type$.nullable_Object;
+        dartObject = A.LinkedHashMap_LinkedHashMap$_empty(t2, t2);
+        t1.$indexSet(0, o, dartObject);
+        originalKeys = Object.keys(o);
+        dartKeys = [];
+        for (t1 = J.getInterceptor$ax(originalKeys), t2 = t1.get$iterator(originalKeys); t2.moveNext$0();)
+          dartKeys.push(A.dartify(t2.get$current()));
+        for (i = 0; i < t1.get$length(originalKeys); ++i) {
+          jsKey = t1.$index(originalKeys, i);
+          if (!(i < dartKeys.length))
+            return A.ioore(dartKeys, i);
+          dartKey = dartKeys[i];
+          if (jsKey != null)
+            dartObject.$indexSet(0, dartKey, this.call$1(o[jsKey]));
+        }
+        return dartObject;
+      }
+      if (o instanceof Array) {
+        l = o;
+        dartObject = [];
+        t1.$indexSet(0, o, dartObject);
+        $length = A._asInt(o.length);
+        for (t1 = J.getInterceptor$asx(l), i = 0; i < $length; ++i)
+          dartObject.push(this.call$1(t1.$index(l, i)));
+        return dartObject;
+      }
+      return o;
+    },
+    $signature: 20
+  };
+  A.NullRejectionException.prototype = {
+    toString$0(_) {
+      return "Promise was rejected with a value of `" + (this.isUndefined ? "undefined" : "null") + "`.";
+    }
+  };
+  A._JSRandom.prototype = {
+    nextInt$1(max) {
+      if (max <= 0 || max > 4294967296)
+        throw A.wrapException(A.RangeError$("max must be in range 0 < max \u2264 2^32, was " + max));
+      return Math.random() * max >>> 0;
+    }
+  };
+  A.AsyncMemoizer.prototype = {};
+  A.Level.prototype = {
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      return other instanceof A.Level && this.value === other.value;
+    },
+    get$hashCode(_) {
+      return this.value;
+    },
+    toString$0(_) {
+      return this.name;
+    }
+  };
+  A.LogRecord.prototype = {
+    toString$0(_) {
+      return "[" + this.level.name + "] " + this.loggerName + ": " + this.message;
+    }
+  };
+  A.Logger.prototype = {
+    get$fullName() {
+      var t1 = this.parent,
+        t2 = t1 == null ? null : t1.name.length !== 0,
+        t3 = this.name;
+      return t2 === true ? t1.get$fullName() + "." + t3 : t3;
+    },
+    get$level() {
+      var t1, effectiveLevel;
+      if (this.parent == null) {
+        t1 = this._level;
+        t1.toString;
+        effectiveLevel = t1;
+      } else {
+        t1 = $.$get$Logger_root()._level;
+        t1.toString;
+        effectiveLevel = t1;
+      }
+      return effectiveLevel;
+    },
+    log$4(logLevel, message, error, stackTrace) {
+      var record, _this = this,
+        t1 = logLevel.value;
+      if (t1 >= _this.get$level().value) {
+        if (t1 >= 2000) {
+          A.StackTrace_current();
+          logLevel.toString$0(0);
+        }
+        t1 = _this.get$fullName();
+        Date.now();
+        $.LogRecord__nextNumber = $.LogRecord__nextNumber + 1;
+        record = new A.LogRecord(logLevel, message, t1);
+        if (_this.parent == null)
+          _this._publish$1(record);
+        else
+          $.$get$Logger_root()._publish$1(record);
+      }
+    },
+    _publish$1(record) {
+      return null;
+    }
+  };
+  A.Logger_Logger_closure.prototype = {
+    call$0() {
+      var dot, $parent, t1,
+        thisName = this.name;
+      if (B.JSString_methods.startsWith$1(thisName, "."))
+        A.throwExpression(A.ArgumentError$("name shouldn't start with a '.'", null));
+      if (B.JSString_methods.endsWith$1(thisName, "."))
+        A.throwExpression(A.ArgumentError$("name shouldn't end with a '.'", null));
+      dot = B.JSString_methods.lastIndexOf$1(thisName, ".");
+      if (dot === -1)
+        $parent = thisName !== "" ? A.Logger_Logger("") : null;
+      else {
+        $parent = A.Logger_Logger(B.JSString_methods.substring$2(thisName, 0, dot));
+        thisName = B.JSString_methods.substring$1(thisName, dot + 1);
+      }
+      t1 = new A.Logger(thisName, $parent, A.LinkedHashMap_LinkedHashMap$_empty(type$.String, type$.Logger));
+      if ($parent == null)
+        t1._level = B.Level_INFO_800;
+      else
+        $parent._children.$indexSet(0, thisName, t1);
+      return t1;
+    },
+    $signature: 21
+  };
+  A.Pool.prototype = {
+    request$0() {
+      var t1, t2, _this = this;
+      if ((_this._closeMemo._completer.future._state & 30) !== 0)
+        throw A.wrapException(A.StateError$("request() may not be called on a closed Pool."));
+      t1 = _this._allocatedResources;
+      if (t1 < _this._maxAllocatedResources) {
+        _this._allocatedResources = t1 + 1;
+        return A.Future_Future$value(new A.PoolResource(_this), type$.PoolResource);
+      } else {
+        t1 = _this._onReleaseCallbacks;
+        if (!t1.get$isEmpty(0))
+          return _this._runOnRelease$1(t1.removeFirst$0());
+        else {
+          t1 = new A._Future($.Zone__current, type$._Future_PoolResource);
+          t2 = _this._requestedResources;
+          t2._add$1(t2.$ti._precomputed1._as(new A._AsyncCompleter(t1, type$._AsyncCompleter_PoolResource)));
+          _this._resetTimer$0();
+          return t1;
+        }
+      }
+    },
+    withResource$1$1(callback, $T) {
+      return this.withResource$body$Pool($T._eval$1("0/()")._as(callback), $T, $T);
+    },
+    withResource$body$Pool(callback, $T, $async$type) {
+      var $async$goto = 0,
+        $async$completer = A._makeAsyncAwaitCompleter($async$type),
+        $async$returnValue, $async$handler = 2, $async$currentError, $async$next = [], $async$self = this, resource, t1, t2;
+      var $async$withResource$1$1 = A._wrapJsFunctionForAsync(function($async$errorCode, $async$result) {
+        if ($async$errorCode === 1) {
+          $async$currentError = $async$result;
+          $async$goto = $async$handler;
+        }
+        while (true)
+          switch ($async$goto) {
+            case 0:
+              // Function start
+              if (($async$self._closeMemo._completer.future._state & 30) !== 0)
+                throw A.wrapException(A.StateError$("withResource() may not be called on a closed Pool."));
+              $async$goto = 3;
+              return A._asyncAwait($async$self.request$0(), $async$withResource$1$1);
+            case 3:
+              // returning from await.
+              resource = $async$result;
+              $async$handler = 4;
+              t1 = callback.call$0();
+              $async$goto = 7;
+              return A._asyncAwait($T._eval$1("Future<0>")._is(t1) ? t1 : A._Future$value($T._as(t1), $T), $async$withResource$1$1);
+            case 7:
+              // returning from await.
+              t1 = $async$result;
+              $async$returnValue = t1;
+              $async$next = [1];
+              // goto finally
+              $async$goto = 5;
+              break;
+              $async$next.push(6);
+              // goto finally
+              $async$goto = 5;
+              break;
+            case 4:
+              // uncaught
+              $async$next = [2];
+            case 5:
+              // finally
+              $async$handler = 2;
+              t1 = resource;
+              if (t1._released)
+                A.throwExpression(A.StateError$("A PoolResource may only be released once."));
+              t1._released = true;
+              t1 = t1._pool;
+              t1._resetTimer$0();
+              t2 = t1._requestedResources;
+              if (!t2.get$isEmpty(0))
+                t2.removeFirst$0().complete$1(new A.PoolResource(t1));
+              else {
+                t2 = --t1._allocatedResources;
+                if ((t1._closeMemo._completer.future._state & 30) !== 0 && t2 === 0)
+                  null.close$0();
+              }
+              // goto the next finally handler
+              $async$goto = $async$next.pop();
+              break;
+            case 6:
+              // after finally
+            case 1:
+              // return
+              return A._asyncReturn($async$returnValue, $async$completer);
+            case 2:
+              // rethrow
+              return A._asyncRethrow($async$currentError, $async$completer);
+          }
+      });
+      return A._asyncStartSync($async$withResource$1$1, $async$completer);
+    },
+    _runOnRelease$1(onRelease) {
+      var t1 = A.Future_Future$sync(type$.dynamic_Function._as(onRelease), type$.dynamic).then$1$1(new A.Pool__runOnRelease_closure(this), type$.Null),
+        onError = new A.Pool__runOnRelease_closure0(this),
+        t2 = t1.$ti,
+        t3 = $.Zone__current;
+      if (t3 !== B.C__RootZone)
+        onError = A._registerErrorHandler(onError, t3);
+      t1._addListener$1(new A._FutureListener(new A._Future(t3, t2), 2, null, onError, t2._eval$1("@<1>")._bind$1(t2._precomputed1)._eval$1("_FutureListener<1,2>")));
+      t1 = new A._Future($.Zone__current, type$._Future_PoolResource);
+      t2 = this._onReleaseCompleters;
+      t2._add$1(t2.$ti._precomputed1._as(new A._SyncCompleter(t1, type$._SyncCompleter_PoolResource)));
+      return t1;
+    },
+    _resetTimer$0() {
+      var t2,
+        t1 = this._timer;
+      if (t1 == null)
+        return;
+      t2 = this._requestedResources;
+      if (t2._head === t2._tail)
+        t1._restartable_timer$_timer.cancel$0();
+      else {
+        t1._restartable_timer$_timer.cancel$0();
+        t1._restartable_timer$_timer = A.Timer_Timer(t1._restartable_timer$_duration, t1._callback);
+      }
+    }
+  };
+  A.Pool__runOnRelease_closure.prototype = {
+    call$1(value) {
+      var t1 = this.$this;
+      t1._onReleaseCompleters.removeFirst$0().complete$1(new A.PoolResource(t1));
+    },
+    $signature: 4
+  };
+  A.Pool__runOnRelease_closure0.prototype = {
+    call$2(error, stackTrace) {
+      type$.Object._as(error);
+      type$.StackTrace._as(stackTrace);
+      this.$this._onReleaseCompleters.removeFirst$0().completeError$2(error, stackTrace);
+    },
+    $signature: 5
+  };
+  A.PoolResource.prototype = {};
+  A.SseClient.prototype = {
+    SseClient$2$debugKey(serverUrl, debugKey) {
+      var t2, t3, _this = this,
+        t1 = serverUrl + "?sseClientId=" + _this._clientId;
+      _this.__SseClient__serverUrl_A = t1;
+      t2 = type$.JSObject;
+      t1 = t2._as(new self.EventSource(t1, {withCredentials: true}));
+      _this.__SseClient__eventSource_A = t1;
+      new A._EventStream(t1, "open", false, type$._EventStream_JSObject).get$first(0).whenComplete$1(new A.SseClient_closure(_this));
+      t1 = type$.Function;
+      t3 = type$.JavaScriptFunction;
+      _this.__SseClient__eventSource_A.addEventListener("message", t3._as(A.allowInterop(_this.get$_onIncomingMessage(), t1)));
+      _this.__SseClient__eventSource_A.addEventListener("control", t3._as(A.allowInterop(_this.get$_onIncomingControlMessage(), t1)));
+      t1 = type$.nullable_void_Function_JSObject;
+      A._EventStreamSubscription$(_this.__SseClient__eventSource_A, "open", t1._as(new A.SseClient_closure0(_this)), false, t2);
+      A._EventStreamSubscription$(_this.__SseClient__eventSource_A, "error", t1._as(new A.SseClient_closure1(_this)), false, t2);
+    },
+    close$0() {
+      var _this = this,
+        t1 = _this.__SseClient__eventSource_A;
+      t1 === $ && A.throwLateFieldNI("_eventSource");
+      t1.close();
+      if ((_this._onConnected.future._state & 30) === 0) {
+        t1 = _this._outgoingController;
+        new A._ControllerStream(t1, A._instanceType(t1)._eval$1("_ControllerStream<1>")).listen$2$cancelOnError(null, true).asFuture$1$1(null, type$.void);
+      }
+      _this._incomingController.close$0();
+      _this._outgoingController.close$0();
+    },
+    _closeWithError$1(error) {
+      var stackTrace, t2,
+        t1 = this._incomingController;
+      A.checkNotNullable(error, "error", type$.Object);
+      if (t1._state >= 4)
+        A.throwExpression(t1._badEventState$0());
+      stackTrace = A.AsyncError_defaultStackTrace(error);
+      t2 = t1._state;
+      if ((t2 & 1) !== 0)
+        t1._sendError$2(error, stackTrace);
+      else if ((t2 & 3) === 0)
+        t1._ensurePendingEvents$0().add$1(0, new A._DelayedError(error, stackTrace));
+      this.close$0();
+      t1 = this._onConnected;
+      if ((t1.future._state & 30) === 0)
+        t1.completeError$1(error);
+    },
+    _onIncomingControlMessage$1(message) {
+      var data = type$.JSObject._as(message).data;
+      if (J.$eq$(A.dartify(data), "close"))
+        this.close$0();
+      else
+        throw A.wrapException(A.UnsupportedError$("[" + this._clientId + '] Illegal Control Message "' + A.S(data) + '"'));
+    },
+    _onIncomingMessage$1(message) {
+      this._incomingController.add$1(0, A._asString(B.C_JsonCodec.decode$2$reviver(A._asString(type$.JSObject._as(message).data), null)));
+    },
+    _onOutgoingDone$0() {
+      this.close$0();
+    },
+    _onOutgoingMessage$1(message) {
+      return this._onOutgoingMessage$body$SseClient(A._asStringQ(message));
+    },
+    _onOutgoingMessage$body$SseClient(message) {
+      var $async$goto = 0,
+        $async$completer = A._makeAsyncAwaitCompleter(type$.void),
+        $async$self = this, t1;
+      var $async$_onOutgoingMessage$1 = A._wrapJsFunctionForAsync(function($async$errorCode, $async$result) {
+        if ($async$errorCode === 1)
+          return A._asyncRethrow($async$result, $async$completer);
+        while (true)
+          switch ($async$goto) {
+            case 0:
+              // Function start
+              t1 = {};
+              t1.encodedMessage = null;
+              $async$goto = 2;
+              return A._asyncAwait($.$get$_requestPool().withResource$1$1(new A.SseClient__onOutgoingMessage_closure(t1, $async$self, message), type$.Null), $async$_onOutgoingMessage$1);
+            case 2:
+              // returning from await.
+              // implicit return
+              return A._asyncReturn(null, $async$completer);
+          }
+      });
+      return A._asyncStartSync($async$_onOutgoingMessage$1, $async$completer);
+    }
+  };
+  A.SseClient_closure.prototype = {
+    call$0() {
+      var t2,
+        t1 = this.$this;
+      t1._onConnected.complete$0();
+      t2 = t1._outgoingController;
+      new A._ControllerStream(t2, A._instanceType(t2)._eval$1("_ControllerStream<1>")).listen$2$onDone(t1.get$_onOutgoingMessage(), t1.get$_onOutgoingDone());
+    },
+    $signature: 2
+  };
+  A.SseClient_closure0.prototype = {
+    call$1(_) {
+      var t1 = this.$this._errorTimer;
+      if (t1 != null)
+        t1.cancel$0();
+    },
+    $signature: 1
+  };
+  A.SseClient_closure1.prototype = {
+    call$1(error) {
+      var t1 = this.$this,
+        t2 = t1._errorTimer;
+      t2 = t2 == null ? null : t2._handle != null;
+      if (t2 !== true)
+        t1._errorTimer = A.Timer_Timer(B.Duration_5000000, new A.SseClient__closure(t1, error));
+    },
+    $signature: 1
+  };
+  A.SseClient__closure.prototype = {
+    call$0() {
+      this.$this._closeWithError$1(this.error);
+    },
+    $signature: 0
+  };
+  A.SseClient__onOutgoingMessage_closure.prototype = {
+    call$0() {
+      var $async$goto = 0,
+        $async$completer = A._makeAsyncAwaitCompleter(type$.Null),
+        $async$handler = 1, $async$currentError, $async$self = this, e, e0, url, error, augmentedError, exception, t1, t2, $async$exception;
+      var $async$call$0 = A._wrapJsFunctionForAsync(function($async$errorCode, $async$result) {
+        if ($async$errorCode === 1) {
+          $async$currentError = $async$result;
+          $async$goto = $async$handler;
+        }
+        while (true)
+          switch ($async$goto) {
+            case 0:
+              // Function start
+              try {
+                $async$self._box_0.encodedMessage = B.C_JsonCodec.encode$2$toEncodable($async$self.message, null);
+              } catch (exception) {
+                t1 = A.unwrapException(exception);
+                if (t1 instanceof A.JsonUnsupportedObjectError) {
+                  e = t1;
+                  t1 = $async$self.$this;
+                  t1._logger.log$4(B.Level_WARNING_900, "[" + t1._clientId + "] Unable to encode outgoing message: " + A.S(e), null, null);
+                } else if (t1 instanceof A.ArgumentError) {
+                  e0 = t1;
+                  t1 = $async$self.$this;
+                  t1._logger.log$4(B.Level_WARNING_900, "[" + t1._clientId + "] Invalid argument: " + A.S(e0), null, null);
+                } else
+                  throw exception;
+              }
+              $async$handler = 3;
+              t1 = $async$self.$this;
+              t2 = t1.__SseClient__serverUrl_A;
+              t2 === $ && A.throwLateFieldNI("_serverUrl");
+              url = t2 + "&messageId=" + ++t1._lastMessageId;
+              t1 = $async$self._box_0.encodedMessage;
+              if (t1 == null)
+                t1 = null;
+              t1 = {method: "POST", body: t1, credentials: "include"};
+              t2 = type$.JSObject;
+              $async$goto = 6;
+              return A._asyncAwait(A.promiseToFuture(t2._as(t2._as(self.window).fetch(url, t1)), t2), $async$call$0);
+            case 6:
+              // returning from await.
+              $async$handler = 1;
+              // goto after finally
+              $async$goto = 5;
+              break;
+            case 3:
+              // catch
+              $async$handler = 2;
+              $async$exception = $async$currentError;
+              error = A.unwrapException($async$exception);
+              t1 = $async$self.$this;
+              augmentedError = "[" + t1._clientId + "] SSE client failed to send " + A.S($async$self.message) + ":\n " + A.S(error);
+              t1._logger.log$4(B.Level_SEVERE_1000, augmentedError, null, null);
+              t1._closeWithError$1(augmentedError);
+              // goto after finally
+              $async$goto = 5;
+              break;
+            case 2:
+              // uncaught
+              // goto rethrow
+              $async$goto = 1;
+              break;
+            case 5:
+              // after finally
+              // implicit return
+              return A._asyncReturn(null, $async$completer);
+            case 1:
+              // rethrow
+              return A._asyncRethrow($async$currentError, $async$completer);
+          }
+      });
+      return A._asyncStartSync($async$call$0, $async$completer);
+    },
+    $signature: 7
+  };
+  A.generateUuidV4_generateBits.prototype = {
+    call$1(bitCount) {
+      return this.random.nextInt$1(B.JSInt_methods._shlPositive$1(1, bitCount));
+    },
+    $signature: 23
+  };
+  A.generateUuidV4_printDigits.prototype = {
+    call$2(value, count) {
+      return B.JSString_methods.padLeft$2(B.JSInt_methods.toRadixString$1(value, 16), count, "0");
+    },
+    $signature: 11
+  };
+  A.generateUuidV4_bitsDigits.prototype = {
+    call$2(bitCount, digitCount) {
+      return this.printDigits.call$2(this.generateBits.call$1(bitCount), digitCount);
+    },
+    $signature: 11
+  };
+  A.StreamChannelMixin.prototype = {};
+  A.EventStreamProvider.prototype = {};
+  A._EventStream.prototype = {
+    listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, onError) {
+      var t1 = A._instanceType(this);
+      t1._eval$1("~(1)?")._as(onData);
+      type$.nullable_void_Function._as(onDone);
+      return A._EventStreamSubscription$(this._target, this._eventType, onData, false, t1._precomputed1);
+    }
+  };
+  A._ElementEventStreamImpl.prototype = {};
+  A._EventStreamSubscription.prototype = {
+    cancel$0() {
+      var _this = this,
+        emptyFuture = A.Future_Future$value(null, type$.void);
+      if (_this._target == null)
+        return emptyFuture;
+      _this._unlisten$0();
+      _this._streams$_onData = _this._target = null;
+      return emptyFuture;
+    },
+    onData$1(handleData) {
+      var t1, _this = this;
+      _this.$ti._eval$1("~(1)?")._as(handleData);
+      if (_this._target == null)
+        throw A.wrapException(A.StateError$("Subscription has been canceled."));
+      _this._unlisten$0();
+      t1 = A._wrapZone(new A._EventStreamSubscription_onData_closure(handleData), type$.JSObject);
+      t1 = t1 == null ? null : type$.JavaScriptFunction._as(A.allowInterop(t1, type$.Function));
+      _this._streams$_onData = t1;
+      _this._tryResume$0();
+    },
+    _tryResume$0() {
+      var t1 = this._streams$_onData;
+      if (t1 != null)
+        this._target.addEventListener(this._eventType, t1, false);
+    },
+    _unlisten$0() {
+      var t1 = this._streams$_onData;
+      if (t1 != null)
+        this._target.removeEventListener(this._eventType, t1, false);
+    },
+    $isStreamSubscription: 1
+  };
+  A._EventStreamSubscription_closure.prototype = {
+    call$1(e) {
+      return this.onData.call$1(type$.JSObject._as(e));
+    },
+    $signature: 1
+  };
+  A._EventStreamSubscription_onData_closure.prototype = {
+    call$1(e) {
+      return this.handleData.call$1(type$.JSObject._as(e));
+    },
+    $signature: 1
+  };
+  A.main_closure.prototype = {
+    call$1(_) {
+      this.channel._outgoingController.close$0();
+    },
+    $signature: 1
+  };
+  A.main_closure0.prototype = {
+    call$1(s) {
+      var count, t1, t2, t3, i, t4, t5, lastEvent;
+      A._asString(s);
+      if (B.JSString_methods.startsWith$1(s, "send ")) {
+        count = A.int_parse(B.JSArray_methods.get$last(s.split(" ")), null);
+        for (t1 = this.channel._outgoingController, t2 = A._instanceType(t1), t3 = t2._precomputed1, t2 = t2._eval$1("_DelayedData<1>"), i = 0; i < count; ++i) {
+          t4 = t3._as("" + i);
+          t5 = t1._state;
+          if (t5 >= 4)
+            A.throwExpression(t1._badEventState$0());
+          if ((t5 & 1) !== 0)
+            t1._sendData$1(t4);
+          else if ((t5 & 3) === 0) {
+            t5 = t1._ensurePendingEvents$0();
+            t4 = new A._DelayedData(t4, t2);
+            lastEvent = t5.lastPendingEvent;
+            if (lastEvent == null)
+              t5.firstPendingEvent = t5.lastPendingEvent = t4;
+            else {
+              lastEvent.set$next(t4);
+              t5.lastPendingEvent = t4;
+            }
+          }
+        }
+      } else {
+        t1 = this.channel._outgoingController;
+        t1.add$1(0, A._instanceType(t1)._precomputed1._as(s));
+      }
+    },
+    $signature: 24
+  };
+  (function aliases() {
+    var _ = J.LegacyJavaScriptObject.prototype;
+    _.super$LegacyJavaScriptObject$toString = _.toString$0;
+  })();
+  (function installTearOffs() {
+    var _static_1 = hunkHelpers._static_1,
+      _static_0 = hunkHelpers._static_0,
+      _static_2 = hunkHelpers._static_2,
+      _instance_2_u = hunkHelpers._instance_2u,
+      _instance_1_u = hunkHelpers._instance_1u,
+      _instance_0_u = hunkHelpers._instance_0u;
+    _static_1(A, "async__AsyncRun__scheduleImmediateJsOverride$closure", "_AsyncRun__scheduleImmediateJsOverride", 6);
+    _static_1(A, "async__AsyncRun__scheduleImmediateWithSetImmediate$closure", "_AsyncRun__scheduleImmediateWithSetImmediate", 6);
+    _static_1(A, "async__AsyncRun__scheduleImmediateWithTimer$closure", "_AsyncRun__scheduleImmediateWithTimer", 6);
+    _static_0(A, "async___startMicrotaskLoop$closure", "_startMicrotaskLoop", 0);
+    _static_1(A, "async___nullDataHandler$closure", "_nullDataHandler", 3);
+    _static_2(A, "async___nullErrorHandler$closure", "_nullErrorHandler", 9);
+    _static_0(A, "async___nullDoneHandler$closure", "_nullDoneHandler", 0);
+    _instance_2_u(A._Future.prototype, "get$_completeError", "_completeError$2", 9);
+    _static_1(A, "convert___defaultToEncodable$closure", "_defaultToEncodable", 8);
+    var _;
+    _instance_1_u(_ = A.SseClient.prototype, "get$_onIncomingControlMessage", "_onIncomingControlMessage$1", 1);
+    _instance_1_u(_, "get$_onIncomingMessage", "_onIncomingMessage$1", 1);
+    _instance_0_u(_, "get$_onOutgoingDone", "_onOutgoingDone$0", 0);
+    _instance_1_u(_, "get$_onOutgoingMessage", "_onOutgoingMessage$1", 22);
+  })();
+  (function inheritance() {
+    var _mixin = hunkHelpers.mixin,
+      _inherit = hunkHelpers.inherit,
+      _inheritMany = hunkHelpers.inheritMany;
+    _inherit(A.Object, null);
+    _inheritMany(A.Object, [A.JS_CONST, J.Interceptor, J.ArrayIterator, A.Error, A.Closure, A.Iterable, A.ListIterator, A.FixedLengthListMixin, A.Symbol, A.MapView, A.ConstantMap, A.JSInvocationMirror, A.TypeErrorDecoder, A.NullThrownFromJavaScriptException, A.ExceptionAndStackTrace, A._StackTrace, A._Required, A.MapBase, A.LinkedHashMapCell, A.LinkedHashMapKeyIterator, A.StringMatch, A.Rti, A._FunctionParameters, A._Type, A._TimerImpl, A._AsyncAwaitCompleter, A.AsyncError, A._Completer, A._FutureListener, A._Future, A._AsyncCallbackEntry, A.Stream, A._StreamController, A._AsyncStreamControllerDispatch, A._BufferingStreamSubscription, A._StreamSinkWrapper, A._DelayedEvent, A._DelayedDone, A._PendingEvents, A._StreamIterator, A._Zone, A._HashMapKeyIterator, A.ListBase, A._UnmodifiableMapMixin, A._ListQueueIterator, A.Codec, A.Converter, A._JsonStringifier, A.DateTime, A.Duration, A.OutOfMemoryError, A.StackOverflowError, A._Exception, A.FormatException, A.Null, A._StringStackTrace, A.StringBuffer, A.NullRejectionException, A._JSRandom, A.AsyncMemoizer, A.Level, A.LogRecord, A.Logger, A.Pool, A.PoolResource, A.StreamChannelMixin, A.EventStreamProvider, A._EventStreamSubscription]);
+    _inheritMany(J.Interceptor, [J.JSBool, J.JSNull, J.JavaScriptObject, J.JavaScriptBigInt, J.JavaScriptSymbol, J.JSNumber, J.JSString]);
+    _inheritMany(J.JavaScriptObject, [J.LegacyJavaScriptObject, J.JSArray, A.NativeByteBuffer, A.NativeTypedData]);
+    _inheritMany(J.LegacyJavaScriptObject, [J.PlainJavaScriptObject, J.UnknownJavaScriptObject, J.JavaScriptFunction]);
+    _inherit(J.JSUnmodifiableArray, J.JSArray);
+    _inheritMany(J.JSNumber, [J.JSInt, J.JSNumNotInt]);
+    _inheritMany(A.Error, [A.LateError, A.TypeError, A.JsNoSuchMethodError, A.UnknownJsTypeError, A._CyclicInitializationError, A.RuntimeError, A._Error, A.JsonUnsupportedObjectError, A.AssertionError, A.ArgumentError, A.NoSuchMethodError, A.UnsupportedError, A.UnimplementedError, A.StateError, A.ConcurrentModificationError]);
+    _inheritMany(A.Closure, [A.Closure0Args, A.Closure2Args, A.TearOffClosure, A.initHooks_closure, A.initHooks_closure1, A._AsyncRun__initializeScheduleImmediate_internalCallback, A._AsyncRun__initializeScheduleImmediate_closure, A._awaitOnObject_closure, A._Future__chainForeignFuture_closure, A._Future__propagateToListeners_handleWhenCompleteCallback_closure, A.Stream_length_closure, A.Stream_first_closure0, A._RootZone_bindUnaryCallbackGuarded_closure, A.promiseToFuture_closure, A.promiseToFuture_closure0, A.dartify_convert, A.Pool__runOnRelease_closure, A.SseClient_closure0, A.SseClient_closure1, A.generateUuidV4_generateBits, A._EventStreamSubscription_closure, A._EventStreamSubscription_onData_closure, A.main_closure, A.main_closure0]);
+    _inheritMany(A.Closure0Args, [A.nullFuture_closure, A._AsyncRun__scheduleImmediateJsOverride_internalCallback, A._AsyncRun__scheduleImmediateWithSetImmediate_internalCallback, A._TimerImpl_internalCallback, A._Future__addListener_closure, A._Future__prependListeners_closure, A._Future__chainForeignFuture_closure1, A._Future__chainCoreFutureAsync_closure, A._Future__asyncCompleteWithValue_closure, A._Future__asyncCompleteError_closure, A._Future__propagateToListeners_handleWhenCompleteCallback, A._Future__propagateToListeners_handleValueCallback, A._Future__propagateToListeners_handleError, A.Stream_length_closure0, A.Stream_first_closure, A._StreamController__subscribe_closure, A._StreamController__recordCancel_complete, A._BufferingStreamSubscription_asFuture_closure, A._BufferingStreamSubscription_asFuture__closure, A._BufferingStreamSubscription__sendError_sendError, A._BufferingStreamSubscription__sendDone_sendDone, A._PendingEvents_schedule_closure, A._cancelAndValue_closure, A._rootHandleError_closure, A._RootZone_bindCallbackGuarded_closure, A.Logger_Logger_closure, A.SseClient_closure, A.SseClient__closure, A.SseClient__onOutgoingMessage_closure]);
+    _inherit(A.EfficientLengthIterable, A.Iterable);
+    _inheritMany(A.EfficientLengthIterable, [A.ListIterable, A.LinkedHashMapKeyIterable, A._HashMapKeyIterable]);
+    _inherit(A._UnmodifiableMapView_MapView__UnmodifiableMapMixin, A.MapView);
+    _inherit(A.UnmodifiableMapView, A._UnmodifiableMapView_MapView__UnmodifiableMapMixin);
+    _inherit(A.ConstantMapView, A.UnmodifiableMapView);
+    _inherit(A.ConstantStringMap, A.ConstantMap);
+    _inheritMany(A.Closure2Args, [A.Primitives_functionNoSuchMethod_closure, A.initHooks_closure0, A._awaitOnObject_closure0, A._wrapJsFunctionForAsync_closure, A._Future__chainForeignFuture_closure0, A._BufferingStreamSubscription_asFuture_closure0, A.MapBase_mapToString_closure, A._JsonStringifier_writeMap_closure, A.NoSuchMethodError_toString_closure, A.Pool__runOnRelease_closure0, A.generateUuidV4_printDigits, A.generateUuidV4_bitsDigits]);
+    _inherit(A.NullError, A.TypeError);
+    _inheritMany(A.TearOffClosure, [A.StaticClosure, A.BoundClosure]);
+    _inheritMany(A.MapBase, [A.JsLinkedHashMap, A._HashMap, A._JsonMap]);
+    _inheritMany(A.NativeTypedData, [A.NativeByteData, A.NativeTypedArray]);
+    _inheritMany(A.NativeTypedArray, [A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin, A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin]);
+    _inherit(A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin, A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin);
+    _inherit(A.NativeTypedArrayOfDouble, A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin);
+    _inherit(A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin, A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin);
+    _inherit(A.NativeTypedArrayOfInt, A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin);
+    _inheritMany(A.NativeTypedArrayOfDouble, [A.NativeFloat32List, A.NativeFloat64List]);
+    _inheritMany(A.NativeTypedArrayOfInt, [A.NativeInt16List, A.NativeInt32List, A.NativeInt8List, A.NativeUint16List, A.NativeUint32List, A.NativeUint8ClampedList, A.NativeUint8List]);
+    _inherit(A._TypeError, A._Error);
+    _inheritMany(A._Completer, [A._AsyncCompleter, A._SyncCompleter]);
+    _inherit(A._AsyncStreamController, A._StreamController);
+    _inheritMany(A.Stream, [A._StreamImpl, A._EventStream]);
+    _inherit(A._ControllerStream, A._StreamImpl);
+    _inherit(A._ControllerSubscription, A._BufferingStreamSubscription);
+    _inheritMany(A._DelayedEvent, [A._DelayedData, A._DelayedError]);
+    _inherit(A._RootZone, A._Zone);
+    _inherit(A._IdentityHashMap, A._HashMap);
+    _inheritMany(A.ListIterable, [A.ListQueue, A._JsonMapKeyIterable]);
+    _inherit(A.JsonCyclicError, A.JsonUnsupportedObjectError);
+    _inherit(A.JsonCodec, A.Codec);
+    _inheritMany(A.Converter, [A.JsonEncoder, A.JsonDecoder]);
+    _inherit(A._JsonStringStringifier, A._JsonStringifier);
+    _inheritMany(A.ArgumentError, [A.RangeError, A.IndexError]);
+    _inherit(A.SseClient, A.StreamChannelMixin);
+    _inherit(A._ElementEventStreamImpl, A._EventStream);
+    _mixin(A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin, A.ListBase);
+    _mixin(A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin, A.FixedLengthListMixin);
+    _mixin(A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin, A.ListBase);
+    _mixin(A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin, A.FixedLengthListMixin);
+    _mixin(A._AsyncStreamController, A._AsyncStreamControllerDispatch);
+    _mixin(A._UnmodifiableMapView_MapView__UnmodifiableMapMixin, A._UnmodifiableMapMixin);
+  })();
+  var init = {
+    typeUniverse: {eC: new Map(), tR: {}, eT: {}, tPV: {}, sEA: []},
+    mangledGlobalNames: {int: "int", double: "double", num: "num", String: "String", bool: "bool", Null: "Null", List: "List", Object: "Object", Map: "Map"},
+    mangledNames: {},
+    types: ["~()", "~(JSObject)", "Null()", "~(@)", "Null(@)", "Null(Object,StackTrace)", "~(~())", "Future<Null>()", "@(@)", "~(Object,StackTrace)", "~(Object?,Object?)", "String(int,int)", "~(String,@)", "@(@,String)", "@(String)", "Null(~())", "Null(@,StackTrace)", "~(int,@)", "_Future<@>(@)", "~(Symbol0,@)", "Object?(Object?)", "Logger()", "~(String?)", "int(int)", "~(String)"],
+    interceptorsByTag: null,
+    leafTags: null,
+    arrayRti: Symbol("$ti")
+  };
+  A._Universe_addRules(init.typeUniverse, JSON.parse('{"PlainJavaScriptObject":"LegacyJavaScriptObject","UnknownJavaScriptObject":"LegacyJavaScriptObject","JavaScriptFunction":"LegacyJavaScriptObject","JSBool":{"bool":[],"TrustedGetRuntimeType":[]},"JSNull":{"Null":[],"TrustedGetRuntimeType":[]},"JavaScriptObject":{"JSObject":[]},"LegacyJavaScriptObject":{"JSObject":[]},"JSArray":{"List":["1"],"JSObject":[],"Iterable":["1"]},"JSUnmodifiableArray":{"JSArray":["1"],"List":["1"],"JSObject":[],"Iterable":["1"]},"JSNumber":{"double":[],"num":[]},"JSInt":{"double":[],"int":[],"num":[],"TrustedGetRuntimeType":[]},"JSNumNotInt":{"double":[],"num":[],"TrustedGetRuntimeType":[]},"JSString":{"String":[],"Pattern":[],"TrustedGetRuntimeType":[]},"LateError":{"Error":[]},"EfficientLengthIterable":{"Iterable":["1"]},"ListIterable":{"Iterable":["1"]},"Symbol":{"Symbol0":[]},"ConstantMapView":{"UnmodifiableMapView":["1","2"],"_UnmodifiableMapView_MapView__UnmodifiableMapMixin":["1","2"],"MapView":["1","2"],"_UnmodifiableMapMixin":["1","2"],"Map":["1","2"]},"ConstantMap":{"Map":["1","2"]},"ConstantStringMap":{"ConstantMap":["1","2"],"Map":["1","2"]},"JSInvocationMirror":{"Invocation":[]},"NullError":{"TypeError":[],"Error":[]},"JsNoSuchMethodError":{"Error":[]},"UnknownJsTypeError":{"Error":[]},"_StackTrace":{"StackTrace":[]},"Closure":{"Function":[]},"Closure0Args":{"Function":[]},"Closure2Args":{"Function":[]},"TearOffClosure":{"Function":[]},"StaticClosure":{"Function":[]},"BoundClosure":{"Function":[]},"_CyclicInitializationError":{"Error":[]},"RuntimeError":{"Error":[]},"JsLinkedHashMap":{"MapBase":["1","2"],"Map":["1","2"],"MapBase.K":"1","MapBase.V":"2"},"LinkedHashMapKeyIterable":{"Iterable":["1"]},"NativeByteBuffer":{"JSObject":[],"TrustedGetRuntimeType":[]},"NativeTypedData":{"JSObject":[]},"NativeByteData":{"JSObject":[],"TrustedGetRuntimeType":[]},"NativeTypedArray":{"JavaScriptIndexingBehavior":["1"],"JSObject":[]},"NativeTypedArrayOfDouble":{"ListBase":["double"],"List":["double"],"JavaScriptIndexingBehavior":["double"],"JSObject":[],"Iterable":["double"],"FixedLengthListMixin":["double"]},"NativeTypedArrayOfInt":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JSObject":[],"Iterable":["int"],"FixedLengthListMixin":["int"]},"NativeFloat32List":{"ListBase":["double"],"List":["double"],"JavaScriptIndexingBehavior":["double"],"JSObject":[],"Iterable":["double"],"FixedLengthListMixin":["double"],"TrustedGetRuntimeType":[],"ListBase.E":"double"},"NativeFloat64List":{"ListBase":["double"],"List":["double"],"JavaScriptIndexingBehavior":["double"],"JSObject":[],"Iterable":["double"],"FixedLengthListMixin":["double"],"TrustedGetRuntimeType":[],"ListBase.E":"double"},"NativeInt16List":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JSObject":[],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeInt32List":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JSObject":[],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeInt8List":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JSObject":[],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint16List":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JSObject":[],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint32List":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JSObject":[],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint8ClampedList":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JSObject":[],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint8List":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JSObject":[],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"_Error":{"Error":[]},"_TypeError":{"TypeError":[],"Error":[]},"_Future":{"Future":["1"]},"_TimerImpl":{"Timer":[]},"_AsyncAwaitCompleter":{"Completer":["1"]},"AsyncError":{"Error":[]},"_Completer":{"Completer":["1"]},"_AsyncCompleter":{"_Completer":["1"],"Completer":["1"]},"_SyncCompleter":{"_Completer":["1"],"Completer":["1"]},"_StreamController":{"StreamController":["1"],"_StreamControllerLifecycle":["1"],"_EventDispatch":["1"]},"_AsyncStreamController":{"_AsyncStreamControllerDispatch":["1"],"_StreamController":["1"],"StreamController":["1"],"_StreamControllerLifecycle":["1"],"_EventDispatch":["1"]},"_ControllerStream":{"_StreamImpl":["1"],"Stream":["1"]},"_ControllerSubscription":{"_BufferingStreamSubscription":["1"],"StreamSubscription":["1"],"_EventDispatch":["1"]},"_BufferingStreamSubscription":{"StreamSubscription":["1"],"_EventDispatch":["1"]},"_StreamImpl":{"Stream":["1"]},"_DelayedData":{"_DelayedEvent":["1"]},"_DelayedError":{"_DelayedEvent":["@"]},"_DelayedDone":{"_DelayedEvent":["@"]},"_Zone":{"Zone":[]},"_RootZone":{"_Zone":[],"Zone":[]},"_HashMap":{"MapBase":["1","2"],"Map":["1","2"]},"_IdentityHashMap":{"_HashMap":["1","2"],"MapBase":["1","2"],"Map":["1","2"],"MapBase.K":"1","MapBase.V":"2"},"_HashMapKeyIterable":{"Iterable":["1"]},"MapBase":{"Map":["1","2"]},"MapView":{"Map":["1","2"]},"UnmodifiableMapView":{"_UnmodifiableMapView_MapView__UnmodifiableMapMixin":["1","2"],"MapView":["1","2"],"_UnmodifiableMapMixin":["1","2"],"Map":["1","2"]},"ListQueue":{"Queue":["1"],"ListIterable":["1"],"Iterable":["1"],"ListIterable.E":"1"},"_JsonMap":{"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"_JsonMapKeyIterable":{"ListIterable":["String"],"Iterable":["String"],"ListIterable.E":"String"},"JsonUnsupportedObjectError":{"Error":[]},"JsonCyclicError":{"Error":[]},"JsonCodec":{"Codec":["Object?","String"]},"JsonEncoder":{"Converter":["Object?","String"]},"JsonDecoder":{"Converter":["String","Object?"]},"double":{"num":[]},"int":{"num":[]},"String":{"Pattern":[]},"AssertionError":{"Error":[]},"TypeError":{"Error":[]},"ArgumentError":{"Error":[]},"RangeError":{"Error":[]},"IndexError":{"Error":[]},"NoSuchMethodError":{"Error":[]},"UnsupportedError":{"Error":[]},"UnimplementedError":{"Error":[]},"StateError":{"Error":[]},"ConcurrentModificationError":{"Error":[]},"OutOfMemoryError":{"Error":[]},"StackOverflowError":{"Error":[]},"_StringStackTrace":{"StackTrace":[]},"StringBuffer":{"StringSink":[]},"_EventStream":{"Stream":["1"]},"_ElementEventStreamImpl":{"_EventStream":["1"],"Stream":["1"]},"_EventStreamSubscription":{"StreamSubscription":["1"]},"Int8List":{"List":["int"],"Iterable":["int"]},"Uint8List":{"List":["int"],"Iterable":["int"]},"Uint8ClampedList":{"List":["int"],"Iterable":["int"]},"Int16List":{"List":["int"],"Iterable":["int"]},"Uint16List":{"List":["int"],"Iterable":["int"]},"Int32List":{"List":["int"],"Iterable":["int"]},"Uint32List":{"List":["int"],"Iterable":["int"]},"Float32List":{"List":["double"],"Iterable":["double"]},"Float64List":{"List":["double"],"Iterable":["double"]}}'));
+  A._Universe_addErasedTypes(init.typeUniverse, JSON.parse('{"EfficientLengthIterable":1,"NativeTypedArray":1,"_DelayedEvent":1,"StreamChannelMixin":1}'));
+  var string$ = {
+    Error_: "Error handler must accept one Object or one Object and a StackTrace as arguments, and return a value of the returned future's type"
+  };
+  var type$ = (function rtii() {
+    var findType = A.findType;
+    return {
+      $env_1_1_void: findType("@<~>"),
+      AsyncError: findType("AsyncError"),
+      ConstantMapView_Symbol_dynamic: findType("ConstantMapView<Symbol0,@>"),
+      Error: findType("Error"),
+      Function: findType("Function"),
+      Future_dynamic: findType("Future<@>"),
+      Invocation: findType("Invocation"),
+      Iterable_dynamic: findType("Iterable<@>"),
+      JSArray_String: findType("JSArray<String>"),
+      JSArray_dynamic: findType("JSArray<@>"),
+      JSNull: findType("JSNull"),
+      JSObject: findType("JSObject"),
+      JavaScriptFunction: findType("JavaScriptFunction"),
+      JavaScriptIndexingBehavior_dynamic: findType("JavaScriptIndexingBehavior<@>"),
+      JsLinkedHashMap_Symbol_dynamic: findType("JsLinkedHashMap<Symbol0,@>"),
+      List_dynamic: findType("List<@>"),
+      Logger: findType("Logger"),
+      Map_dynamic_dynamic: findType("Map<@,@>"),
+      Null: findType("Null"),
+      Object: findType("Object"),
+      PoolResource: findType("PoolResource"),
+      Record: findType("Record"),
+      StackTrace: findType("StackTrace"),
+      String: findType("String"),
+      Symbol: findType("Symbol0"),
+      TrustedGetRuntimeType: findType("TrustedGetRuntimeType"),
+      TypeError: findType("TypeError"),
+      UnknownJavaScriptObject: findType("UnknownJavaScriptObject"),
+      _AsyncCompleter_PoolResource: findType("_AsyncCompleter<PoolResource>"),
+      _AsyncCompleter_void: findType("_AsyncCompleter<~>"),
+      _ElementEventStreamImpl_JSObject: findType("_ElementEventStreamImpl<JSObject>"),
+      _EventStream_JSObject: findType("_EventStream<JSObject>"),
+      _Future_PoolResource: findType("_Future<PoolResource>"),
+      _Future_dynamic: findType("_Future<@>"),
+      _Future_int: findType("_Future<int>"),
+      _Future_void: findType("_Future<~>"),
+      _IdentityHashMap_of_nullable_Object_and_nullable_Object: findType("_IdentityHashMap<Object?,Object?>"),
+      _StreamControllerAddStreamState_nullable_Object: findType("_StreamControllerAddStreamState<Object?>"),
+      _SyncCompleter_PoolResource: findType("_SyncCompleter<PoolResource>"),
+      bool: findType("bool"),
+      bool_Function_Object: findType("bool(Object)"),
+      double: findType("double"),
+      dynamic: findType("@"),
+      dynamic_Function: findType("@()"),
+      dynamic_Function_Object: findType("@(Object)"),
+      dynamic_Function_Object_StackTrace: findType("@(Object,StackTrace)"),
+      int: findType("int"),
+      legacy_Never: findType("0&*"),
+      legacy_Object: findType("Object*"),
+      nullable_Future_Null: findType("Future<Null>?"),
+      nullable_JSObject: findType("JSObject?"),
+      nullable_List_dynamic: findType("List<@>?"),
+      nullable_Object: findType("Object?"),
+      nullable_StackTrace: findType("StackTrace?"),
+      nullable__DelayedEvent_dynamic: findType("_DelayedEvent<@>?"),
+      nullable__FutureListener_dynamic_dynamic: findType("_FutureListener<@,@>?"),
+      nullable_void_Function: findType("~()?"),
+      nullable_void_Function_JSObject: findType("~(JSObject)?"),
+      num: findType("num"),
+      void: findType("~"),
+      void_Function: findType("~()"),
+      void_Function_Object: findType("~(Object)"),
+      void_Function_Object_StackTrace: findType("~(Object,StackTrace)"),
+      void_Function_String_dynamic: findType("~(String,@)")
+    };
+  })();
+  (function constants() {
+    var makeConstList = hunkHelpers.makeConstList;
+    B.Interceptor_methods = J.Interceptor.prototype;
+    B.JSArray_methods = J.JSArray.prototype;
+    B.JSInt_methods = J.JSInt.prototype;
+    B.JSNumber_methods = J.JSNumber.prototype;
+    B.JSString_methods = J.JSString.prototype;
+    B.JavaScriptFunction_methods = J.JavaScriptFunction.prototype;
+    B.JavaScriptObject_methods = J.JavaScriptObject.prototype;
+    B.PlainJavaScriptObject_methods = J.PlainJavaScriptObject.prototype;
+    B.UnknownJavaScriptObject_methods = J.UnknownJavaScriptObject.prototype;
+    B.C_JS_CONST = function getTagFallback(o) {
+  var s = Object.prototype.toString.call(o);
+  return s.substring(8, s.length - 1);
+};
+    B.C_JS_CONST0 = function() {
+  var toStringFunction = Object.prototype.toString;
+  function getTag(o) {
+    var s = toStringFunction.call(o);
+    return s.substring(8, s.length - 1);
+  }
+  function getUnknownTag(object, tag) {
+    if (/^HTML[A-Z].*Element$/.test(tag)) {
+      var name = toStringFunction.call(object);
+      if (name == "[object Object]") return null;
+      return "HTMLElement";
+    }
+  }
+  function getUnknownTagGenericBrowser(object, tag) {
+    if (object instanceof HTMLElement) return "HTMLElement";
+    return getUnknownTag(object, tag);
+  }
+  function prototypeForTag(tag) {
+    if (typeof window == "undefined") return null;
+    if (typeof window[tag] == "undefined") return null;
+    var constructor = window[tag];
+    if (typeof constructor != "function") return null;
+    return constructor.prototype;
+  }
+  function discriminator(tag) { return null; }
+  var isBrowser = typeof HTMLElement == "function";
+  return {
+    getTag: getTag,
+    getUnknownTag: isBrowser ? getUnknownTagGenericBrowser : getUnknownTag,
+    prototypeForTag: prototypeForTag,
+    discriminator: discriminator };
+};
+    B.C_JS_CONST6 = function(getTagFallback) {
+  return function(hooks) {
+    if (typeof navigator != "object") return hooks;
+    var userAgent = navigator.userAgent;
+    if (typeof userAgent != "string") return hooks;
+    if (userAgent.indexOf("DumpRenderTree") >= 0) return hooks;
+    if (userAgent.indexOf("Chrome") >= 0) {
+      function confirm(p) {
+        return typeof window == "object" && window[p] && window[p].name == p;
+      }
+      if (confirm("Window") && confirm("HTMLElement")) return hooks;
+    }
+    hooks.getTag = getTagFallback;
+  };
+};
+    B.C_JS_CONST1 = function(hooks) {
+  if (typeof dartExperimentalFixupGetTag != "function") return hooks;
+  hooks.getTag = dartExperimentalFixupGetTag(hooks.getTag);
+};
+    B.C_JS_CONST5 = function(hooks) {
+  if (typeof navigator != "object") return hooks;
+  var userAgent = navigator.userAgent;
+  if (typeof userAgent != "string") return hooks;
+  if (userAgent.indexOf("Firefox") == -1) return hooks;
+  var getTag = hooks.getTag;
+  var quickMap = {
+    "BeforeUnloadEvent": "Event",
+    "DataTransfer": "Clipboard",
+    "GeoGeolocation": "Geolocation",
+    "Location": "!Location",
+    "WorkerMessageEvent": "MessageEvent",
+    "XMLDocument": "!Document"};
+  function getTagFirefox(o) {
+    var tag = getTag(o);
+    return quickMap[tag] || tag;
+  }
+  hooks.getTag = getTagFirefox;
+};
+    B.C_JS_CONST4 = function(hooks) {
+  if (typeof navigator != "object") return hooks;
+  var userAgent = navigator.userAgent;
+  if (typeof userAgent != "string") return hooks;
+  if (userAgent.indexOf("Trident/") == -1) return hooks;
+  var getTag = hooks.getTag;
+  var quickMap = {
+    "BeforeUnloadEvent": "Event",
+    "DataTransfer": "Clipboard",
+    "HTMLDDElement": "HTMLElement",
+    "HTMLDTElement": "HTMLElement",
+    "HTMLPhraseElement": "HTMLElement",
+    "Position": "Geoposition"
+  };
+  function getTagIE(o) {
+    var tag = getTag(o);
+    var newTag = quickMap[tag];
+    if (newTag) return newTag;
+    if (tag == "Object") {
+      if (window.DataView && (o instanceof window.DataView)) return "DataView";
+    }
+    return tag;
+  }
+  function prototypeForTagIE(tag) {
+    var constructor = window[tag];
+    if (constructor == null) return null;
+    return constructor.prototype;
+  }
+  hooks.getTag = getTagIE;
+  hooks.prototypeForTag = prototypeForTagIE;
+};
+    B.C_JS_CONST2 = function(hooks) {
+  var getTag = hooks.getTag;
+  var prototypeForTag = hooks.prototypeForTag;
+  function getTagFixed(o) {
+    var tag = getTag(o);
+    if (tag == "Document") {
+      if (!!o.xmlVersion) return "!Document";
+      return "!HTMLDocument";
+    }
+    return tag;
+  }
+  function prototypeForTagFixed(tag) {
+    if (tag == "Document") return null;
+    return prototypeForTag(tag);
+  }
+  hooks.getTag = getTagFixed;
+  hooks.prototypeForTag = prototypeForTagFixed;
+};
+    B.C_JS_CONST3 = function(hooks) { return hooks; }
+;
+    B.C_JsonCodec = new A.JsonCodec();
+    B.C_OutOfMemoryError = new A.OutOfMemoryError();
+    B.C__DelayedDone = new A._DelayedDone();
+    B.C__JSRandom = new A._JSRandom();
+    B.C__Required = new A._Required();
+    B.C__RootZone = new A._RootZone();
+    B.Duration_0 = new A.Duration(0);
+    B.Duration_5000000 = new A.Duration(5000000);
+    B.JsonDecoder_null = new A.JsonDecoder(null);
+    B.JsonEncoder_null = new A.JsonEncoder(null);
+    B.Level_INFO_800 = new A.Level("INFO", 800);
+    B.Level_SEVERE_1000 = new A.Level("SEVERE", 1000);
+    B.Level_WARNING_900 = new A.Level("WARNING", 900);
+    B.List_empty = A._setArrayType(makeConstList([]), type$.JSArray_dynamic);
+    B.Object_empty = {};
+    B.Map_empty = new A.ConstantStringMap(B.Object_empty, [], A.findType("ConstantStringMap<Symbol0,@>"));
+    B.Symbol_call = new A.Symbol("call");
+    B.Type_ByteBuffer_RkP = A.typeLiteral("ByteBuffer");
+    B.Type_ByteData_zNC = A.typeLiteral("ByteData");
+    B.Type_Float32List_LB7 = A.typeLiteral("Float32List");
+    B.Type_Float64List_LB7 = A.typeLiteral("Float64List");
+    B.Type_Int16List_uXf = A.typeLiteral("Int16List");
+    B.Type_Int32List_O50 = A.typeLiteral("Int32List");
+    B.Type_Int8List_ekJ = A.typeLiteral("Int8List");
+    B.Type_Uint16List_2bx = A.typeLiteral("Uint16List");
+    B.Type_Uint32List_2bx = A.typeLiteral("Uint32List");
+    B.Type_Uint8ClampedList_Jik = A.typeLiteral("Uint8ClampedList");
+    B.Type_Uint8List_WLA = A.typeLiteral("Uint8List");
+    B._StringStackTrace_3uE = new A._StringStackTrace("");
+  })();
+  (function staticFields() {
+    $._JS_INTEROP_INTERCEPTOR_TAG = null;
+    $.toStringVisiting = A._setArrayType([], A.findType("JSArray<Object>"));
+    $.Primitives__identityHashCodeProperty = null;
+    $.BoundClosure__receiverFieldNameCache = null;
+    $.BoundClosure__interceptorFieldNameCache = null;
+    $.getTagFunction = null;
+    $.alternateTagFunction = null;
+    $.prototypeForTagFunction = null;
+    $.dispatchRecordsForInstanceTags = null;
+    $.interceptorsForUncacheableTags = null;
+    $.initNativeDispatchFlag = null;
+    $._nextCallback = null;
+    $._lastCallback = null;
+    $._lastPriorityCallback = null;
+    $._isInCallbackLoop = false;
+    $.Zone__current = B.C__RootZone;
+    $.LogRecord__nextNumber = 0;
+    $.Logger__loggers = A.LinkedHashMap_LinkedHashMap$_empty(type$.String, type$.Logger);
+  })();
+  (function lazyInitializers() {
+    var _lazyFinal = hunkHelpers.lazyFinal;
+    _lazyFinal($, "DART_CLOSURE_PROPERTY_NAME", "$get$DART_CLOSURE_PROPERTY_NAME", () => A.getIsolateAffinityTag("_$dart_dartClosure"));
+    _lazyFinal($, "nullFuture", "$get$nullFuture", () => B.C__RootZone.run$1$1(new A.nullFuture_closure(), A.findType("Future<Null>")));
+    _lazyFinal($, "TypeErrorDecoder_noSuchMethodPattern", "$get$TypeErrorDecoder_noSuchMethodPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn({
+      toString: function() {
+        return "$receiver$";
+      }
+    })));
+    _lazyFinal($, "TypeErrorDecoder_notClosurePattern", "$get$TypeErrorDecoder_notClosurePattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn({$method$: null,
+      toString: function() {
+        return "$receiver$";
+      }
+    })));
+    _lazyFinal($, "TypeErrorDecoder_nullCallPattern", "$get$TypeErrorDecoder_nullCallPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn(null)));
+    _lazyFinal($, "TypeErrorDecoder_nullLiteralCallPattern", "$get$TypeErrorDecoder_nullLiteralCallPattern", () => A.TypeErrorDecoder_extractPattern(function() {
+      var $argumentsExpr$ = "$arguments$";
+      try {
+        null.$method$($argumentsExpr$);
+      } catch (e) {
+        return e.message;
+      }
+    }()));
+    _lazyFinal($, "TypeErrorDecoder_undefinedCallPattern", "$get$TypeErrorDecoder_undefinedCallPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn(void 0)));
+    _lazyFinal($, "TypeErrorDecoder_undefinedLiteralCallPattern", "$get$TypeErrorDecoder_undefinedLiteralCallPattern", () => A.TypeErrorDecoder_extractPattern(function() {
+      var $argumentsExpr$ = "$arguments$";
+      try {
+        (void 0).$method$($argumentsExpr$);
+      } catch (e) {
+        return e.message;
+      }
+    }()));
+    _lazyFinal($, "TypeErrorDecoder_nullPropertyPattern", "$get$TypeErrorDecoder_nullPropertyPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokePropertyErrorOn(null)));
+    _lazyFinal($, "TypeErrorDecoder_nullLiteralPropertyPattern", "$get$TypeErrorDecoder_nullLiteralPropertyPattern", () => A.TypeErrorDecoder_extractPattern(function() {
+      try {
+        null.$method$;
+      } catch (e) {
+        return e.message;
+      }
+    }()));
+    _lazyFinal($, "TypeErrorDecoder_undefinedPropertyPattern", "$get$TypeErrorDecoder_undefinedPropertyPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokePropertyErrorOn(void 0)));
+    _lazyFinal($, "TypeErrorDecoder_undefinedLiteralPropertyPattern", "$get$TypeErrorDecoder_undefinedLiteralPropertyPattern", () => A.TypeErrorDecoder_extractPattern(function() {
+      try {
+        (void 0).$method$;
+      } catch (e) {
+        return e.message;
+      }
+    }()));
+    _lazyFinal($, "_AsyncRun__scheduleImmediateClosure", "$get$_AsyncRun__scheduleImmediateClosure", () => A._AsyncRun__initializeScheduleImmediate());
+    _lazyFinal($, "Future__nullFuture", "$get$Future__nullFuture", () => A.findType("_Future<Null>")._as($.$get$nullFuture()));
+    _lazyFinal($, "Logger_root", "$get$Logger_root", () => A.Logger_Logger(""));
+    _lazyFinal($, "_requestPool", "$get$_requestPool", () => {
+      var t4,
+        t1 = A.findType("Completer<PoolResource>"),
+        t2 = A.ListQueue$(t1),
+        t3 = A.ListQueue$(type$.void_Function);
+      t1 = A.ListQueue$(t1);
+      t4 = A.Completer_Completer(type$.dynamic);
+      return new A.Pool(t2, t3, t1, 1000, new A.AsyncMemoizer(t4, A.findType("AsyncMemoizer<@>")));
+    });
+  })();
+  (function nativeSupport() {
+    !function() {
+      var intern = function(s) {
+        var o = {};
+        o[s] = 1;
+        return Object.keys(hunkHelpers.convertToFastObject(o))[0];
+      };
+      init.getIsolateTag = function(name) {
+        return intern("___dart_" + name + init.isolateTag);
+      };
+      var tableProperty = "___dart_isolate_tags_";
+      var usedProperties = Object[tableProperty] || (Object[tableProperty] = Object.create(null));
+      var rootProperty = "_ZxYxX";
+      for (var i = 0;; i++) {
+        var property = intern(rootProperty + "_" + i + "_");
+        if (!(property in usedProperties)) {
+          usedProperties[property] = 1;
+          init.isolateTag = property;
+          break;
+        }
+      }
+      init.dispatchPropertyName = init.getIsolateTag("dispatch_record");
+    }();
+    hunkHelpers.setOrUpdateInterceptorsByTag({ArrayBuffer: A.NativeByteBuffer, ArrayBufferView: A.NativeTypedData, DataView: A.NativeByteData, Float32Array: A.NativeFloat32List, Float64Array: A.NativeFloat64List, Int16Array: A.NativeInt16List, Int32Array: A.NativeInt32List, Int8Array: A.NativeInt8List, Uint16Array: A.NativeUint16List, Uint32Array: A.NativeUint32List, Uint8ClampedArray: A.NativeUint8ClampedList, CanvasPixelArray: A.NativeUint8ClampedList, Uint8Array: A.NativeUint8List});
+    hunkHelpers.setOrUpdateLeafTags({ArrayBuffer: true, ArrayBufferView: false, DataView: true, Float32Array: true, Float64Array: true, Int16Array: true, Int32Array: true, Int8Array: true, Uint16Array: true, Uint32Array: true, Uint8ClampedArray: true, CanvasPixelArray: true, Uint8Array: false});
+    A.NativeTypedArray.$nativeSuperclassTag = "ArrayBufferView";
+    A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin.$nativeSuperclassTag = "ArrayBufferView";
+    A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin.$nativeSuperclassTag = "ArrayBufferView";
+    A.NativeTypedArrayOfDouble.$nativeSuperclassTag = "ArrayBufferView";
+    A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin.$nativeSuperclassTag = "ArrayBufferView";
+    A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin.$nativeSuperclassTag = "ArrayBufferView";
+    A.NativeTypedArrayOfInt.$nativeSuperclassTag = "ArrayBufferView";
+  })();
+  Function.prototype.call$1 = function(a) {
+    return this(a);
+  };
+  Function.prototype.call$0 = function() {
+    return this();
+  };
+  Function.prototype.call$2 = function(a, b) {
+    return this(a, b);
+  };
+  Function.prototype.call$3 = function(a, b, c) {
+    return this(a, b, c);
+  };
+  Function.prototype.call$4 = function(a, b, c, d) {
+    return this(a, b, c, d);
+  };
+  Function.prototype.call$1$1 = function(a) {
+    return this(a);
+  };
+  convertAllToFastObject(holders);
+  convertToFastObject($);
+  (function(callback) {
+    if (typeof document === "undefined") {
+      callback(null);
+      return;
+    }
+    if (typeof document.currentScript != "undefined") {
+      callback(document.currentScript);
+      return;
+    }
+    var scripts = document.scripts;
+    function onLoad(event) {
+      for (var i = 0; i < scripts.length; ++i) {
+        scripts[i].removeEventListener("load", onLoad, false);
+      }
+      callback(event.target);
+    }
+    for (var i = 0; i < scripts.length; ++i) {
+      scripts[i].addEventListener("load", onLoad, false);
+    }
+  })(function(currentScript) {
+    init.currentScript = currentScript;
+    var callMain = A.main;
+    if (typeof dartMainRunner === "function") {
+      dartMainRunner(callMain, []);
+    } else {
+      callMain([]);
+    }
+  });
+})();
diff --git a/pkgs/sse/test/web/index.html b/pkgs/sse/test/web/index.html
new file mode 100644
index 0000000..be26763
--- /dev/null
+++ b/pkgs/sse/test/web/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+    <title>SSE Broadcast Channel Test</title>
+</head>
+
+<body>
+    <button type="button">Close Sink</button>
+    <script type="application/javascript" src="index.dart.js"></script>
+</body>
+
+</html>
diff --git a/pkgs/sse/tool/build_js.sh b/pkgs/sse/tool/build_js.sh
new file mode 100755
index 0000000..ef29b70
--- /dev/null
+++ b/pkgs/sse/tool/build_js.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+dart compile js --no-source-maps test/web/index.dart -o test/web/index.dart.js
diff --git a/pkgs/stack_trace/.gitignore b/pkgs/stack_trace/.gitignore
new file mode 100644
index 0000000..f023015
--- /dev/null
+++ b/pkgs/stack_trace/.gitignore
@@ -0,0 +1,6 @@
+# See https://dart.dev/guides/libraries/private-files
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+.pub/
+pubspec.lock
diff --git a/pkgs/stack_trace/CHANGELOG.md b/pkgs/stack_trace/CHANGELOG.md
new file mode 100644
index 0000000..e92cf9c
--- /dev/null
+++ b/pkgs/stack_trace/CHANGELOG.md
@@ -0,0 +1,363 @@
+## 1.12.1
+
+* Move to `dart-lang/tools` monorepo.
+
+## 1.12.0
+
+* Added support for parsing Wasm frames of Chrome (V8), Firefox, Safari.
+* Require Dart 3.4 or greater
+
+## 1.11.1
+
+* Make use of `@pragma('vm:awaiter-link')` to make package work better with
+  Dart VM's builtin awaiter stack unwinding. No other changes.
+
+## 1.11.0
+
+* Added the parameter `zoneValues` to `Chain.capture` to be able to use custom
+  zone values with the `runZoned` internal calls.
+* Populate the pubspec `repository` field.
+* Require Dart 2.18 or greater
+
+## 1.10.0
+
+* Stable release for null safety.
+* Fix broken test, `test/chain/vm_test.dart`, which incorrectly handles
+  asynchronous suspension gap markers at the end of stack traces.
+
+## 1.10.0-nullsafety.6
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces, when parsing with `Trace.parse` and `Chain.parse`.
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+## 1.10.0-nullsafety.5
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.10.0-nullsafety.4
+
+* Allow the `2.10.0` stable and dev SDKs.
+
+## 1.10.0-nullsafety.3
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces.
+
+## 1.10.0-nullsafety.2
+
+* Forward fix for a change in SDK type promotion behavior.
+
+## 1.10.0-nullsafety.1
+
+* Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 1.10.0-nullsafety
+
+* Opt in to null safety.
+
+## 1.9.6 (backpublish)
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces. (Also fixed separately in 1.10.0-nullsafety.3)
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces, when parsing with `Trace.parse` and `Chain.parse`. (Also fixed
+  separately in 1.10.0-nullsafety.6)
+
+## 1.9.5
+
+* Parse the format for `data:` URIs that the Dart VM has used since `2.2.0`.
+
+## 1.9.4
+
+* Add support for firefox anonymous stack traces.
+* Add support for chrome eval stack traces without a column.
+* Change the argument type to `Chain.capture` from `Function(dynamic, Chain)` to
+  `Function(Object, Chain)`. Existing functions which take `dynamic` are still
+  fine, but new uses can have a safer type.
+
+## 1.9.3
+
+* Set max SDK version to `<3.0.0`.
+
+## 1.9.2
+
+* Fix Dart 2.0 runtime cast failure in test.
+
+## 1.9.1
+
+* Preserve the original chain for a trace to handle cases where an
+  error is rethrown.
+
+## 1.9.0
+
+* Add an `errorZone` parameter to `Chain.capture()` that makes it avoid creating
+  an error zone.
+
+## 1.8.3
+
+* `Chain.forTrace()` now returns a full stack chain for *all* `StackTrace`s
+  within `Chain.capture()`, even those that haven't been processed by
+  `dart:async` yet.
+
+* `Chain.forTrace()` now uses the Dart VM's stack chain information when called
+  synchronously within `Chain.capture()`. This matches the existing behavior
+  outside `Chain.capture()`.
+
+* `Chain.forTrace()` now trims the VM's stack chains for the innermost stack
+  trace within `Chain.capture()` (unless it's called synchronously, as above).
+  This avoids duplicated frames and makes the format of the innermost traces
+  consistent with the other traces in the chain.
+
+## 1.8.2
+
+* Update to use strong-mode clean Zone API.
+
+## 1.8.1
+
+* Use official generic function syntax.
+
+* Updated minimum SDK to 1.23.0.
+
+## 1.8.0
+
+* Add a `Trace.original` field to provide access to the original `StackTrace`s
+  from which the `Trace` was created, and a matching constructor parameter to
+  `new Trace()`.
+
+## 1.7.4
+
+* Always run `onError` callbacks for `Chain.capture()` in the parent zone.
+
+## 1.7.3
+
+* Fix broken links in the README.
+
+## 1.7.2
+
+* `Trace.foldFrames()` and `Chain.foldFrames()` now remove the outermost folded
+  frame. This matches the behavior of `.terse` with core frames.
+
+* Fix bug parsing a friendly frame with spaces in the member name.
+
+* Fix bug parsing a friendly frame where the location is a data url.
+
+## 1.7.1
+
+* Make `Trace.parse()`, `Chain.parse()`, treat the VM's new causal asynchronous
+  stack traces as chains. Outside of a `Chain.capture()` block, `new
+  Chain.current()` will return a stack chain constructed from the asynchronous
+  stack traces.
+
+## 1.7.0
+
+* Add a `Chain.disable()` function that disables stack-chain tracking.
+
+* Fix a bug where `Chain.capture(..., when: false)` would throw if an error was
+  emitted without a stack trace.
+
+## 1.6.8
+
+* Add a note to the documentation of `Chain.terse` and `Trace.terse`.
+
+## 1.6.7
+
+* Fix a bug where `new Frame.caller()` returned the wrong depth of frame on
+  Dartium.
+
+## 1.6.6
+
+* `new Trace.current()` and `new Chain.current()` now skip an extra frame when
+  run in a JS context. This makes their return values match the VM context.
+
+## 1.6.5
+
+* Really fix strong mode warnings.
+
+## 1.6.4
+
+* Fix a syntax error introduced in 1.6.3.
+
+## 1.6.3
+
+* Make `Chain.capture()` generic. Its signature is now `T Chain.capture<T>(T
+  callback(), ...)`.
+
+## 1.6.2
+
+* Fix all strong mode warnings.
+
+## 1.6.1
+
+* Use `StackTrace.current` in Dart SDK 1.14 to get the current stack trace.
+
+## 1.6.0
+
+* Add a `when` parameter to `Chain.capture()`. This allows capturing to be
+  easily enabled and disabled based on whether the application is running in
+  debug/development mode or not.
+
+* Deprecate the `ChainHandler` typedef. This didn't provide any value over
+  directly annotating the function argument, and it made the documentation less
+  clear.
+
+## 1.5.1
+
+* Fix a crash in `Chain.foldFrames()` and `Chain.terse` when one of the chain's
+  traces has no frames.
+
+## 1.5.0
+
+* `new Chain.parse()` now parses all the stack trace formats supported by `new
+  Trace.parse()`. Formats other than that emitted by `Chain.toString()` will
+  produce single-element chains.
+
+* `new Trace.parse()` now parses the output of `Chain.toString()`. It produces
+  the same result as `Chain.parse().toTrace()`.
+
+## 1.4.2
+
+* Improve the display of `data:` URIs in stack traces.
+
+## 1.4.1
+
+* Fix a crashing bug in `UnparsedFrame.toString()`.
+
+## 1.4.0
+
+* `new Trace.parse()` and related constructors will no longer throw an exception
+  if they encounter an unparseable stack frame. Instead, they will generate an
+  `UnparsedFrame`, which exposes no metadata but preserves the frame's original
+  text.
+
+* Properly parse native-code V8 frames.
+
+## 1.3.5
+
+* Properly shorten library names for pathnames of folded frames on Windows.
+
+## 1.3.4
+
+* No longer say that stack chains aren't supported on dart2js now that
+  [sdk#15171][] is fixed. Note that this fix only applies to Dart 1.12.
+
+[sdk#15171]: https://github.com/dart-lang/sdk/issues/15171
+
+## 1.3.3
+
+* When a `null` stack trace is passed to a completer or stream controller in
+  nested `Chain.capture()` blocks, substitute the inner block's chain rather
+  than the outer block's.
+
+* Add support for empty chains and chains of empty traces to `Chain.parse()`.
+
+* Don't crash when parsing stack traces from Dart VM stack overflows.
+
+## 1.3.2
+
+* Don't crash when running `Trace.terse` on empty stack traces.
+
+## 1.3.1
+
+* Support more types of JavaScriptCore stack frames.
+
+## 1.3.0
+
+* Support stack traces generated by JavaScriptCore. They can be explicitly
+  parsed via `new Trace.parseJSCore` and `new Frame.parseJSCore`.
+
+## 1.2.4
+
+* Fix a type annotation in `LazyTrace`.
+
+## 1.2.3
+
+* Fix a crash in `Chain.parse`.
+
+## 1.2.2
+
+* Don't print the first folded frame of terse stack traces. This frame
+  is always just an internal isolate message handler anyway. This
+  improves the readability of stack traces, especially in stack chains.
+
+* Remove the line numbers and specific files in all terse folded frames, not
+  just those from core libraries.
+
+* Make padding consistent across all stack traces for `Chain.toString()`.
+
+## 1.2.1
+
+* Add `terse` to `LazyTrace.foldFrames()`.
+
+* Further improve stack chains when using the VM's async/await implementation.
+
+## 1.2.0
+
+* Add a `terse` argument to `Trace.foldFrames()` and `Chain.foldFrames()`. This
+  allows them to inherit the behavior of `Trace.terse` and `Chain.terse` without
+  having to duplicate the logic.
+
+## 1.1.3
+
+* Produce nicer-looking stack chains when using the VM's async/await
+  implementation.
+
+## 1.1.2
+
+* Support VM frames without line *or* column numbers, which async/await programs
+  occasionally generate.
+
+* Replace `<<anonymous closure>_async_body>` in VM frames' members with the
+  terser `<async>`.
+
+## 1.1.1
+
+* Widen the SDK constraint to include 1.7.0-dev.4.0.
+
+## 1.1.0
+
+* Unify the parsing of Safari and Firefox stack traces. This fixes an error in
+  Firefox trace parsing.
+
+* Deprecate `Trace.parseSafari6_0`, `Trace.parseSafari6_1`,
+  `Frame.parseSafari6_0`, and `Frame.parseSafari6_1`.
+
+* Add `Frame.parseSafari`.
+
+## 1.0.3
+
+* Use `Zone.errorCallback` to attach stack chains to all errors without the need
+  for `Chain.track`, which is now deprecated.
+
+## 1.0.2
+
+* Remove a workaround for [issue 17083][].
+
+[issue 17083]: https://github.com/dart-lang/sdk/issues/17083
+
+## 1.0.1
+
+* Synchronous errors in the [Chain.capture] callback are now handled correctly.
+
+## 1.0.0
+
+* No API changes, just declared stable.
+
+## 0.9.3+2
+
+* Update the dependency on path.
+
+* Improve the formatting of library URIs in stack traces.
+
+## 0.9.3+1
+
+* If an error is thrown in `Chain.capture`'s `onError` handler, that error is
+  handled by the parent zone. This matches the behavior of `runZoned` in
+  `dart:async`.
+
+## 0.9.3
+
+* Add a `Chain.foldFrames` method that parallels `Trace.foldFrames`.
+
+* Record anonymous method frames in IE10 as "<fn>".
diff --git a/pkgs/stack_trace/LICENSE b/pkgs/stack_trace/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/pkgs/stack_trace/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/stack_trace/README.md b/pkgs/stack_trace/README.md
new file mode 100644
index 0000000..b10a556
--- /dev/null
+++ b/pkgs/stack_trace/README.md
@@ -0,0 +1,169 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/stack_trace.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/stack_trace.yaml)
+[![pub package](https://img.shields.io/pub/v/stack_trace.svg)](https://pub.dev/packages/stack_trace)
+[![package publisher](https://img.shields.io/pub/publisher/stack_trace.svg)](https://pub.dev/packages/stack_trace/publisher)
+
+This library provides the ability to parse, inspect, and manipulate stack traces
+produced by the underlying Dart implementation. It also provides functions to
+produce string representations of stack traces in a more readable format than
+the native [StackTrace] implementation.
+
+`Trace`s can be parsed from native [StackTrace]s using `Trace.from`, or captured
+using `Trace.current`. Native [StackTrace]s can also be directly converted to
+human-readable strings using `Trace.format`.
+
+[StackTrace]: https://api.dart.dev/stable/dart-core/StackTrace-class.html
+
+Here's an example native stack trace from debugging this library:
+
+    #0      Object.noSuchMethod (dart:core-patch:1884:25)
+    #1      Trace.terse.<anonymous closure> (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/src/trace.dart:47:21)
+    #2      IterableMixinWorkaround.reduce (dart:collection:29:29)
+    #3      List.reduce (dart:core-patch:1247:42)
+    #4      Trace.terse (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/src/trace.dart:40:35)
+    #5      format (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/stack_trace.dart:24:28)
+    #6      main.<anonymous closure> (file:///usr/local/google-old/home/goog/dart/dart/test.dart:21:29)
+    #7      _CatchErrorFuture._sendError (dart:async:525:24)
+    #8      _FutureImpl._setErrorWithoutAsyncTrace (dart:async:393:26)
+    #9      _FutureImpl._setError (dart:async:378:31)
+    #10     _ThenFuture._sendValue (dart:async:490:16)
+    #11     _FutureImpl._handleValue.<anonymous closure> (dart:async:349:28)
+    #12     Timer.run.<anonymous closure> (dart:async:2402:21)
+    #13     Timer.Timer.<anonymous closure> (dart:async-patch:15:15)
+
+and its human-readable representation:
+
+    dart:core-patch 1884:25                     Object.noSuchMethod
+    pkg/stack_trace/lib/src/trace.dart 47:21    Trace.terse.<fn>
+    dart:collection 29:29                       IterableMixinWorkaround.reduce
+    dart:core-patch 1247:42                     List.reduce
+    pkg/stack_trace/lib/src/trace.dart 40:35    Trace.terse
+    pkg/stack_trace/lib/stack_trace.dart 24:28  format
+    test.dart 21:29                             main.<fn>
+    dart:async 525:24                           _CatchErrorFuture._sendError
+    dart:async 393:26                           _FutureImpl._setErrorWithoutAsyncTrace
+    dart:async 378:31                           _FutureImpl._setError
+    dart:async 490:16                           _ThenFuture._sendValue
+    dart:async 349:28                           _FutureImpl._handleValue.<fn>
+    dart:async 2402:21                          Timer.run.<fn>
+    dart:async-patch 15:15                      Timer.Timer.<fn>
+
+You can further clean up the stack trace using `Trace.terse`. This folds
+together multiple stack frames from the Dart core libraries, so that only the
+core library method that was directly called from user code is visible. For
+example:
+
+    dart:core                                   Object.noSuchMethod
+    pkg/stack_trace/lib/src/trace.dart 47:21    Trace.terse.<fn>
+    dart:core                                   List.reduce
+    pkg/stack_trace/lib/src/trace.dart 40:35    Trace.terse
+    pkg/stack_trace/lib/stack_trace.dart 24:28  format
+    test.dart 21:29                             main.<fn>
+
+## Stack Chains
+
+This library also provides the ability to capture "stack chains" with the
+`Chain` class. When writing asynchronous code, a single stack trace isn't very
+useful, since the call stack is unwound every time something async happens. A
+stack chain tracks stack traces through asynchronous calls, so that you can see
+the full path from `main` down to the error.
+
+To use stack chains, just wrap the code that you want to track in
+`Chain.capture`. This will create a new [Zone][] in which stack traces are
+recorded and woven into chains every time an asynchronous call occurs. Zones are
+sticky, too, so any asynchronous operations started in the `Chain.capture`
+callback will have their chains tracked, as will asynchronous operations they
+start and so on.
+
+Here's an example of some code that doesn't capture its stack chains:
+
+```dart
+import 'dart:async';
+
+void main() {
+  _scheduleAsync();
+}
+
+void _scheduleAsync() {
+  Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+  throw 'oh no!';
+}
+```
+
+If we run this, it prints the following:
+
+    Unhandled exception:
+    oh no!
+    #0      _runAsync (file:///Users/kevmoo/github/stack_trace/example/example.dart:12:3)
+    #1      _scheduleAsync.<anonymous closure> (file:///Users/kevmoo/github/stack_trace/example/example.dart:8:52)
+    <asynchronous suspension>
+
+Notice how there's no mention of `main` in that stack trace. All we know is that
+the error was in `runAsync`; we don't know why `runAsync` was called.
+
+Now let's look at the same code with stack chains captured:
+
+```dart
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+void main() {
+  Chain.capture(_scheduleAsync);
+}
+
+void _scheduleAsync() {
+  Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+  throw 'oh no!';
+}
+```
+
+Now if we run it, it prints this:
+
+    Unhandled exception:
+    oh no!
+    example/example.dart 14:3                                     _runAsync
+    example/example.dart 10:52                                    _scheduleAsync.<fn>
+    package:stack_trace/src/stack_zone_specification.dart 126:26  StackZoneSpecification._registerUnaryCallback.<fn>.<fn>
+    package:stack_trace/src/stack_zone_specification.dart 208:15  StackZoneSpecification._run
+    package:stack_trace/src/stack_zone_specification.dart 126:14  StackZoneSpecification._registerUnaryCallback.<fn>
+    dart:async/zone.dart 1406:47                                  _rootRunUnary
+    dart:async/zone.dart 1307:19                                  _CustomZone.runUnary
+    ===== asynchronous gap ===========================
+    dart:async/zone.dart 1328:19                                  _CustomZone.registerUnaryCallback
+    dart:async/future_impl.dart 315:23                            Future.then
+    example/example.dart 10:40                                    _scheduleAsync
+    package:stack_trace/src/chain.dart 97:24                      Chain.capture.<fn>
+    dart:async/zone.dart 1398:13                                  _rootRun
+    dart:async/zone.dart 1300:19                                  _CustomZone.run
+    dart:async/zone.dart 1803:10                                  _runZoned
+    dart:async/zone.dart 1746:10                                  runZoned
+    package:stack_trace/src/chain.dart 95:12                      Chain.capture
+    example/example.dart 6:9                                      main
+    dart:isolate-patch/isolate_patch.dart 297:19                  _delayEntrypointInvocation.<fn>
+    dart:isolate-patch/isolate_patch.dart 192:12                  _RawReceivePortImpl._handleMessage
+
+That's a lot of text! If you look closely, though, you can see that `main` is
+listed in the first trace in the chain.
+
+Thankfully, you can call `Chain.terse` just like `Trace.terse` to get rid of all
+the frames you don't care about. The terse version of the stack chain above is
+this:
+
+    test.dart 17:3       runAsync
+    test.dart 13:28      scheduleAsync.<fn>
+    ===== asynchronous gap ===========================
+    dart:async           _Future.then
+    test.dart 13:12      scheduleAsync
+    test.dart 7:18       main.<fn>
+    package:stack_trace  Chain.capture
+    test.dart 6:16       main
+
+That's a lot easier to understand!
+
+[Zone]: https://api.dart.dev/stable/dart-async/Zone-class.html
diff --git a/pkgs/stack_trace/analysis_options.yaml b/pkgs/stack_trace/analysis_options.yaml
new file mode 100644
index 0000000..4eb82ce
--- /dev/null
+++ b/pkgs/stack_trace/analysis_options.yaml
@@ -0,0 +1,22 @@
+# https://dart.dev/tools/analysis#the-analysis-options-file
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+    strict-raw-types: true
+
+linter:
+  rules:
+    - avoid_private_typedef_functions
+    - avoid_redundant_argument_values
+    - avoid_unused_constructor_parameters
+    - avoid_void_async
+    - cancel_subscriptions
+    - literal_only_boolean_expressions
+    - missing_whitespace_between_adjacent_strings
+    - no_adjacent_strings_in_list
+    - no_runtimeType_toString
+    - prefer_const_declarations
+    - unnecessary_await_in_return
+    - use_string_buffers
diff --git a/pkgs/stack_trace/example/example.dart b/pkgs/stack_trace/example/example.dart
new file mode 100644
index 0000000..d601ca4
--- /dev/null
+++ b/pkgs/stack_trace/example/example.dart
@@ -0,0 +1,15 @@
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+void main() {
+  Chain.capture(_scheduleAsync);
+}
+
+void _scheduleAsync() {
+  Future<void>.delayed(const Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+  throw StateError('oh no!');
+}
diff --git a/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart
new file mode 100644
index 0000000..6a815c6
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/chain.dart
@@ -0,0 +1,264 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:math' as math;
+
+import 'frame.dart';
+import 'lazy_chain.dart';
+import 'stack_zone_specification.dart';
+import 'trace.dart';
+import 'utils.dart';
+
+/// A function that handles errors in the zone wrapped by [Chain.capture].
+@Deprecated('Will be removed in stack_trace 2.0.0.')
+typedef ChainHandler = void Function(dynamic error, Chain chain);
+
+/// An opaque key used to track the current [StackZoneSpecification].
+final _specKey = Object();
+
+/// A chain of stack traces.
+///
+/// A stack chain is a collection of one or more stack traces that collectively
+/// represent the path from `main` through nested function calls to a particular
+/// code location, usually where an error was thrown. Multiple stack traces are
+/// necessary when using asynchronous functions, since the program's stack is
+/// reset before each asynchronous callback is run.
+///
+/// Stack chains can be automatically tracked using [Chain.capture]. This sets
+/// up a new [Zone] in which the current stack chain is tracked and can be
+/// accessed using [Chain.current]. Any errors that would be top-leveled in
+/// the zone can be handled, along with their associated chains, with the
+/// `onError` callback. For example:
+///
+///     Chain.capture(() {
+///       // ...
+///     }, onError: (error, stackChain) {
+///       print("Caught error $error\n"
+///             "$stackChain");
+///     });
+class Chain implements StackTrace {
+  /// The stack traces that make up this chain.
+  ///
+  /// Like the frames in a stack trace, the traces are ordered from most local
+  /// to least local. The first one is the trace where the actual exception was
+  /// raised, the second one is where that callback was scheduled, and so on.
+  final List<Trace> traces;
+
+  /// The [StackZoneSpecification] for the current zone.
+  static StackZoneSpecification? get _currentSpec =>
+      Zone.current[_specKey] as StackZoneSpecification?;
+
+  /// If [when] is `true`, runs [callback] in a [Zone] in which the current
+  /// stack chain is tracked and automatically associated with (most) errors.
+  ///
+  /// If [when] is `false`, this does not track stack chains. Instead, it's
+  /// identical to [runZoned], except that it wraps any errors in
+  /// [Chain.forTrace]—which will only wrap the trace unless there's a different
+  /// [Chain.capture] active. This makes it easy for the caller to only capture
+  /// stack chains in debug mode or during development.
+  ///
+  /// If [onError] is passed, any error in the zone that would otherwise go
+  /// unhandled is passed to it, along with the [Chain] associated with that
+  /// error. Note that if [callback] produces multiple unhandled errors,
+  /// [onError] may be called more than once. If [onError] isn't passed, the
+  /// parent Zone's `unhandledErrorHandler` will be called with the error and
+  /// its chain.
+  ///
+  /// The zone this creates will be an error zone if either [onError] is
+  /// not `null` and [when] is false,
+  /// or if both [when] and [errorZone] are `true`.
+  ///  If [errorZone] is `false`, [onError] must be `null`.
+  ///
+  /// If [callback] returns a value, it will be returned by [capture] as well.
+  ///
+  /// [zoneValues] is added to the [runZoned] calls.
+  static T capture<T>(T Function() callback,
+      {void Function(Object error, Chain)? onError,
+      bool when = true,
+      bool errorZone = true,
+      Map<Object?, Object?>? zoneValues}) {
+    if (!errorZone && onError != null) {
+      throw ArgumentError.value(
+          onError, 'onError', 'must be null if errorZone is false');
+    }
+
+    if (!when) {
+      if (onError == null) return runZoned(callback, zoneValues: zoneValues);
+      return runZonedGuarded(callback, (error, stackTrace) {
+        onError(error, Chain.forTrace(stackTrace));
+      }, zoneValues: zoneValues) as T;
+    }
+
+    var spec = StackZoneSpecification(onError, errorZone: errorZone);
+    return runZoned(() {
+      try {
+        return callback();
+      } on Object catch (error, stackTrace) {
+        // Forward synchronous errors through the async error path to match the
+        // behavior of `runZonedGuarded`.
+        Zone.current.handleUncaughtError(error, stackTrace);
+
+        // If the expected return type of capture() is not nullable, this will
+        // throw a cast exception. But the only other alternative is to throw
+        // some other exception. Casting null to T at least lets existing uses
+        // where T is a nullable type continue to work.
+        return null as T;
+      }
+    }, zoneSpecification: spec.toSpec(), zoneValues: {
+      ...?zoneValues,
+      _specKey: spec,
+      StackZoneSpecification.disableKey: false
+    });
+  }
+
+  /// If [when] is `true` and this is called within a [Chain.capture] zone, runs
+  /// [callback] in a [Zone] in which chain capturing is disabled.
+  ///
+  /// If [callback] returns a value, it will be returned by [disable] as well.
+  static T disable<T>(T Function() callback, {bool when = true}) {
+    var zoneValues =
+        when ? {_specKey: null, StackZoneSpecification.disableKey: true} : null;
+
+    return runZoned(callback, zoneValues: zoneValues);
+  }
+
+  /// Returns [futureOrStream] unmodified.
+  ///
+  /// Prior to Dart 1.7, this was necessary to ensure that stack traces for
+  /// exceptions reported with [Completer.completeError] and
+  /// [StreamController.addError] were tracked correctly.
+  @Deprecated('Chain.track is not necessary in Dart 1.7+.')
+  static dynamic track(Object? futureOrStream) => futureOrStream;
+
+  /// Returns the current stack chain.
+  ///
+  /// By default, the first frame of the first trace will be the line where
+  /// [Chain.current] is called. If [level] is passed, the first trace will
+  /// start that many frames up instead.
+  ///
+  /// If this is called outside of a [capture] zone, it just returns a
+  /// single-trace chain.
+  factory Chain.current([int level = 0]) {
+    if (_currentSpec != null) return _currentSpec!.currentChain(level + 1);
+
+    var chain = Chain.forTrace(StackTrace.current);
+    return LazyChain(() {
+      // JS includes a frame for the call to StackTrace.current, but the VM
+      // doesn't, so we skip an extra frame in a JS context.
+      var first = Trace(chain.traces.first.frames.skip(level + (inJS ? 2 : 1)),
+          original: chain.traces.first.original.toString());
+      return Chain([first, ...chain.traces.skip(1)]);
+    });
+  }
+
+  /// Returns the stack chain associated with [trace].
+  ///
+  /// The first stack trace in the returned chain will always be [trace]
+  /// (converted to a [Trace] if necessary). If there is no chain associated
+  /// with [trace] or if this is called outside of a [capture] zone, this just
+  /// returns a single-trace chain containing [trace].
+  ///
+  /// If [trace] is already a [Chain], it will be returned as-is.
+  factory Chain.forTrace(StackTrace trace) {
+    if (trace is Chain) return trace;
+    if (_currentSpec != null) return _currentSpec!.chainFor(trace);
+    if (trace is Trace) return Chain([trace]);
+    return LazyChain(() => Chain.parse(trace.toString()));
+  }
+
+  /// Parses a string representation of a stack chain.
+  ///
+  /// If [chain] is the output of a call to [Chain.toString], it will be parsed
+  /// as a full stack chain. Otherwise, it will be parsed as in [Trace.parse]
+  /// and returned as a single-trace chain.
+  factory Chain.parse(String chain) {
+    if (chain.isEmpty) return Chain([]);
+    if (chain.contains(vmChainGap)) {
+      return Chain(chain
+          .split(vmChainGap)
+          .where((line) => line.isNotEmpty)
+          .map(Trace.parseVM));
+    }
+    if (!chain.contains(chainGap)) return Chain([Trace.parse(chain)]);
+
+    return Chain(chain.split(chainGap).map(Trace.parseFriendly));
+  }
+
+  /// Returns a new [Chain] comprised of [traces].
+  Chain(Iterable<Trace> traces) : traces = List<Trace>.unmodifiable(traces);
+
+  /// Returns a terser version of this chain.
+  ///
+  /// This calls [Trace.terse] on every trace in [traces], and discards any
+  /// trace that contain only internal frames.
+  ///
+  /// This won't do anything with a raw JavaScript trace, since there's no way
+  /// to determine which frames come from which Dart libraries. However, the
+  /// [`source_map_stack_trace`](https://pub.dev/packages/source_map_stack_trace)
+  /// package can be used to convert JavaScript traces into Dart-style traces.
+  Chain get terse => foldFrames((_) => false, terse: true);
+
+  /// Returns a new [Chain] based on this chain where multiple stack frames
+  /// matching [predicate] are folded together.
+  ///
+  /// This means that whenever there are multiple frames in a row that match
+  /// [predicate], only the last one is kept. In addition, traces that are
+  /// composed entirely of frames matching [predicate] are omitted.
+  ///
+  /// This is useful for limiting the amount of library code that appears in a
+  /// stack trace by only showing user code and code that's called by user code.
+  ///
+  /// If [terse] is true, this will also fold together frames from the core
+  /// library or from this package, and simplify core library frames as in
+  /// [Trace.terse].
+  Chain foldFrames(bool Function(Frame) predicate, {bool terse = false}) {
+    var foldedTraces =
+        traces.map((trace) => trace.foldFrames(predicate, terse: terse));
+    var nonEmptyTraces = foldedTraces.where((trace) {
+      // Ignore traces that contain only folded frames.
+      if (trace.frames.length > 1) return true;
+      if (trace.frames.isEmpty) return false;
+
+      // In terse mode, the trace may have removed an outer folded frame,
+      // leaving a single non-folded frame. We can detect a folded frame because
+      // it has no line information.
+      if (!terse) return false;
+      return trace.frames.single.line != null;
+    });
+
+    // If all the traces contain only internal processing, preserve the last
+    // (top-most) one so that the chain isn't empty.
+    if (nonEmptyTraces.isEmpty && foldedTraces.isNotEmpty) {
+      return Chain([foldedTraces.last]);
+    }
+
+    return Chain(nonEmptyTraces);
+  }
+
+  /// Converts this chain to a [Trace].
+  ///
+  /// The trace version of a chain is just the concatenation of all the traces
+  /// in the chain.
+  Trace toTrace() => Trace(traces.expand((trace) => trace.frames));
+
+  @override
+  String toString() {
+    // Figure out the longest path so we know how much to pad.
+    var longest = traces
+        .map((trace) => trace.frames
+            .map((frame) => frame.location.length)
+            .fold(0, math.max))
+        .fold(0, math.max);
+
+    // Don't call out to [Trace.toString] here because that doesn't ensure that
+    // padding is consistent across all traces.
+    return traces
+        .map((trace) => trace.frames
+            .map((frame) =>
+                '${frame.location.padRight(longest)}  ${frame.member}\n')
+            .join())
+        .join(chainGap);
+  }
+}
diff --git a/pkgs/stack_trace/lib/src/frame.dart b/pkgs/stack_trace/lib/src/frame.dart
new file mode 100644
index 0000000..d4043b7
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/frame.dart
@@ -0,0 +1,458 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as path;
+
+import 'trace.dart';
+import 'unparsed_frame.dart';
+
+// #1      Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)
+// #1      Foo._bar (file:///home/nweiz/code/stuff.dart:42)
+// #1      Foo._bar (file:///home/nweiz/code/stuff.dart)
+final _vmFrame = RegExp(r'^#\d+\s+(\S.*) \((.+?)((?::\d+){0,2})\)$');
+
+//     at Object.stringify (native)
+//     at VW.call$0 (https://example.com/stuff.dart.js:560:28)
+//     at VW.call$0 (eval as fn
+//         (https://example.com/stuff.dart.js:560:28), efn:3:28)
+//     at https://example.com/stuff.dart.js:560:28
+final _v8JsFrame =
+    RegExp(r'^\s*at (?:(\S.*?)(?: \[as [^\]]+\])? \((.*)\)|(.*))$');
+
+// https://example.com/stuff.dart.js:560:28
+// https://example.com/stuff.dart.js:560
+//
+// Group 1: URI, required
+// Group 2: line number, required
+// Group 3: column number, optional
+final _v8JsUrlLocation = RegExp(r'^(.*?):(\d+)(?::(\d+))?$|native$');
+
+// With names:
+//
+//     at Error.f (wasm://wasm/0006d966:wasm-function[119]:0xbb13)
+//     at g (wasm://wasm/0006d966:wasm-function[796]:0x143b4)
+//
+// Without names:
+//
+//     at wasm://wasm/0005168a:wasm-function[119]:0xbb13
+//     at wasm://wasm/0005168a:wasm-function[796]:0x143b4
+//
+// Matches named groups:
+//
+// - "member": optional, `Error.f` in the first example, NA in the second.
+// - "uri":  `wasm://wasm/0006d966`.
+// - "index": `119`.
+// - "offset": (hex number) `bb13`.
+//
+// To avoid having multiple groups for the same part of the frame, this regex
+// matches unmatched parentheses after the member name.
+final _v8WasmFrame = RegExp(r'^\s*at (?:(?<member>.+) )?'
+    r'(?:\(?(?:(?<uri>\S+):wasm-function\[(?<index>\d+)\]'
+    r'\:0x(?<offset>[0-9a-fA-F]+))\)?)$');
+
+// eval as function (https://example.com/stuff.dart.js:560:28), efn:3:28
+// eval as function (https://example.com/stuff.dart.js:560:28)
+// eval as function (eval as otherFunction
+//     (https://example.com/stuff.dart.js:560:28))
+final _v8EvalLocation =
+    RegExp(r'^eval at (?:\S.*?) \((.*)\)(?:, .*?:\d+:\d+)?$');
+
+// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+final _firefoxEvalLocation =
+    RegExp(r'(\S+)@(\S+) line (\d+) >.* (Function|eval):\d+:\d+');
+
+// .VW.call$0@https://example.com/stuff.dart.js:560
+// .VW.call$0("arg")@https://example.com/stuff.dart.js:560
+// .VW.call$0/name<@https://example.com/stuff.dart.js:560
+// .VW.call$0@https://example.com/stuff.dart.js:560:36
+// https://example.com/stuff.dart.js:560
+final _firefoxSafariJSFrame = RegExp(r'^'
+    r'(?:' // Member description. Not present in some Safari frames.
+    r'([^@(/]*)' // The actual name of the member.
+    r'(?:\(.*\))?' // Arguments to the member, sometimes captured by Firefox.
+    r'((?:/[^/]*)*)' // Extra characters indicating a nested closure.
+    r'(?:\(.*\))?' // Arguments to the closure.
+    r'@'
+    r')?'
+    r'(.*?)' // The frame's URL.
+    r':'
+    r'(\d*)' // The line number. Empty in Safari if it's unknown.
+    r'(?::(\d*))?' // The column number. Not present in older browsers and
+    // empty in Safari if it's unknown.
+    r'$');
+
+// With names:
+//
+// g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4
+// f@http://localhost:8080/test.wasm:wasm-function[795]:0x143a8
+// main@http://localhost:8080/test.wasm:wasm-function[792]:0x14390
+//
+// Without names:
+//
+// @http://localhost:8080/test.wasm:wasm-function[796]:0x143b4
+// @http://localhost:8080/test.wasm:wasm-function[795]:0x143a8
+// @http://localhost:8080/test.wasm:wasm-function[792]:0x14390
+//
+// JSShell in the command line uses a different format, which this regex also
+// parses.
+//
+// With names:
+//
+// main@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378
+//
+// Without names:
+//
+// @/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378
+//
+// Matches named groups:
+//
+// - "member": Function name, may be empty: `g`.
+// - "uri": `http://localhost:8080/test.wasm`.
+// - "index": `796`.
+// - "offset": (in hex) `143b4`.
+final _firefoxWasmFrame =
+    RegExp(r'^(?<member>.*?)@(?:(?<uri>\S+).*?:wasm-function'
+        r'\[(?<index>\d+)\]:0x(?<offset>[0-9a-fA-F]+))$');
+
+// With names:
+//
+// (Note: Lines below are literal text, e.g. <?> is not a placeholder, it's a
+// part of the stack frame.)
+//
+// <?>.wasm-function[g]@[wasm code]
+// <?>.wasm-function[f]@[wasm code]
+// <?>.wasm-function[main]@[wasm code]
+//
+// Without names:
+//
+// <?>.wasm-function[796]@[wasm code]
+// <?>.wasm-function[795]@[wasm code]
+// <?>.wasm-function[792]@[wasm code]
+//
+// Matches named group "member": `g` or `796`.
+final _safariWasmFrame =
+    RegExp(r'^.*?wasm-function\[(?<member>.*)\]@\[wasm code\]$');
+
+// foo/bar.dart 10:11 Foo._bar
+// foo/bar.dart 10:11 (anonymous function).dart.fn
+// https://dart.dev/foo/bar.dart Foo._bar
+// data:... 10:11 Foo._bar
+final _friendlyFrame = RegExp(r'^(\S+)(?: (\d+)(?::(\d+))?)?\s+([^\d].*)$');
+
+/// A regular expression that matches asynchronous member names generated by the
+/// VM.
+final _asyncBody = RegExp(r'<(<anonymous closure>|[^>]+)_async_body>');
+
+final _initialDot = RegExp(r'^\.');
+
+/// A single stack frame. Each frame points to a precise location in Dart code.
+class Frame {
+  /// The URI of the file in which the code is located.
+  ///
+  /// This URI will usually have the scheme `dart`, `file`, `http`, or `https`.
+  final Uri uri;
+
+  /// The line number on which the code location is located.
+  ///
+  /// This can be null, indicating that the line number is unknown or
+  /// unimportant.
+  final int? line;
+
+  /// The column number of the code location.
+  ///
+  /// This can be null, indicating that the column number is unknown or
+  /// unimportant.
+  final int? column;
+
+  /// The name of the member in which the code location occurs.
+  ///
+  /// Anonymous closures are represented as `<fn>` in this member string.
+  final String? member;
+
+  /// Whether this stack frame comes from the Dart core libraries.
+  bool get isCore => uri.scheme == 'dart';
+
+  /// Returns a human-friendly description of the library that this stack frame
+  /// comes from.
+  ///
+  /// This will usually be the string form of [uri], but a relative URI will be
+  /// used if possible. Data URIs will be truncated.
+  String get library {
+    if (uri.scheme == 'data') return 'data:...';
+    return path.prettyUri(uri);
+  }
+
+  /// Returns the name of the package this stack frame comes from, or `null` if
+  /// this stack frame doesn't come from a `package:` URL.
+  String? get package {
+    if (uri.scheme != 'package') return null;
+    return uri.path.split('/').first;
+  }
+
+  /// A human-friendly description of the code location.
+  String get location {
+    if (line == null) return library;
+    if (column == null) return '$library $line';
+    return '$library $line:$column';
+  }
+
+  /// Returns a single frame of the current stack.
+  ///
+  /// By default, this will return the frame above the current method. If
+  /// [level] is `0`, it will return the current method's frame; if [level] is
+  /// higher than `1`, it will return higher frames.
+  factory Frame.caller([int level = 1]) {
+    if (level < 0) {
+      throw ArgumentError('Argument [level] must be greater than or equal '
+          'to 0.');
+    }
+
+    return Trace.current(level + 1).frames.first;
+  }
+
+  /// Parses a string representation of a Dart VM stack frame.
+  factory Frame.parseVM(String frame) => _catchFormatException(frame, () {
+        // The VM sometimes folds multiple stack frames together and replaces
+        // them with "...".
+        if (frame == '...') {
+          return Frame(Uri(), null, null, '...');
+        }
+
+        var match = _vmFrame.firstMatch(frame);
+        if (match == null) return UnparsedFrame(frame);
+
+        // Get the pieces out of the regexp match. Function, URI and line should
+        // always be found. The column is optional.
+        var member = match[1]!
+            .replaceAll(_asyncBody, '<async>')
+            .replaceAll('<anonymous closure>', '<fn>');
+        var uri = match[2]!.startsWith('<data:')
+            ? Uri.dataFromString('')
+            : Uri.parse(match[2]!);
+
+        var lineAndColumn = match[3]!.split(':');
+        var line =
+            lineAndColumn.length > 1 ? int.parse(lineAndColumn[1]) : null;
+        var column =
+            lineAndColumn.length > 2 ? int.parse(lineAndColumn[2]) : null;
+        return Frame(uri, line, column, member);
+      });
+
+  /// Parses a string representation of a Chrome/V8 stack frame.
+  factory Frame.parseV8(String frame) => _catchFormatException(frame, () {
+        // Try to match a Wasm frame first: the Wasm frame regex won't match a
+        // JS frame but the JS frame regex may match a Wasm frame.
+        var match = _v8WasmFrame.firstMatch(frame);
+        if (match != null) {
+          final member = match.namedGroup('member');
+          final uri = _uriOrPathToUri(match.namedGroup('uri')!);
+          final functionIndex = match.namedGroup('index')!;
+          final functionOffset =
+              int.parse(match.namedGroup('offset')!, radix: 16);
+          return Frame(uri, 1, functionOffset + 1, member ?? functionIndex);
+        }
+
+        match = _v8JsFrame.firstMatch(frame);
+        if (match != null) {
+          // v8 location strings can be arbitrarily-nested, since it adds a
+          // layer of nesting for each eval performed on that line.
+          Frame parseJsLocation(String location, String member) {
+            var evalMatch = _v8EvalLocation.firstMatch(location);
+            while (evalMatch != null) {
+              location = evalMatch[1]!;
+              evalMatch = _v8EvalLocation.firstMatch(location);
+            }
+
+            if (location == 'native') {
+              return Frame(Uri.parse('native'), null, null, member);
+            }
+
+            var urlMatch = _v8JsUrlLocation.firstMatch(location);
+            if (urlMatch == null) return UnparsedFrame(frame);
+
+            final uri = _uriOrPathToUri(urlMatch[1]!);
+            final line = int.parse(urlMatch[2]!);
+            final columnMatch = urlMatch[3];
+            final column = columnMatch != null ? int.parse(columnMatch) : null;
+            return Frame(uri, line, column, member);
+          }
+
+          // V8 stack frames can be in two forms.
+          if (match[2] != null) {
+            // The first form looks like " at FUNCTION (LOCATION)". V8 proper
+            // lists anonymous functions within eval as "<anonymous>", while
+            // IE10 lists them as "Anonymous function".
+            return parseJsLocation(
+                match[2]!,
+                match[1]!
+                    .replaceAll('<anonymous>', '<fn>')
+                    .replaceAll('Anonymous function', '<fn>')
+                    .replaceAll('(anonymous function)', '<fn>'));
+          } else {
+            // The second form looks like " at LOCATION", and is used for
+            // anonymous functions.
+            return parseJsLocation(match[3]!, '<fn>');
+          }
+        }
+
+        return UnparsedFrame(frame);
+      });
+
+  /// Parses a string representation of a JavaScriptCore stack trace.
+  factory Frame.parseJSCore(String frame) => Frame.parseV8(frame);
+
+  /// Parses a string representation of an IE stack frame.
+  ///
+  /// IE10+ frames look just like V8 frames. Prior to IE10, stack traces can't
+  /// be retrieved.
+  factory Frame.parseIE(String frame) => Frame.parseV8(frame);
+
+  /// Parses a Firefox 'eval' or 'function' stack frame.
+  ///
+  /// For example:
+  ///
+  /// ```
+  /// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+  /// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+  /// ```
+  factory Frame._parseFirefoxEval(String frame) =>
+      _catchFormatException(frame, () {
+        final match = _firefoxEvalLocation.firstMatch(frame);
+        if (match == null) return UnparsedFrame(frame);
+        var member = match[1]!.replaceAll('/<', '');
+        final uri = _uriOrPathToUri(match[2]!);
+        final line = int.parse(match[3]!);
+        if (member.isEmpty || member == 'anonymous') {
+          member = '<fn>';
+        }
+        return Frame(uri, line, null, member);
+      });
+
+  /// Parses a string representation of a Firefox or Safari stack frame.
+  factory Frame.parseFirefox(String frame) => _catchFormatException(frame, () {
+        var match = _firefoxSafariJSFrame.firstMatch(frame);
+        if (match != null) {
+          if (match[3]!.contains(' line ')) {
+            return Frame._parseFirefoxEval(frame);
+          }
+
+          // Normally this is a URI, but in a jsshell trace it can be a path.
+          var uri = _uriOrPathToUri(match[3]!);
+
+          var member = match[1];
+          if (member != null) {
+            member +=
+                List.filled('/'.allMatches(match[2]!).length, '.<fn>').join();
+            if (member == '') member = '<fn>';
+
+            // Some Firefox members have initial dots. We remove them for
+            // consistency with other platforms.
+            member = member.replaceFirst(_initialDot, '');
+          } else {
+            member = '<fn>';
+          }
+
+          var line = match[4] == '' ? null : int.parse(match[4]!);
+          var column =
+              match[5] == null || match[5] == '' ? null : int.parse(match[5]!);
+          return Frame(uri, line, column, member);
+        }
+
+        match = _firefoxWasmFrame.firstMatch(frame);
+        if (match != null) {
+          final member = match.namedGroup('member')!;
+          final uri = _uriOrPathToUri(match.namedGroup('uri')!);
+          final functionIndex = match.namedGroup('index')!;
+          final functionOffset =
+              int.parse(match.namedGroup('offset')!, radix: 16);
+          return Frame(uri, 1, functionOffset + 1,
+              member.isNotEmpty ? member : functionIndex);
+        }
+
+        match = _safariWasmFrame.firstMatch(frame);
+        if (match != null) {
+          final member = match.namedGroup('member')!;
+          return Frame(Uri(path: 'wasm code'), null, null, member);
+        }
+
+        return UnparsedFrame(frame);
+      });
+
+  /// Parses a string representation of a Safari 6.0 stack frame.
+  @Deprecated('Use Frame.parseSafari instead.')
+  factory Frame.parseSafari6_0(String frame) => Frame.parseFirefox(frame);
+
+  /// Parses a string representation of a Safari 6.1+ stack frame.
+  @Deprecated('Use Frame.parseSafari instead.')
+  factory Frame.parseSafari6_1(String frame) => Frame.parseFirefox(frame);
+
+  /// Parses a string representation of a Safari stack frame.
+  factory Frame.parseSafari(String frame) => Frame.parseFirefox(frame);
+
+  /// Parses this package's string representation of a stack frame.
+  factory Frame.parseFriendly(String frame) => _catchFormatException(frame, () {
+        var match = _friendlyFrame.firstMatch(frame);
+        if (match == null) {
+          throw FormatException(
+              "Couldn't parse package:stack_trace stack trace line '$frame'.");
+        }
+        // Fake truncated data urls generated by the friendly stack trace format
+        // cause Uri.parse to throw an exception so we have to special case
+        // them.
+        var uri = match[1] == 'data:...'
+            ? Uri.dataFromString('')
+            : Uri.parse(match[1]!);
+        // If there's no scheme, this is a relative URI. We should interpret it
+        // as relative to the current working directory.
+        if (uri.scheme == '') {
+          uri = path.toUri(path.absolute(path.fromUri(uri)));
+        }
+
+        var line = match[2] == null ? null : int.parse(match[2]!);
+        var column = match[3] == null ? null : int.parse(match[3]!);
+        return Frame(uri, line, column, match[4]);
+      });
+
+  /// A regular expression matching an absolute URI.
+  static final _uriRegExp = RegExp(r'^[a-zA-Z][-+.a-zA-Z\d]*://');
+
+  /// A regular expression matching a Windows path.
+  static final _windowsRegExp = RegExp(r'^([a-zA-Z]:[\\/]|\\\\)');
+
+  /// Converts [uriOrPath], which can be a URI, a Windows path, or a Posix path,
+  /// to a URI (absolute if possible).
+  static Uri _uriOrPathToUri(String uriOrPath) {
+    if (uriOrPath.contains(_uriRegExp)) {
+      return Uri.parse(uriOrPath);
+    } else if (uriOrPath.contains(_windowsRegExp)) {
+      return Uri.file(uriOrPath, windows: true);
+    } else if (uriOrPath.startsWith('/')) {
+      return Uri.file(uriOrPath, windows: false);
+    }
+
+    // As far as I've seen, Firefox and V8 both always report absolute paths in
+    // their stack frames. However, if we do get a relative path, we should
+    // handle it gracefully.
+    if (uriOrPath.contains('\\')) return path.windows.toUri(uriOrPath);
+    return Uri.parse(uriOrPath);
+  }
+
+  /// Runs [body] and returns its result.
+  ///
+  /// If [body] throws a [FormatException], returns an [UnparsedFrame] with
+  /// [text] instead.
+  static Frame _catchFormatException(String text, Frame Function() body) {
+    try {
+      return body();
+    } on FormatException catch (_) {
+      return UnparsedFrame(text);
+    }
+  }
+
+  Frame(this.uri, this.line, this.column, this.member);
+
+  @override
+  String toString() => '$location in $member';
+}
diff --git a/pkgs/stack_trace/lib/src/lazy_chain.dart b/pkgs/stack_trace/lib/src/lazy_chain.dart
new file mode 100644
index 0000000..063ed59
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/lazy_chain.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, 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 'chain.dart';
+import 'frame.dart';
+import 'lazy_trace.dart';
+import 'trace.dart';
+
+/// A thunk for lazily constructing a [Chain].
+typedef ChainThunk = Chain Function();
+
+/// A wrapper around a [ChainThunk]. This works around issue 9579 by avoiding
+/// the conversion of native [StackTrace]s to strings until it's absolutely
+/// necessary.
+class LazyChain implements Chain {
+  final ChainThunk _thunk;
+  late final Chain _chain = _thunk();
+
+  LazyChain(this._thunk);
+
+  @override
+  List<Trace> get traces => _chain.traces;
+  @override
+  Chain get terse => _chain.terse;
+  @override
+  Chain foldFrames(bool Function(Frame) predicate, {bool terse = false}) =>
+      LazyChain(() => _chain.foldFrames(predicate, terse: terse));
+  @override
+  Trace toTrace() => LazyTrace(_chain.toTrace);
+  @override
+  String toString() => _chain.toString();
+}
diff --git a/pkgs/stack_trace/lib/src/lazy_trace.dart b/pkgs/stack_trace/lib/src/lazy_trace.dart
new file mode 100644
index 0000000..3ecaa2d
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/lazy_trace.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'frame.dart';
+import 'trace.dart';
+
+/// A thunk for lazily constructing a [Trace].
+typedef TraceThunk = Trace Function();
+
+/// A wrapper around a [TraceThunk]. This works around issue 9579 by avoiding
+/// the conversion of native [StackTrace]s to strings until it's absolutely
+/// necessary.
+class LazyTrace implements Trace {
+  final TraceThunk _thunk;
+  late final Trace _trace = _thunk();
+
+  LazyTrace(this._thunk);
+
+  @override
+  List<Frame> get frames => _trace.frames;
+  @override
+  StackTrace get original => _trace.original;
+  @override
+  StackTrace get vmTrace => _trace.vmTrace;
+  @override
+  Trace get terse => LazyTrace(() => _trace.terse);
+  @override
+  Trace foldFrames(bool Function(Frame) predicate, {bool terse = false}) =>
+      LazyTrace(() => _trace.foldFrames(predicate, terse: terse));
+  @override
+  String toString() => _trace.toString();
+}
diff --git a/pkgs/stack_trace/lib/src/stack_zone_specification.dart b/pkgs/stack_trace/lib/src/stack_zone_specification.dart
new file mode 100644
index 0000000..901a5ee
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/stack_zone_specification.dart
@@ -0,0 +1,262 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'chain.dart';
+import 'lazy_chain.dart';
+import 'lazy_trace.dart';
+import 'trace.dart';
+import 'utils.dart';
+
+/// A class encapsulating the zone specification for a [Chain.capture] zone.
+///
+/// Until they're materialized and exposed to the user, stack chains are tracked
+/// as linked lists of [Trace]s using the [_Node] class. These nodes are stored
+/// in three distinct ways:
+///
+/// * When a callback is registered, a node is created and stored as a captured
+///   local variable until the callback is run.
+///
+/// * When a callback is run, its captured node is set as the [_currentNode] so
+///   it can be available to [Chain.current] and to be linked into additional
+///   chains when more callbacks are scheduled.
+///
+/// * When a callback throws an error or a Future or Stream emits an error, the
+///   current node is associated with that error's stack trace using the
+///   [_chains] expando.
+///
+/// Since [ZoneSpecification] can't be extended or even implemented, in order to
+/// get a real [ZoneSpecification] instance it's necessary to call [toSpec].
+class StackZoneSpecification {
+  /// An opaque object used as a zone value to disable chain tracking in a given
+  /// zone.
+  ///
+  /// If `Zone.current[disableKey]` is `true`, no stack chains will be tracked.
+  static final disableKey = Object();
+
+  /// Whether chain-tracking is disabled in the current zone.
+  bool get _disabled => Zone.current[disableKey] == true;
+
+  /// The expando that associates stack chains with [StackTrace]s.
+  ///
+  /// The chains are associated with stack traces rather than errors themselves
+  /// because it's a common practice to throw strings as errors, which can't be
+  /// used with expandos.
+  ///
+  /// The chain associated with a given stack trace doesn't contain a node for
+  /// that stack trace.
+  final _chains = Expando<_Node>('stack chains');
+
+  /// The error handler for the zone.
+  ///
+  /// If this is null, that indicates that any unhandled errors should be passed
+  /// to the parent zone.
+  final void Function(Object error, Chain)? _onError;
+
+  /// The most recent node of the current stack chain.
+  _Node? _currentNode;
+
+  /// Whether this is an error zone.
+  final bool _errorZone;
+
+  StackZoneSpecification(this._onError, {bool errorZone = true})
+      : _errorZone = errorZone;
+
+  /// Converts this specification to a real [ZoneSpecification].
+  ZoneSpecification toSpec() => ZoneSpecification(
+      handleUncaughtError: _errorZone ? _handleUncaughtError : null,
+      registerCallback: _registerCallback,
+      registerUnaryCallback: _registerUnaryCallback,
+      registerBinaryCallback: _registerBinaryCallback,
+      errorCallback: _errorCallback);
+
+  /// Returns the current stack chain.
+  ///
+  /// By default, the first frame of the first trace will be the line where
+  /// [currentChain] is called. If [level] is passed, the first trace will start
+  /// that many frames up instead.
+  Chain currentChain([int level = 0]) => _createNode(level + 1).toChain();
+
+  /// Returns the stack chain associated with [trace], if one exists.
+  ///
+  /// The first stack trace in the returned chain will always be [trace]
+  /// (converted to a [Trace] if necessary). If there is no chain associated
+  /// with [trace], this just returns a single-trace chain containing [trace].
+  Chain chainFor(StackTrace? trace) {
+    if (trace is Chain) return trace;
+    trace ??= StackTrace.current;
+
+    var previous = _chains[trace] ?? _currentNode;
+    if (previous == null) {
+      // If there's no [_currentNode], we're running synchronously beneath
+      // [Chain.capture] and we should fall back to the VM's stack chaining. We
+      // can't use [Chain.from] here because it'll just call [chainFor] again.
+      if (trace is Trace) return Chain([trace]);
+      return LazyChain(() => Chain.parse(trace!.toString()));
+    } else {
+      if (trace is! Trace) {
+        var original = trace;
+        trace = LazyTrace(() => Trace.parse(_trimVMChain(original)));
+      }
+
+      return _Node(trace, previous).toChain();
+    }
+  }
+
+  /// Tracks the current stack chain so it can be set to [_currentNode] when
+  /// [f] is run.
+  ZoneCallback<R> _registerCallback<R>(
+      Zone self, ZoneDelegate parent, Zone zone, R Function() f) {
+    if (_disabled) return parent.registerCallback(zone, f);
+    var node = _createNode(1);
+    return parent.registerCallback(zone, () => _run(f, node));
+  }
+
+  /// Tracks the current stack chain so it can be set to [_currentNode] when
+  /// [f] is run.
+  ZoneUnaryCallback<R, T> _registerUnaryCallback<R, T>(
+      Zone self,
+      ZoneDelegate parent,
+      Zone zone,
+      @pragma('vm:awaiter-link') R Function(T) f) {
+    if (_disabled) return parent.registerUnaryCallback(zone, f);
+    var node = _createNode(1);
+    return parent.registerUnaryCallback(
+        zone, (arg) => _run(() => f(arg), node));
+  }
+
+  /// Tracks the current stack chain so it can be set to [_currentNode] when
+  /// [f] is run.
+  ZoneBinaryCallback<R, T1, T2> _registerBinaryCallback<R, T1, T2>(
+      Zone self, ZoneDelegate parent, Zone zone, R Function(T1, T2) f) {
+    if (_disabled) return parent.registerBinaryCallback(zone, f);
+
+    var node = _createNode(1);
+    return parent.registerBinaryCallback(
+        zone, (arg1, arg2) => _run(() => f(arg1, arg2), node));
+  }
+
+  /// Looks up the chain associated with [stackTrace] and passes it either to
+  /// [_onError] or [parent]'s error handler.
+  void _handleUncaughtError(Zone self, ZoneDelegate parent, Zone zone,
+      Object error, StackTrace stackTrace) {
+    if (_disabled) {
+      parent.handleUncaughtError(zone, error, stackTrace);
+      return;
+    }
+
+    var stackChain = chainFor(stackTrace);
+    if (_onError == null) {
+      parent.handleUncaughtError(zone, error, stackChain);
+      return;
+    }
+
+    // TODO(nweiz): Currently this copies a lot of logic from [runZoned]. Just
+    // allow [runBinary] to throw instead once issue 18134 is fixed.
+    try {
+      // TODO(rnystrom): Is the null-assertion correct here? It is nullable in
+      // Zone. Should we check for that here?
+      self.parent!.runBinary(_onError, error, stackChain);
+    } on Object catch (newError, newStackTrace) {
+      if (identical(newError, error)) {
+        parent.handleUncaughtError(zone, error, stackChain);
+      } else {
+        parent.handleUncaughtError(zone, newError, newStackTrace);
+      }
+    }
+  }
+
+  /// Attaches the current stack chain to [stackTrace], replacing it if
+  /// necessary.
+  AsyncError? _errorCallback(Zone self, ZoneDelegate parent, Zone zone,
+      Object error, StackTrace? stackTrace) {
+    if (_disabled) return parent.errorCallback(zone, error, stackTrace);
+
+    // Go up two levels to get through [_CustomZone.errorCallback].
+    if (stackTrace == null) {
+      stackTrace = _createNode(2).toChain();
+    } else {
+      if (_chains[stackTrace] == null) _chains[stackTrace] = _createNode(2);
+    }
+
+    var asyncError = parent.errorCallback(zone, error, stackTrace);
+    return asyncError ?? AsyncError(error, stackTrace);
+  }
+
+  /// Creates a [_Node] with the current stack trace and linked to
+  /// [_currentNode].
+  ///
+  /// By default, the first frame of the first trace will be the line where
+  /// [_createNode] is called. If [level] is passed, the first trace will start
+  /// that many frames up instead.
+  _Node _createNode([int level = 0]) =>
+      _Node(_currentTrace(level + 1), _currentNode);
+
+  // TODO(nweiz): use a more robust way of detecting and tracking errors when
+  // issue 15105 is fixed.
+  /// Runs [f] with [_currentNode] set to [node].
+  ///
+  /// If [f] throws an error, this associates [node] with that error's stack
+  /// trace.
+  T _run<T>(T Function() f, _Node node) {
+    var previousNode = _currentNode;
+    _currentNode = node;
+    try {
+      return f();
+    } catch (e, stackTrace) {
+      // We can see the same stack trace multiple times if it's rethrown through
+      // guarded callbacks.  The innermost chain will have the most
+      // information so it should take precedence.
+      _chains[stackTrace] ??= node;
+      rethrow;
+    } finally {
+      _currentNode = previousNode;
+    }
+  }
+
+  /// Like [Trace.current], but if the current stack trace has VM chaining
+  /// enabled, this only returns the innermost sub-trace.
+  Trace _currentTrace([int? level]) {
+    var stackTrace = StackTrace.current;
+    return LazyTrace(() {
+      var text = _trimVMChain(stackTrace);
+      var trace = Trace.parse(text);
+      // JS includes a frame for the call to StackTrace.current, but the VM
+      // doesn't, so we skip an extra frame in a JS context.
+      return Trace(trace.frames.skip((level ?? 0) + (inJS ? 2 : 1)),
+          original: text);
+    });
+  }
+
+  /// Removes the VM's stack chains from the native [trace], since we're
+  /// generating our own and we don't want duplicate frames.
+  String _trimVMChain(StackTrace trace) {
+    var text = trace.toString();
+    var index = text.indexOf(vmChainGap);
+    return index == -1 ? text : text.substring(0, index);
+  }
+}
+
+/// A linked list node representing a single entry in a stack chain.
+class _Node {
+  /// The stack trace for this link of the chain.
+  final Trace trace;
+
+  /// The previous node in the chain.
+  final _Node? previous;
+
+  _Node(StackTrace trace, [this.previous]) : trace = Trace.from(trace);
+
+  /// Converts this to a [Chain].
+  Chain toChain() {
+    var nodes = <Trace>[];
+    _Node? node = this;
+    while (node != null) {
+      nodes.add(node.trace);
+      node = node.previous;
+    }
+    return Chain(nodes);
+  }
+}
diff --git a/pkgs/stack_trace/lib/src/trace.dart b/pkgs/stack_trace/lib/src/trace.dart
new file mode 100644
index 0000000..b8c62f5
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -0,0 +1,341 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' as math;
+
+import 'chain.dart';
+import 'frame.dart';
+import 'lazy_trace.dart';
+import 'unparsed_frame.dart';
+import 'utils.dart';
+import 'vm_trace.dart';
+
+final _terseRegExp = RegExp(r'(-patch)?([/\\].*)?$');
+
+/// A RegExp to match V8's stack traces.
+///
+/// V8's traces start with a line that's either just "Error" or else is a
+/// description of the exception that occurred. That description can be multiple
+/// lines, so we just look for any line other than the first that begins with
+/// three or four spaces and "at".
+final _v8Trace = RegExp(r'\n    ?at ');
+
+/// A RegExp to match indidual lines of V8's stack traces.
+///
+/// This is intended to filter out the leading exception details of the trace
+/// though it is possible for the message to match this as well.
+final _v8TraceLine = RegExp(r'    ?at ');
+
+/// A RegExp to match Firefox's eval and Function stack traces.
+///
+/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack
+///
+/// These stack traces look like:
+///
+/// ````
+/// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+/// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+/// ````
+final _firefoxEvalTrace = RegExp(r'@\S+ line \d+ >.* (Function|eval):\d+:\d+');
+
+/// A RegExp to match Firefox and Safari's stack traces.
+///
+/// Firefox and Safari have very similar stack trace formats, so we use the same
+/// logic for parsing them.
+///
+/// Firefox's trace frames start with the name of the function in which the
+/// error occurred, possibly including its parameters inside `()`. For example,
+/// `.VW.call$0("arg")@https://example.com/stuff.dart.js:560`.
+///
+/// Safari traces occasionally don't include the initial method name followed by
+/// "@", and they always have both the line and column number (or just a
+/// trailing colon if no column number is available). They can also contain
+/// empty lines or lines consisting only of `[native code]`.
+final _firefoxSafariTrace = RegExp(
+    r'^'
+    r'(' // Member description. Not present in some Safari frames.
+    r'([.0-9A-Za-z_$/<]|\(.*\))*' // Member name and arguments.
+    r'@'
+    r')?'
+    r'[^\s]*' // Frame URL.
+    r':\d*' // Line or column number. Some older frames only have a line number.
+    r'$',
+    multiLine: true);
+
+/// A RegExp to match this package's stack traces.
+final _friendlyTrace =
+    RegExp(r'^[^\s<][^\s]*( \d+(:\d+)?)?[ \t]+[^\s]+$', multiLine: true);
+
+/// A stack trace, comprised of a list of stack frames.
+class Trace implements StackTrace {
+  /// The stack frames that comprise this stack trace.
+  final List<Frame> frames;
+
+  /// The original stack trace from which this trace was parsed.
+  final StackTrace original;
+
+  /// Returns a human-readable representation of [stackTrace]. If [terse] is
+  /// set, this folds together multiple stack frames from the Dart core
+  /// libraries, so that only the core library method directly called from user
+  /// code is visible (see [Trace.terse]).
+  static String format(StackTrace stackTrace, {bool terse = true}) {
+    var trace = Trace.from(stackTrace);
+    if (terse) trace = trace.terse;
+    return trace.toString();
+  }
+
+  /// Returns the current stack trace.
+  ///
+  /// By default, the first frame of this trace will be the line where
+  /// [Trace.current] is called. If [level] is passed, the trace will start that
+  /// many frames up instead.
+  factory Trace.current([int level = 0]) {
+    if (level < 0) {
+      throw ArgumentError('Argument [level] must be greater than or equal '
+          'to 0.');
+    }
+
+    var trace = Trace.from(StackTrace.current);
+    return LazyTrace(
+      () =>
+          // JS includes a frame for the call to StackTrace.current, but the VM
+          // doesn't, so we skip an extra frame in a JS context.
+          Trace(trace.frames.skip(level + (inJS ? 2 : 1)),
+              original: trace.original.toString()),
+    );
+  }
+
+  /// Returns a new stack trace containing the same data as [trace].
+  ///
+  /// If [trace] is a native [StackTrace], its data will be parsed out; if it's
+  /// a [Trace], it will be returned as-is.
+  factory Trace.from(StackTrace trace) {
+    if (trace is Trace) return trace;
+    if (trace is Chain) return trace.toTrace();
+    return LazyTrace(() => Trace.parse(trace.toString()));
+  }
+
+  /// Parses a string representation of a stack trace.
+  ///
+  /// [trace] should be formatted in the same way as a Dart VM or browser stack
+  /// trace. If it's formatted as a stack chain, this will return the equivalent
+  /// of [Chain.toTrace].
+  factory Trace.parse(String trace) {
+    try {
+      if (trace.isEmpty) return Trace(<Frame>[]);
+      if (trace.contains(_v8Trace)) return Trace.parseV8(trace);
+      if (trace.contains('\tat ')) return Trace.parseJSCore(trace);
+      if (trace.contains(_firefoxSafariTrace) ||
+          trace.contains(_firefoxEvalTrace)) {
+        return Trace.parseFirefox(trace);
+      }
+      if (trace.contains(chainGap)) return Chain.parse(trace).toTrace();
+      if (trace.contains(_friendlyTrace)) {
+        return Trace.parseFriendly(trace);
+      }
+
+      // Default to parsing the stack trace as a VM trace. This is also hit on
+      // IE and Safari, where the stack trace is just an empty string (issue
+      // 11257).
+      return Trace.parseVM(trace);
+    } on FormatException catch (error) {
+      throw FormatException('${error.message}\nStack trace:\n$trace');
+    }
+  }
+
+  /// Parses a string representation of a Dart VM stack trace.
+  Trace.parseVM(String trace) : this(_parseVM(trace), original: trace);
+
+  static List<Frame> _parseVM(String trace) {
+    // Ignore [vmChainGap]. This matches the behavior of
+    // `Chain.parse().toTrace()`.
+    var lines = trace
+        .trim()
+        .replaceAll(vmChainGap, '')
+        .split('\n')
+        .where((line) => line.isNotEmpty);
+
+    if (lines.isEmpty) {
+      return [];
+    }
+
+    var frames = lines.take(lines.length - 1).map(Frame.parseVM).toList();
+
+    // TODO(nweiz): Remove this when issue 23614 is fixed.
+    if (!lines.last.endsWith('.da')) {
+      frames.add(Frame.parseVM(lines.last));
+    }
+
+    return frames;
+  }
+
+  /// Parses a string representation of a Chrome/V8 stack trace.
+  Trace.parseV8(String trace)
+      : this(
+            trace
+                .split('\n')
+                .skip(1)
+                // It's possible that an Exception's description contains a line
+                // that looks like a V8 trace line, which will screw this up.
+                // Unfortunately, that's impossible to detect.
+                .skipWhile((line) => !line.startsWith(_v8TraceLine))
+                .map(Frame.parseV8),
+            original: trace);
+
+  /// Parses a string representation of a JavaScriptCore stack trace.
+  Trace.parseJSCore(String trace)
+      : this(
+            trace
+                .split('\n')
+                .where((line) => line != '\tat ')
+                .map(Frame.parseV8),
+            original: trace);
+
+  /// Parses a string representation of an Internet Explorer stack trace.
+  ///
+  /// IE10+ traces look just like V8 traces. Prior to IE10, stack traces can't
+  /// be retrieved.
+  Trace.parseIE(String trace) : this.parseV8(trace);
+
+  /// Parses a string representation of a Firefox stack trace.
+  Trace.parseFirefox(String trace)
+      : this(
+            trace
+                .trim()
+                .split('\n')
+                .where((line) => line.isNotEmpty && line != '[native code]')
+                .map(Frame.parseFirefox),
+            original: trace);
+
+  /// Parses a string representation of a Safari stack trace.
+  Trace.parseSafari(String trace) : this.parseFirefox(trace);
+
+  /// Parses a string representation of a Safari 6.1+ stack trace.
+  @Deprecated('Use Trace.parseSafari instead.')
+  Trace.parseSafari6_1(String trace) : this.parseSafari(trace);
+
+  /// Parses a string representation of a Safari 6.0 stack trace.
+  @Deprecated('Use Trace.parseSafari instead.')
+  Trace.parseSafari6_0(String trace)
+      : this(
+            trace
+                .trim()
+                .split('\n')
+                .where((line) => line != '[native code]')
+                .map(Frame.parseFirefox),
+            original: trace);
+
+  /// Parses this package's string representation of a stack trace.
+  ///
+  /// This also parses string representations of [Chain]s. They parse to the
+  /// same trace that [Chain.toTrace] would return.
+  Trace.parseFriendly(String trace)
+      : this(
+            trace.isEmpty
+                ? []
+                : trace
+                    .trim()
+                    .split('\n')
+                    // Filter out asynchronous gaps from [Chain]s.
+                    .where((line) => !line.startsWith('====='))
+                    .map(Frame.parseFriendly),
+            original: trace);
+
+  /// Returns a new [Trace] comprised of [frames].
+  Trace(Iterable<Frame> frames, {String? original})
+      : frames = List<Frame>.unmodifiable(frames),
+        original = StackTrace.fromString(original ?? '');
+
+  /// Returns a VM-style [StackTrace] object.
+  ///
+  /// The return value's [toString] method will always return a string
+  /// representation in the Dart VM's stack trace format, regardless of what
+  /// platform is being used.
+  StackTrace get vmTrace => VMTrace(frames);
+
+  /// Returns a terser version of this trace.
+  ///
+  /// This is accomplished by folding together multiple stack frames from the
+  /// core library or from this package, as in [foldFrames]. Remaining core
+  /// library frames have their libraries, "-patch" suffixes, and line numbers
+  /// removed. If the outermost frame of the stack trace is a core library
+  /// frame, it's removed entirely.
+  ///
+  /// This won't do anything with a raw JavaScript trace, since there's no way
+  /// to determine which frames come from which Dart libraries. However, the
+  /// [`source_map_stack_trace`][https://pub.dev/packages/source_map_stack_trace]
+  /// package can be used to convert JavaScript traces into Dart-style traces.
+  ///
+  /// For custom folding, see [foldFrames].
+  Trace get terse => foldFrames((_) => false, terse: true);
+
+  /// Returns a new [Trace] based on `this` where multiple stack frames matching
+  /// [predicate] are folded together.
+  ///
+  /// This means that whenever there are multiple frames in a row that match
+  /// [predicate], only the last one is kept. This is useful for limiting the
+  /// amount of library code that appears in a stack trace by only showing user
+  /// code and code that's called by user code.
+  ///
+  /// If [terse] is true, this will also fold together frames from the core
+  /// library or from this package, simplify core library frames, and
+  /// potentially remove the outermost frame as in [Trace.terse].
+  Trace foldFrames(bool Function(Frame) predicate, {bool terse = false}) {
+    if (terse) {
+      var oldPredicate = predicate;
+      predicate = (frame) {
+        if (oldPredicate(frame)) return true;
+
+        if (frame.isCore) return true;
+        if (frame.package == 'stack_trace') return true;
+
+        // Ignore async stack frames without any line or column information.
+        // These come from the VM's async/await implementation and represent
+        // internal frames. They only ever show up in stack chains and are
+        // always surrounded by other traces that are actually useful, so we can
+        // just get rid of them.
+        // TODO(nweiz): Get rid of this logic some time after issue 22009 is
+        // fixed.
+        if (!frame.member!.contains('<async>')) return false;
+        return frame.line == null;
+      };
+    }
+
+    var newFrames = <Frame>[];
+    for (var frame in frames.reversed) {
+      if (frame is UnparsedFrame || !predicate(frame)) {
+        newFrames.add(frame);
+      } else if (newFrames.isEmpty || !predicate(newFrames.last)) {
+        newFrames.add(Frame(frame.uri, frame.line, frame.column, frame.member));
+      }
+    }
+
+    if (terse) {
+      newFrames = newFrames.map((frame) {
+        if (frame is UnparsedFrame || !predicate(frame)) return frame;
+        var library = frame.library.replaceAll(_terseRegExp, '');
+        return Frame(Uri.parse(library), null, null, frame.member);
+      }).toList();
+
+      if (newFrames.length > 1 && predicate(newFrames.first)) {
+        newFrames.removeAt(0);
+      }
+    }
+
+    return Trace(newFrames.reversed, original: original.toString());
+  }
+
+  @override
+  String toString() {
+    // Figure out the longest path so we know how much to pad.
+    var longest =
+        frames.map((frame) => frame.location.length).fold(0, math.max);
+
+    // Print out the stack trace nicely formatted.
+    return frames.map((frame) {
+      if (frame is UnparsedFrame) return '$frame\n';
+      return '${frame.location.padRight(longest)}  ${frame.member}\n';
+    }).join();
+  }
+}
diff --git a/pkgs/stack_trace/lib/src/unparsed_frame.dart b/pkgs/stack_trace/lib/src/unparsed_frame.dart
new file mode 100644
index 0000000..27e97f6
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/unparsed_frame.dart
@@ -0,0 +1,33 @@
+// 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 'frame.dart';
+
+/// A frame that failed to parse.
+///
+/// The [member] property contains the original frame's contents.
+class UnparsedFrame implements Frame {
+  @override
+  final Uri uri = Uri(path: 'unparsed');
+  @override
+  final int? line = null;
+  @override
+  final int? column = null;
+  @override
+  final bool isCore = false;
+  @override
+  final String library = 'unparsed';
+  @override
+  final String? package = null;
+  @override
+  final String location = 'unparsed';
+
+  @override
+  final String member;
+
+  UnparsedFrame(this.member);
+
+  @override
+  String toString() => member;
+}
diff --git a/pkgs/stack_trace/lib/src/utils.dart b/pkgs/stack_trace/lib/src/utils.dart
new file mode 100644
index 0000000..bd971fe
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/utils.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// The line used in the string representation of stack chains to represent
+/// the gap between traces.
+const chainGap = '===== asynchronous gap ===========================\n';
+
+/// The line used in the string representation of VM stack chains to represent
+/// the gap between traces.
+final vmChainGap = RegExp(r'^<asynchronous suspension>\n?$', multiLine: true);
+
+// TODO(nweiz): When cross-platform imports work, use them to set this.
+/// Whether we're running in a JS context.
+const bool inJS = 0.0 is int;
diff --git a/pkgs/stack_trace/lib/src/vm_trace.dart b/pkgs/stack_trace/lib/src/vm_trace.dart
new file mode 100644
index 0000000..005b7af
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/vm_trace.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'frame.dart';
+
+/// An implementation of [StackTrace] that emulates the behavior of the VM's
+/// implementation.
+///
+/// In particular, when [toString] is called, this returns a string in the VM's
+/// stack trace format.
+class VMTrace implements StackTrace {
+  /// The stack frames that comprise this stack trace.
+  final List<Frame> frames;
+
+  VMTrace(this.frames);
+
+  @override
+  String toString() {
+    var i = 1;
+    return frames.map((frame) {
+      var number = '#${i++}'.padRight(8);
+      var member = frame.member!
+          .replaceAllMapped(RegExp(r'[^.]+\.<async>'),
+              (match) => '${match[1]}.<${match[1]}_async_body>')
+          .replaceAll('<fn>', '<anonymous closure>');
+      var line = frame.line ?? 0;
+      var column = frame.column ?? 0;
+      return '$number$member (${frame.uri}:$line:$column)\n';
+    }).join();
+  }
+}
diff --git a/pkgs/stack_trace/lib/stack_trace.dart b/pkgs/stack_trace/lib/stack_trace.dart
new file mode 100644
index 0000000..fad30ce
--- /dev/null
+++ b/pkgs/stack_trace/lib/stack_trace.dart
@@ -0,0 +1,8 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/chain.dart';
+export 'src/frame.dart';
+export 'src/trace.dart';
+export 'src/unparsed_frame.dart';
diff --git a/pkgs/stack_trace/pubspec.yaml b/pkgs/stack_trace/pubspec.yaml
new file mode 100644
index 0000000..4f387b1
--- /dev/null
+++ b/pkgs/stack_trace/pubspec.yaml
@@ -0,0 +1,14 @@
+name: stack_trace
+version: 1.12.1
+description: A package for manipulating stack traces and printing them readably.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/stack_trace
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  path: ^1.8.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.6
diff --git a/pkgs/stack_trace/test/chain/chain_test.dart b/pkgs/stack_trace/test/chain/chain_test.dart
new file mode 100644
index 0000000..d5426dd
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/chain_test.dart
@@ -0,0 +1,375 @@
+// 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 'dart:async';
+
+import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  group('Chain.parse()', () {
+    test('parses a real Chain', () async {
+      // ignore: only_throw_errors
+      final chain = await captureFuture(() => inMicrotask(() => throw 'error'));
+
+      expect(
+        Chain.parse(chain.toString()).toString(),
+        equals(chain.toString()),
+      );
+    });
+
+    test('parses an empty string', () {
+      var chain = Chain.parse('');
+      expect(chain.traces, isEmpty);
+    });
+
+    test('parses a chain containing empty traces', () {
+      var chain =
+          Chain.parse('===== asynchronous gap ===========================\n'
+              '===== asynchronous gap ===========================\n');
+      expect(chain.traces, hasLength(3));
+      expect(chain.traces[0].frames, isEmpty);
+      expect(chain.traces[1].frames, isEmpty);
+      expect(chain.traces[2].frames, isEmpty);
+    });
+
+    test('parses a chain with VM gaps', () {
+      final chain =
+          Chain.parse('#1      MyClass.run (package:my_lib.dart:134:5)\n'
+              '<asynchronous suspension>\n'
+              '#2      main (file:///my_app.dart:9:3)\n'
+              '<asynchronous suspension>\n');
+      expect(chain.traces, hasLength(2));
+      expect(chain.traces[0].frames, hasLength(1));
+      expect(chain.traces[0].frames[0].toString(),
+          equals('package:my_lib.dart 134:5 in MyClass.run'));
+      expect(chain.traces[1].frames, hasLength(1));
+      expect(
+        chain.traces[1].frames[0].toString(),
+        anyOf(
+          equals('/my_app.dart 9:3 in main'), // VM
+          equals('file:///my_app.dart 9:3 in main'), // Browser
+        ),
+      );
+    });
+  });
+
+  group('Chain.capture()', () {
+    test('with onError blocks errors', () {
+      Chain.capture(() {
+        return Future<void>.error('oh no');
+      }, onError: expectAsync2((error, chain) {
+        expect(error, equals('oh no'));
+        expect(chain, isA<Chain>());
+      })).then(expectAsync1((_) {}, count: 0),
+          onError: expectAsync2((_, __) {}, count: 0));
+    });
+
+    test('with no onError blocks errors', () {
+      runZonedGuarded(() {
+        Chain.capture(() => Future<void>.error('oh no')).then(
+            expectAsync1((_) {}, count: 0),
+            onError: expectAsync2((_, __) {}, count: 0));
+      }, expectAsync2((error, chain) {
+        expect(error, equals('oh no'));
+        expect(chain, isA<Chain>());
+      }));
+    });
+
+    test("with errorZone: false doesn't block errors", () {
+      expect(Chain.capture(() => Future<void>.error('oh no'), errorZone: false),
+          throwsA('oh no'));
+    });
+
+    test("doesn't allow onError and errorZone: false", () {
+      expect(() => Chain.capture(() {}, onError: (_, __) {}, errorZone: false),
+          throwsArgumentError);
+    });
+
+    group('with when: false', () {
+      test("with no onError doesn't block errors", () {
+        expect(Chain.capture(() => Future<void>.error('oh no'), when: false),
+            throwsA('oh no'));
+      });
+
+      test('with onError blocks errors', () {
+        Chain.capture(() {
+          return Future<void>.error('oh no');
+        }, onError: expectAsync2((error, chain) {
+          expect(error, equals('oh no'));
+          expect(chain, isA<Chain>());
+        }), when: false);
+      });
+
+      test("doesn't enable chain-tracking", () {
+        return Chain.disable(() {
+          return Chain.capture(() {
+            var completer = Completer<Chain>();
+            inMicrotask(() {
+              completer.complete(Chain.current());
+            });
+
+            return completer.future.then((chain) {
+              expect(chain.traces, hasLength(1));
+            });
+          }, when: false);
+        });
+      });
+    });
+  });
+
+  test('Chain.capture() with custom zoneValues', () {
+    return Chain.capture(() {
+      expect(Zone.current[#enabled], true);
+    }, zoneValues: {#enabled: true});
+  });
+
+  group('Chain.disable()', () {
+    test('disables chain-tracking', () {
+      return Chain.disable(() {
+        var completer = Completer<Chain>();
+        inMicrotask(() => completer.complete(Chain.current()));
+
+        return completer.future.then((chain) {
+          expect(chain.traces, hasLength(1));
+        });
+      });
+    });
+
+    test('Chain.capture() re-enables chain-tracking', () {
+      return Chain.disable(() {
+        return Chain.capture(() {
+          var completer = Completer<Chain>();
+          inMicrotask(() => completer.complete(Chain.current()));
+
+          return completer.future.then((chain) {
+            expect(chain.traces, hasLength(2));
+          });
+        });
+      });
+    });
+
+    test('preserves parent zones of the capture zone', () {
+      // The outer disable call turns off the test package's chain-tracking.
+      return Chain.disable(() {
+        return runZoned(() {
+          return Chain.capture(() {
+            expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
+          });
+        }, zoneValues: {#enabled: true});
+      });
+    });
+
+    test('preserves child zones of the capture zone', () {
+      // The outer disable call turns off the test package's chain-tracking.
+      return Chain.disable(() {
+        return Chain.capture(() {
+          return runZoned(() {
+            expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
+          }, zoneValues: {#enabled: true});
+        });
+      });
+    });
+
+    test("with when: false doesn't disable", () {
+      return Chain.capture(() {
+        return Chain.disable(() {
+          var completer = Completer<Chain>();
+          inMicrotask(() => completer.complete(Chain.current()));
+
+          return completer.future.then((chain) {
+            expect(chain.traces, hasLength(2));
+          });
+        }, when: false);
+      });
+    });
+  });
+
+  test('toString() ensures that all traces are aligned', () {
+    var chain = Chain([
+      Trace.parse('short 10:11  Foo.bar\n'),
+      Trace.parse('loooooooooooong 10:11  Zop.zoop')
+    ]);
+
+    expect(
+        chain.toString(),
+        equals('short 10:11            Foo.bar\n'
+            '===== asynchronous gap ===========================\n'
+            'loooooooooooong 10:11  Zop.zoop\n'));
+  });
+
+  var userSlashCode = p.join('user', 'code.dart');
+  group('Chain.terse', () {
+    test('makes each trace terse', () {
+      var chain = Chain([
+        Trace.parse('dart:core 10:11       Foo.bar\n'
+            'dart:core 10:11       Bar.baz\n'
+            'user/code.dart 10:11  Bang.qux\n'
+            'dart:core 10:11       Zip.zap\n'
+            'dart:core 10:11       Zop.zoop'),
+        Trace.parse('user/code.dart 10:11                        Bang.qux\n'
+            'dart:core 10:11                             Foo.bar\n'
+            'package:stack_trace/stack_trace.dart 10:11  Bar.baz\n'
+            'dart:core 10:11                             Zip.zap\n'
+            'user/code.dart 10:11                        Zop.zoop')
+      ]);
+
+      expect(
+          chain.terse.toString(),
+          equals('dart:core             Bar.baz\n'
+              '$userSlashCode 10:11  Bang.qux\n'
+              '===== asynchronous gap ===========================\n'
+              '$userSlashCode 10:11  Bang.qux\n'
+              'dart:core             Zip.zap\n'
+              '$userSlashCode 10:11  Zop.zoop\n'));
+    });
+
+    test('eliminates internal-only traces', () {
+      var chain = Chain([
+        Trace.parse('user/code.dart 10:11  Foo.bar\n'
+            'dart:core 10:11       Bar.baz'),
+        Trace.parse('dart:core 10:11                             Foo.bar\n'
+            'package:stack_trace/stack_trace.dart 10:11  Bar.baz\n'
+            'dart:core 10:11                             Zip.zap'),
+        Trace.parse('user/code.dart 10:11  Foo.bar\n'
+            'dart:core 10:11       Bar.baz')
+      ]);
+
+      expect(
+          chain.terse.toString(),
+          equals('$userSlashCode 10:11  Foo.bar\n'
+              '===== asynchronous gap ===========================\n'
+              '$userSlashCode 10:11  Foo.bar\n'));
+    });
+
+    test("doesn't return an empty chain", () {
+      var chain = Chain([
+        Trace.parse('dart:core 10:11                             Foo.bar\n'
+            'package:stack_trace/stack_trace.dart 10:11  Bar.baz\n'
+            'dart:core 10:11                             Zip.zap'),
+        Trace.parse('dart:core 10:11                             A.b\n'
+            'package:stack_trace/stack_trace.dart 10:11  C.d\n'
+            'dart:core 10:11                             E.f')
+      ]);
+
+      expect(chain.terse.toString(), equals('dart:core  E.f\n'));
+    });
+
+    // Regression test for #9
+    test("doesn't crash on empty traces", () {
+      var chain = Chain([
+        Trace.parse('user/code.dart 10:11  Bang.qux'),
+        Trace([]),
+        Trace.parse('user/code.dart 10:11  Bang.qux')
+      ]);
+
+      expect(
+          chain.terse.toString(),
+          equals('$userSlashCode 10:11  Bang.qux\n'
+              '===== asynchronous gap ===========================\n'
+              '$userSlashCode 10:11  Bang.qux\n'));
+    });
+  });
+
+  group('Chain.foldFrames', () {
+    test('folds each trace', () {
+      var chain = Chain([
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bar.baz\n'
+            'b.dart 10:11  Bang.qux\n'
+            'a.dart 10:11  Zip.zap\n'
+            'a.dart 10:11  Zop.zoop'),
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bar.baz\n'
+            'a.dart 10:11  Bang.qux\n'
+            'a.dart 10:11  Zip.zap\n'
+            'b.dart 10:11  Zop.zoop')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+      expect(
+          folded.toString(),
+          equals('a.dart 10:11  Bar.baz\n'
+              'b.dart 10:11  Bang.qux\n'
+              'a.dart 10:11  Zop.zoop\n'
+              '===== asynchronous gap ===========================\n'
+              'a.dart 10:11  Zip.zap\n'
+              'b.dart 10:11  Zop.zoop\n'));
+    });
+
+    test('with terse: true, folds core frames as well', () {
+      var chain = Chain([
+        Trace.parse('a.dart 10:11                        Foo.bar\n'
+            'dart:async-patch/future.dart 10:11  Zip.zap\n'
+            'b.dart 10:11                        Bang.qux\n'
+            'dart:core 10:11                     Bar.baz\n'
+            'a.dart 10:11                        Zop.zoop'),
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bar.baz\n'
+            'a.dart 10:11  Bang.qux\n'
+            'a.dart 10:11  Zip.zap\n'
+            'b.dart 10:11  Zop.zoop')
+      ]);
+
+      var folded =
+          chain.foldFrames((frame) => frame.library == 'a.dart', terse: true);
+      expect(
+          folded.toString(),
+          equals('dart:async    Zip.zap\n'
+              'b.dart 10:11  Bang.qux\n'
+              '===== asynchronous gap ===========================\n'
+              'a.dart        Zip.zap\n'
+              'b.dart 10:11  Zop.zoop\n'));
+    });
+
+    test('eliminates completely-folded traces', () {
+      var chain = Chain([
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'b.dart 10:11  Bang.qux'),
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bang.qux'),
+        Trace.parse('a.dart 10:11  Zip.zap\n'
+            'b.dart 10:11  Zop.zoop')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+      expect(
+          folded.toString(),
+          equals('a.dart 10:11  Foo.bar\n'
+              'b.dart 10:11  Bang.qux\n'
+              '===== asynchronous gap ===========================\n'
+              'a.dart 10:11  Zip.zap\n'
+              'b.dart 10:11  Zop.zoop\n'));
+    });
+
+    test("doesn't return an empty trace", () {
+      var chain = Chain([
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bang.qux')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+      expect(folded.toString(), equals('a.dart 10:11  Bang.qux\n'));
+    });
+  });
+
+  test('Chain.toTrace eliminates asynchronous gaps', () {
+    var trace = Chain([
+      Trace.parse('user/code.dart 10:11  Foo.bar\n'
+          'dart:core 10:11       Bar.baz'),
+      Trace.parse('user/code.dart 10:11  Foo.bar\n'
+          'dart:core 10:11       Bar.baz')
+    ]).toTrace();
+
+    expect(
+        trace.toString(),
+        equals('$userSlashCode 10:11  Foo.bar\n'
+            'dart:core 10:11       Bar.baz\n'
+            '$userSlashCode 10:11  Foo.bar\n'
+            'dart:core 10:11       Bar.baz\n'));
+  });
+}
diff --git a/pkgs/stack_trace/test/chain/dart2js_test.dart b/pkgs/stack_trace/test/chain/dart2js_test.dart
new file mode 100644
index 0000000..abb842d
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/dart2js_test.dart
@@ -0,0 +1,337 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+// dart2js chain tests are separated out because dart2js stack traces are
+// inconsistent due to inlining and browser differences. These tests don't
+// assert anything about the content of the traces, just the number of traces in
+// a chain.
+@TestOn('js')
+library;
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  group('capture() with onError catches exceptions', () {
+    test('thrown synchronously', () async {
+      var chain = await captureFuture(() => throw 'error');
+      expect(chain.traces, hasLength(1));
+    });
+
+    test('thrown in a microtask', () async {
+      var chain = await captureFuture(() => inMicrotask(() => throw 'error'));
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('thrown in a one-shot timer', () async {
+      var chain =
+          await captureFuture(() => inOneShotTimer(() => throw 'error'));
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('thrown in a periodic timer', () async {
+      var chain =
+          await captureFuture(() => inPeriodicTimer(() => throw 'error'));
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('thrown in a nested series of asynchronous operations', () async {
+      var chain = await captureFuture(() {
+        inPeriodicTimer(() {
+          inOneShotTimer(() => inMicrotask(() => throw 'error'));
+        });
+      });
+
+      expect(chain.traces, hasLength(4));
+    });
+
+    test('thrown in a long future chain', () async {
+      var chain = await captureFuture(() => inFutureChain(() => throw 'error'));
+
+      // Despite many asynchronous operations, there's only one level of
+      // nested calls, so there should be only two traces in the chain. This
+      // is important; programmers expect stack trace memory consumption to be
+      // O(depth of program), not O(length of program).
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('thrown in new Future()', () async {
+      var chain = await captureFuture(() => inNewFuture(() => throw 'error'));
+      expect(chain.traces, hasLength(3));
+    });
+
+    test('thrown in new Future.sync()', () async {
+      var chain = await captureFuture(() {
+        inMicrotask(() => inSyncFuture(() => throw 'error'));
+      });
+
+      expect(chain.traces, hasLength(3));
+    });
+
+    test('multiple times', () {
+      var completer = Completer<void>();
+      var first = true;
+
+      Chain.capture(() {
+        inMicrotask(() => throw 'first error');
+        inPeriodicTimer(() => throw 'second error');
+      }, onError: (error, chain) {
+        try {
+          if (first) {
+            expect(error, equals('first error'));
+            expect(chain.traces, hasLength(2));
+            first = false;
+          } else {
+            expect(error, equals('second error'));
+            expect(chain.traces, hasLength(2));
+            completer.complete();
+          }
+        } on Object catch (error, stackTrace) {
+          completer.completeError(error, stackTrace);
+        }
+      });
+
+      return completer.future;
+    });
+
+    test('passed to a completer', () async {
+      var trace = Trace.current();
+      var chain = await captureFuture(() {
+        inMicrotask(() => completerErrorFuture(trace));
+      });
+
+      expect(chain.traces, hasLength(3));
+
+      // The first trace is the trace that was manually reported for the
+      // error.
+      expect(chain.traces.first.toString(), equals(trace.toString()));
+    });
+
+    test('passed to a completer with no stack trace', () async {
+      var chain = await captureFuture(() {
+        inMicrotask(completerErrorFuture);
+      });
+
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('passed to a stream controller', () async {
+      var trace = Trace.current();
+      var chain = await captureFuture(() {
+        inMicrotask(() => controllerErrorStream(trace).listen(null));
+      });
+
+      expect(chain.traces, hasLength(3));
+      expect(chain.traces.first.toString(), equals(trace.toString()));
+    });
+
+    test('passed to a stream controller with no stack trace', () async {
+      var chain = await captureFuture(() {
+        inMicrotask(() => controllerErrorStream().listen(null));
+      });
+
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('and relays them to the parent zone', () {
+      var completer = Completer<void>();
+
+      runZonedGuarded(() {
+        Chain.capture(() {
+          inMicrotask(() => throw 'error');
+        }, onError: (error, chain) {
+          expect(error, equals('error'));
+          expect(chain.traces, hasLength(2));
+          throw error;
+        });
+      }, (error, chain) {
+        try {
+          expect(error, equals('error'));
+          expect(chain,
+              isA<Chain>().having((c) => c.traces, 'traces', hasLength(2)));
+          completer.complete();
+        } on Object catch (error, stackTrace) {
+          completer.completeError(error, stackTrace);
+        }
+      });
+
+      return completer.future;
+    });
+  });
+
+  test('capture() without onError passes exceptions to parent zone', () {
+    var completer = Completer<void>();
+
+    runZonedGuarded(() {
+      Chain.capture(() => inMicrotask(() => throw 'error'));
+    }, (error, chain) {
+      try {
+        expect(error, equals('error'));
+        expect(chain,
+            isA<Chain>().having((c) => c.traces, 'traces', hasLength(2)));
+        completer.complete();
+      } on Object catch (error, stackTrace) {
+        completer.completeError(error, stackTrace);
+      }
+    });
+
+    return completer.future;
+  });
+
+  group('current() within capture()', () {
+    test('called in a microtask', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inMicrotask(() => completer.complete(Chain.current()));
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('called in a one-shot timer', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inOneShotTimer(() => completer.complete(Chain.current()));
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('called in a periodic timer', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inPeriodicTimer(() => completer.complete(Chain.current()));
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('called in a nested series of asynchronous operations', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inPeriodicTimer(() {
+          inOneShotTimer(() {
+            inMicrotask(() => completer.complete(Chain.current()));
+          });
+        });
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(4));
+    });
+
+    test('called in a long future chain', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inFutureChain(() => completer.complete(Chain.current()));
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(2));
+    });
+  });
+
+  test(
+    'current() outside of capture() returns a chain wrapping the current trace',
+    () =>
+        // The test runner runs all tests with chains enabled.
+        Chain.disable(() async {
+      var completer = Completer<Chain>();
+      inMicrotask(() => completer.complete(Chain.current()));
+
+      var chain = await completer.future;
+      // Since the chain wasn't loaded within [Chain.capture], the full stack
+      // chain isn't available and it just returns the current stack when
+      // called.
+      expect(chain.traces, hasLength(1));
+    }),
+  );
+
+  group('forTrace() within capture()', () {
+    test('called for a stack trace from a microtask', () async {
+      var chain = await Chain.capture(
+          () => chainForTrace(inMicrotask, () => throw 'error'));
+
+      // Because [chainForTrace] has to set up a future chain to capture the
+      // stack trace while still showing it to the zone specification, it adds
+      // an additional level of async nesting and so an additional trace.
+      expect(chain.traces, hasLength(3));
+    });
+
+    test('called for a stack trace from a one-shot timer', () async {
+      var chain = await Chain.capture(
+          () => chainForTrace(inOneShotTimer, () => throw 'error'));
+
+      expect(chain.traces, hasLength(3));
+    });
+
+    test('called for a stack trace from a periodic timer', () async {
+      var chain = await Chain.capture(
+          () => chainForTrace(inPeriodicTimer, () => throw 'error'));
+
+      expect(chain.traces, hasLength(3));
+    });
+
+    test(
+        'called for a stack trace from a nested series of asynchronous '
+        'operations', () async {
+      var chain = await Chain.capture(() => chainForTrace((callback) {
+            inPeriodicTimer(() => inOneShotTimer(() => inMicrotask(callback)));
+          }, () => throw 'error'));
+
+      expect(chain.traces, hasLength(5));
+    });
+
+    test('called for a stack trace from a long future chain', () async {
+      var chain = await Chain.capture(
+          () => chainForTrace(inFutureChain, () => throw 'error'));
+
+      expect(chain.traces, hasLength(3));
+    });
+
+    test(
+        'called for an unregistered stack trace returns a chain wrapping that '
+        'trace', () {
+      late StackTrace trace;
+      var chain = Chain.capture(() {
+        try {
+          throw 'error';
+        } catch (_, stackTrace) {
+          trace = stackTrace;
+          return Chain.forTrace(stackTrace);
+        }
+      });
+
+      expect(chain.traces, hasLength(1));
+      expect(
+          chain.traces.first.toString(), equals(Trace.from(trace).toString()));
+    });
+  });
+
+  test(
+      'forTrace() outside of capture() returns a chain wrapping the given '
+      'trace', () {
+    late StackTrace trace;
+    var chain = Chain.capture(() {
+      try {
+        throw 'error';
+      } catch (_, stackTrace) {
+        trace = stackTrace;
+        return Chain.forTrace(stackTrace);
+      }
+    });
+
+    expect(chain.traces, hasLength(1));
+    expect(chain.traces.first.toString(), equals(Trace.from(trace).toString()));
+  });
+}
diff --git a/pkgs/stack_trace/test/chain/utils.dart b/pkgs/stack_trace/test/chain/utils.dart
new file mode 100644
index 0000000..27fb0e6
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/utils.dart
@@ -0,0 +1,94 @@
+// 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 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+/// Runs [callback] in a microtask callback.
+void inMicrotask(void Function() callback) => scheduleMicrotask(callback);
+
+/// Runs [callback] in a one-shot timer callback.
+void inOneShotTimer(void Function() callback) => Timer.run(callback);
+
+/// Runs [callback] once in a periodic timer callback.
+void inPeriodicTimer(void Function() callback) {
+  var count = 0;
+  Timer.periodic(const Duration(milliseconds: 1), (timer) {
+    count++;
+    if (count != 5) return;
+    timer.cancel();
+    callback();
+  });
+}
+
+/// Runs [callback] within a long asynchronous Future chain.
+void inFutureChain(void Function() callback) {
+  Future(() {})
+      .then((_) => Future(() {}))
+      .then((_) => Future(() {}))
+      .then((_) => Future(() {}))
+      .then((_) => Future(() {}))
+      .then((_) => callback())
+      .then((_) => Future(() {}));
+}
+
+void inNewFuture(void Function() callback) {
+  Future(callback);
+}
+
+void inSyncFuture(void Function() callback) {
+  Future.sync(callback);
+}
+
+/// Returns a Future that completes to an error using a completer.
+///
+/// If [trace] is passed, it's used as the stack trace for the error.
+Future<void> completerErrorFuture([StackTrace? trace]) {
+  var completer = Completer<void>();
+  completer.completeError('error', trace);
+  return completer.future;
+}
+
+/// Returns a Stream that emits an error using a controller.
+///
+/// If [trace] is passed, it's used as the stack trace for the error.
+Stream<void> controllerErrorStream([StackTrace? trace]) {
+  var controller = StreamController<void>();
+  controller.addError('error', trace);
+  return controller.stream;
+}
+
+/// Runs [callback] within [asyncFn], then converts any errors raised into a
+/// [Chain] with [Chain.forTrace].
+Future<Chain> chainForTrace(
+    void Function(void Function()) asyncFn, void Function() callback) {
+  var completer = Completer<Chain>();
+  asyncFn(() {
+    // We use `new Future.value().then(...)` here as opposed to [new Future] or
+    // [new Future.sync] because those methods don't pass the exception through
+    // the zone specification before propagating it, so there's no chance to
+    // attach a chain to its stack trace. See issue 15105.
+    Future<void>.value()
+        .then((_) => callback())
+        .catchError(completer.completeError);
+  });
+
+  return completer.future
+      .catchError((_, StackTrace stackTrace) => Chain.forTrace(stackTrace));
+}
+
+/// Runs [callback] in a [Chain.capture] zone and returns a Future that
+/// completes to the stack chain for an error thrown by [callback].
+///
+/// [callback] is expected to throw the string `"error"`.
+Future<Chain> captureFuture(void Function() callback) {
+  var completer = Completer<Chain>();
+  Chain.capture(callback, onError: (error, chain) {
+    expect(error, equals('error'));
+    completer.complete(chain);
+  });
+  return completer.future;
+}
diff --git a/pkgs/stack_trace/test/chain/vm_test.dart b/pkgs/stack_trace/test/chain/vm_test.dart
new file mode 100644
index 0000000..5c6c0b7
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/vm_test.dart
@@ -0,0 +1,508 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+// VM chain tests can rely on stronger guarantees about the contents of the
+// stack traces than dart2js.
+@TestOn('dart-vm')
+library;
+
+import 'dart:async';
+
+import 'package:stack_trace/src/utils.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+import 'utils.dart';
+
+void main() {
+  group('capture() with onError catches exceptions', () {
+    test('thrown synchronously', () async {
+      late StackTrace vmTrace;
+      var chain = await captureFuture(() {
+        try {
+          throw 'error';
+        } catch (_, stackTrace) {
+          vmTrace = stackTrace;
+          rethrow;
+        }
+      });
+
+      // Because there's no chain context for a synchronous error, we fall back
+      // on the VM's stack chain tracking.
+      expect(
+          chain.toString(), equals(Chain.parse(vmTrace.toString()).toString()));
+    });
+
+    test('thrown in a microtask', () {
+      return captureFuture(() => inMicrotask(() => throw 'error'))
+          .then((chain) {
+        // Since there was only one asynchronous operation, there should be only
+        // two traces in the chain.
+        expect(chain.traces, hasLength(2));
+
+        // The first frame of the first trace should be the line on which the
+        // actual error was thrown.
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+
+        // The second trace should describe the stack when the error callback
+        // was scheduled.
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('thrown in a one-shot timer', () {
+      return captureFuture(() => inOneShotTimer(() => throw 'error'))
+          .then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+      });
+    });
+
+    test('thrown in a periodic timer', () {
+      return captureFuture(() => inPeriodicTimer(() => throw 'error'))
+          .then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('thrown in a nested series of asynchronous operations', () {
+      return captureFuture(() {
+        inPeriodicTimer(() {
+          inOneShotTimer(() => inMicrotask(() => throw 'error'));
+        });
+      }).then((chain) {
+        expect(chain.traces, hasLength(4));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+        expect(chain.traces[3].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('thrown in a long future chain', () {
+      return captureFuture(() => inFutureChain(() => throw 'error'))
+          .then((chain) {
+        // Despite many asynchronous operations, there's only one level of
+        // nested calls, so there should be only two traces in the chain. This
+        // is important; programmers expect stack trace memory consumption to be
+        // O(depth of program), not O(length of program).
+        expect(chain.traces, hasLength(2));
+
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inFutureChain'))));
+      });
+    });
+
+    test('thrown in new Future()', () {
+      return captureFuture(() => inNewFuture(() => throw 'error'))
+          .then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+
+        // The second trace is the one captured by
+        // [StackZoneSpecification.errorCallback]. Because that runs
+        // asynchronously within [new Future], it doesn't actually refer to the
+        // source file at all.
+        expect(chain.traces[1].frames,
+            everyElement(frameLibrary(isNot(contains('chain_test')))));
+
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inNewFuture'))));
+      });
+    });
+
+    test('thrown in new Future.sync()', () {
+      return captureFuture(() {
+        inMicrotask(() => inSyncFuture(() => throw 'error'));
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inSyncFuture'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('multiple times', () {
+      var completer = Completer<void>();
+      var first = true;
+
+      Chain.capture(() {
+        inMicrotask(() => throw 'first error');
+        inPeriodicTimer(() => throw 'second error');
+      }, onError: (error, chain) {
+        try {
+          if (first) {
+            expect(error, equals('first error'));
+            expect(chain.traces[1].frames,
+                contains(frameMember(startsWith('inMicrotask'))));
+            first = false;
+          } else {
+            expect(error, equals('second error'));
+            expect(chain.traces[1].frames,
+                contains(frameMember(startsWith('inPeriodicTimer'))));
+            completer.complete();
+          }
+        } on Object catch (error, stackTrace) {
+          completer.completeError(error, stackTrace);
+        }
+      });
+
+      return completer.future;
+    });
+
+    test('passed to a completer', () {
+      var trace = Trace.current();
+      return captureFuture(() {
+        inMicrotask(() => completerErrorFuture(trace));
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+
+        // The first trace is the trace that was manually reported for the
+        // error.
+        expect(chain.traces.first.toString(), equals(trace.toString()));
+
+        // The second trace is the trace that was captured when
+        // [Completer.addError] was called.
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('completerErrorFuture'))));
+
+        // The third trace is the automatically-captured trace from when the
+        // microtask was scheduled.
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('passed to a completer with no stack trace', () {
+      return captureFuture(() {
+        inMicrotask(completerErrorFuture);
+      }).then((chain) {
+        expect(chain.traces, hasLength(2));
+
+        // The first trace is the one captured when [Completer.addError] was
+        // called.
+        expect(chain.traces[0].frames,
+            contains(frameMember(startsWith('completerErrorFuture'))));
+
+        // The second trace is the automatically-captured trace from when the
+        // microtask was scheduled.
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('passed to a stream controller', () {
+      var trace = Trace.current();
+      return captureFuture(() {
+        inMicrotask(() => controllerErrorStream(trace).listen(null));
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces.first.toString(), equals(trace.toString()));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('controllerErrorStream'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('passed to a stream controller with no stack trace', () {
+      return captureFuture(() {
+        inMicrotask(() => controllerErrorStream().listen(null));
+      }).then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames,
+            contains(frameMember(startsWith('controllerErrorStream'))));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('and relays them to the parent zone', () {
+      var completer = Completer<void>();
+
+      runZonedGuarded(() {
+        Chain.capture(() {
+          inMicrotask(() => throw 'error');
+        }, onError: (error, chain) {
+          expect(error, equals('error'));
+          expect(chain.traces[1].frames,
+              contains(frameMember(startsWith('inMicrotask'))));
+          throw error;
+        });
+      }, (error, chain) {
+        try {
+          expect(error, equals('error'));
+          expect(
+              chain,
+              isA<Chain>().having((c) => c.traces[1].frames, 'traces[1].frames',
+                  contains(frameMember(startsWith('inMicrotask')))));
+          completer.complete();
+        } on Object catch (error, stackTrace) {
+          completer.completeError(error, stackTrace);
+        }
+      });
+
+      return completer.future;
+    });
+  });
+
+  test('capture() without onError passes exceptions to parent zone', () {
+    var completer = Completer<void>();
+
+    runZonedGuarded(() {
+      Chain.capture(() => inMicrotask(() => throw 'error'));
+    }, (error, chain) {
+      try {
+        expect(error, equals('error'));
+        expect(
+            chain,
+            isA<Chain>().having((c) => c.traces[1].frames, 'traces[1].frames',
+                contains(frameMember(startsWith('inMicrotask')))));
+        completer.complete();
+      } on Object catch (error, stackTrace) {
+        completer.completeError(error, stackTrace);
+      }
+    });
+
+    return completer.future;
+  });
+
+  group('current() within capture()', () {
+    test('called in a microtask', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inMicrotask(() => completer.complete(Chain.current()));
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('called in a one-shot timer', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inOneShotTimer(() => completer.complete(Chain.current()));
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+      });
+    });
+
+    test('called in a periodic timer', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inPeriodicTimer(() => completer.complete(Chain.current()));
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('called in a nested series of asynchronous operations', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inPeriodicTimer(() {
+          inOneShotTimer(() {
+            inMicrotask(() => completer.complete(Chain.current()));
+          });
+        });
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(4));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+        expect(chain.traces[3].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('called in a long future chain', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inFutureChain(() => completer.complete(Chain.current()));
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inFutureChain'))));
+      });
+    });
+  });
+
+  test(
+      'current() outside of capture() returns a chain wrapping the current '
+      'trace', () {
+    // The test runner runs all tests with chains enabled.
+    return Chain.disable(() {
+      var completer = Completer<Chain>();
+      inMicrotask(() => completer.complete(Chain.current()));
+
+      return completer.future.then((chain) {
+        // Since the chain wasn't loaded within [Chain.capture], the full stack
+        // chain isn't available and it just returns the current stack when
+        // called.
+        expect(chain.traces, hasLength(1));
+        expect(
+            chain.traces.first.frames.first, frameMember(startsWith('main')));
+      });
+    });
+  });
+
+  group('forTrace() within capture()', () {
+    test('called for a stack trace from a microtask', () {
+      return Chain.capture(() {
+        return chainForTrace(inMicrotask, () => throw 'error');
+      }).then((chain) {
+        // Because [chainForTrace] has to set up a future chain to capture the
+        // stack trace while still showing it to the zone specification, it adds
+        // an additional level of async nesting and so an additional trace.
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('called for a stack trace from a one-shot timer', () {
+      return Chain.capture(() {
+        return chainForTrace(inOneShotTimer, () => throw 'error');
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+      });
+    });
+
+    test('called for a stack trace from a periodic timer', () {
+      return Chain.capture(() {
+        return chainForTrace(inPeriodicTimer, () => throw 'error');
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test(
+        'called for a stack trace from a nested series of asynchronous '
+        'operations', () {
+      return Chain.capture(() {
+        return chainForTrace((callback) {
+          inPeriodicTimer(() => inOneShotTimer(() => inMicrotask(callback)));
+        }, () => throw 'error');
+      }).then((chain) {
+        expect(chain.traces, hasLength(5));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+        expect(chain.traces[3].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+        expect(chain.traces[4].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('called for a stack trace from a long future chain', () {
+      return Chain.capture(() {
+        return chainForTrace(inFutureChain, () => throw 'error');
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inFutureChain'))));
+      });
+    });
+
+    test('called for an unregistered stack trace uses the current chain',
+        () async {
+      late StackTrace trace;
+      var chain = await Chain.capture(() async {
+        try {
+          throw 'error';
+        } catch (_, stackTrace) {
+          trace = stackTrace;
+          return Chain.forTrace(stackTrace);
+        }
+      });
+
+      expect(chain.traces, hasLength(greaterThan(1)));
+
+      // Assert that we've trimmed the VM's stack chains here to avoid
+      // duplication.
+      expect(chain.traces.first.toString(),
+          equals(Chain.parse(trace.toString()).traces.first.toString()));
+    });
+  });
+
+  test(
+      'forTrace() outside of capture() returns a chain describing the VM stack '
+      'chain', () {
+    // Disable the test package's chain-tracking.
+    return Chain.disable(() async {
+      late StackTrace trace;
+      await Chain.capture(() async {
+        try {
+          throw 'error';
+        } catch (_, stackTrace) {
+          trace = stackTrace;
+        }
+      });
+
+      final chain = Chain.forTrace(trace);
+      final traceStr = trace.toString();
+      final gaps = vmChainGap.allMatches(traceStr);
+      // If the trace ends on a gap, there's no sub-trace following the gap.
+      final expectedLength =
+          (gaps.last.end == traceStr.length) ? gaps.length : gaps.length + 1;
+      expect(chain.traces, hasLength(expectedLength));
+      expect(
+          chain.traces.first.frames, contains(frameMember(startsWith('main'))));
+    });
+  });
+}
diff --git a/pkgs/stack_trace/test/frame_test.dart b/pkgs/stack_trace/test/frame_test.dart
new file mode 100644
index 0000000..a5dfc20
--- /dev/null
+++ b/pkgs/stack_trace/test/frame_test.dart
@@ -0,0 +1,729 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('.parseVM', () {
+    test('parses a stack frame with column correctly', () {
+      var frame = Frame.parseVM('#1      Foo._bar '
+          '(file:///home/nweiz/code/stuff.dart:42:21)');
+      expect(
+          frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+      expect(frame.line, equals(42));
+      expect(frame.column, equals(21));
+      expect(frame.member, equals('Foo._bar'));
+    });
+
+    test('parses a stack frame without column correctly', () {
+      var frame = Frame.parseVM('#1      Foo._bar '
+          '(file:///home/nweiz/code/stuff.dart:24)');
+      expect(
+          frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+      expect(frame.line, equals(24));
+      expect(frame.column, null);
+      expect(frame.member, equals('Foo._bar'));
+    });
+
+    // This can happen with async stack traces. See issue 22009.
+    test('parses a stack frame without line or column correctly', () {
+      var frame = Frame.parseVM('#1      Foo._bar '
+          '(file:///home/nweiz/code/stuff.dart)');
+      expect(
+          frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('Foo._bar'));
+    });
+
+    test('converts "<anonymous closure>" to "<fn>"', () {
+      String? parsedMember(String member) =>
+          Frame.parseVM('#0 $member (foo:0:0)').member;
+
+      expect(parsedMember('Foo.<anonymous closure>'), equals('Foo.<fn>'));
+      expect(parsedMember('<anonymous closure>.<anonymous closure>.bar'),
+          equals('<fn>.<fn>.bar'));
+    });
+
+    test('converts "<<anonymous closure>_async_body>" to "<async>"', () {
+      var frame =
+          Frame.parseVM('#0 Foo.<<anonymous closure>_async_body> (foo:0:0)');
+      expect(frame.member, equals('Foo.<async>'));
+    });
+
+    test('converts "<function_name_async_body>" to "<async>"', () {
+      var frame = Frame.parseVM('#0 Foo.<function_name_async_body> (foo:0:0)');
+      expect(frame.member, equals('Foo.<async>'));
+    });
+
+    test('parses a folded frame correctly', () {
+      var frame = Frame.parseVM('...');
+
+      expect(frame.member, equals('...'));
+      expect(frame.uri, equals(Uri()));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+    });
+  });
+
+  group('.parseV8', () {
+    test('returns an UnparsedFrame for malformed frames', () {
+      expectIsUnparsed(Frame.parseV8, '');
+      expectIsUnparsed(Frame.parseV8, '#1');
+      expectIsUnparsed(Frame.parseV8, '#1      Foo');
+      expectIsUnparsed(Frame.parseV8, '#1      (dart:async/future.dart:10:15)');
+      expectIsUnparsed(Frame.parseV8, 'Foo (dart:async/future.dart:10:15)');
+    });
+
+    test('parses a stack frame correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          '(https://example.com/stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a : in the authority', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          '(http://localhost:8080/stuff.dart.js:560:28)');
+      expect(
+          frame.uri, equals(Uri.parse('http://localhost:8080/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with an absolute POSIX path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          '(/path/to/stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('file:///path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with an absolute Windows path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          r'(C:\path\to\stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('file:///C:/path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a Windows UNC path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          r'(\\mount\path\to\stuff.dart.js:560:28)');
+      expect(
+          frame.uri, equals(Uri.parse('file://mount/path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a relative POSIX path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          '(path/to/stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a relative Windows path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          r'(path\to\stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses an anonymous stack frame correctly', () {
+      var frame =
+          Frame.parseV8('    at https://example.com/stuff.dart.js:560:28');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('<fn>'));
+    });
+
+    test('parses a native stack frame correctly', () {
+      var frame = Frame.parseV8('    at Object.stringify (native)');
+      expect(frame.uri, Uri.parse('native'));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('Object.stringify'));
+    });
+
+    test('parses a stack frame with [as ...] correctly', () {
+      // Ignore "[as ...]", since other stack trace formats don't support a
+      // similar construct.
+      var frame = Frame.parseV8('    at VW.call\$0 [as call\$4] '
+          '(https://example.com/stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a basic eval stack frame correctly', () {
+      var frame = Frame.parseV8('    at eval (eval at <anonymous> '
+          '(https://example.com/stuff.dart.js:560:28))');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('eval'));
+    });
+
+    test('parses an IE10 eval stack frame correctly', () {
+      var frame = Frame.parseV8('    at eval (eval at Anonymous function '
+          '(https://example.com/stuff.dart.js:560:28))');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('eval'));
+    });
+
+    test('parses an eval stack frame with inner position info correctly', () {
+      var frame = Frame.parseV8('    at eval (eval at <anonymous> '
+          '(https://example.com/stuff.dart.js:560:28), <anonymous>:3:28)');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('eval'));
+    });
+
+    test('parses a nested eval stack frame correctly', () {
+      var frame = Frame.parseV8('    at eval (eval at <anonymous> '
+          '(eval at sub (https://example.com/stuff.dart.js:560:28)))');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('eval'));
+    });
+
+    test('converts "<anonymous>" to "<fn>"', () {
+      String? parsedMember(String member) =>
+          Frame.parseV8('    at $member (foo:0:0)').member;
+
+      expect(parsedMember('Foo.<anonymous>'), equals('Foo.<fn>'));
+      expect(
+          parsedMember('<anonymous>.<anonymous>.bar'), equals('<fn>.<fn>.bar'));
+    });
+
+    test('returns an UnparsedFrame for malformed frames', () {
+      expectIsUnparsed(Frame.parseV8, '');
+      expectIsUnparsed(Frame.parseV8, '    at');
+      expectIsUnparsed(Frame.parseV8, '    at Foo');
+      expectIsUnparsed(Frame.parseV8, '    at Foo (dart:async/future.dart)');
+      expectIsUnparsed(Frame.parseV8, '    at (dart:async/future.dart:10:15)');
+      expectIsUnparsed(Frame.parseV8, 'Foo (dart:async/future.dart:10:15)');
+      expectIsUnparsed(Frame.parseV8, '    at dart:async/future.dart');
+      expectIsUnparsed(Frame.parseV8, 'dart:async/future.dart:10:15');
+    });
+  });
+
+  group('.parseFirefox/.parseSafari', () {
+    test('parses a Firefox stack trace with anonymous function', () {
+      var trace = Trace.parse('''
+Foo._bar@https://example.com/stuff.js:18056:12
+anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+baz@https://pub.dev/buz.js:56355:55
+        ''');
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[0].line, equals(18056));
+      expect(trace.frames[0].column, equals(12));
+      expect(trace.frames[0].member, equals('Foo._bar'));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].line, equals(693));
+      expect(trace.frames[1].column, isNull);
+      expect(trace.frames[1].member, equals('<fn>'));
+      expect(trace.frames[2].uri, equals(Uri.parse('https://pub.dev/buz.js')));
+      expect(trace.frames[2].line, equals(56355));
+      expect(trace.frames[2].column, equals(55));
+      expect(trace.frames[2].member, equals('baz'));
+    });
+
+    test('parses a Firefox stack trace with nested evals in anonymous function',
+        () {
+      var trace = Trace.parse('''
+        Foo._bar@https://example.com/stuff.js:18056:12
+        anonymous@file:///C:/example.html line 7 > eval line 1 > eval:1:1
+        anonymous@file:///C:/example.html line 45 > Function:1:1
+        ''');
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[0].line, equals(18056));
+      expect(trace.frames[0].column, equals(12));
+      expect(trace.frames[0].member, equals('Foo._bar'));
+      expect(trace.frames[1].uri, equals(Uri.parse('file:///C:/example.html')));
+      expect(trace.frames[1].line, equals(7));
+      expect(trace.frames[1].column, isNull);
+      expect(trace.frames[1].member, equals('<fn>'));
+      expect(trace.frames[2].uri, equals(Uri.parse('file:///C:/example.html')));
+      expect(trace.frames[2].line, equals(45));
+      expect(trace.frames[2].column, isNull);
+      expect(trace.frames[2].member, equals('<fn>'));
+    });
+
+    test('parses a simple stack frame correctly', () {
+      var frame = Frame.parseFirefox(
+          '.VW.call\$0@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with an absolute POSIX path correctly', () {
+      var frame = Frame.parseFirefox('.VW.call\$0@/path/to/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('file:///path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with an absolute Windows path correctly', () {
+      var frame =
+          Frame.parseFirefox(r'.VW.call$0@C:\path\to\stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('file:///C:/path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a Windows UNC path correctly', () {
+      var frame =
+          Frame.parseFirefox(r'.VW.call$0@\\mount\path\to\stuff.dart.js:560');
+      expect(
+          frame.uri, equals(Uri.parse('file://mount/path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a relative POSIX path correctly', () {
+      var frame = Frame.parseFirefox('.VW.call\$0@path/to/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a relative Windows path correctly', () {
+      var frame = Frame.parseFirefox(r'.VW.call$0@path\to\stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a simple anonymous stack frame correctly', () {
+      var frame = Frame.parseFirefox('@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('<fn>'));
+    });
+
+    test('parses a nested anonymous stack frame correctly', () {
+      var frame =
+          Frame.parseFirefox('.foo/<@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+
+      frame = Frame.parseFirefox('.foo/@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+    });
+
+    test('parses a named nested anonymous stack frame correctly', () {
+      var frame = Frame.parseFirefox(
+          '.foo/.name<@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+
+      frame = Frame.parseFirefox(
+          '.foo/.name@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+    });
+
+    test('parses a stack frame with parameters correctly', () {
+      var frame = Frame.parseFirefox(
+          '.foo(12, "@)()/<")@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo'));
+    });
+
+    test('parses a nested anonymous stack frame with parameters correctly', () {
+      var frame = Frame.parseFirefox(
+        '.foo(12, "@)()/<")/.fn<@https://example.com/stuff.dart.js:560',
+      );
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+    });
+
+    test(
+        'parses a deeply-nested anonymous stack frame with parameters '
+        'correctly', () {
+      var frame = Frame.parseFirefox('.convertDartClosureToJS/\$function</<@'
+          'https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('convertDartClosureToJS.<fn>.<fn>'));
+    });
+
+    test('returns an UnparsedFrame for malformed frames', () {
+      expectIsUnparsed(Frame.parseFirefox, '');
+      expectIsUnparsed(Frame.parseFirefox, '.foo');
+      expectIsUnparsed(Frame.parseFirefox, '.foo@dart:async/future.dart');
+      expectIsUnparsed(Frame.parseFirefox, '.foo(@dart:async/future.dart:10');
+      expectIsUnparsed(Frame.parseFirefox, '@dart:async/future.dart');
+    });
+
+    test('parses a simple stack frame correctly', () {
+      var frame =
+          Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:11');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('foo\$bar'));
+    });
+
+    test('parses an anonymous stack frame correctly', () {
+      var frame = Frame.parseFirefox('https://dart.dev/foo/bar.dart:10:11');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('<fn>'));
+    });
+
+    test('parses a stack frame with no line correctly', () {
+      var frame =
+          Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart::11');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, isNull);
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('foo\$bar'));
+    });
+
+    test('parses a stack frame with no column correctly', () {
+      var frame =
+          Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo\$bar'));
+    });
+
+    test('parses a stack frame with no line or column correctly', () {
+      var frame =
+          Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:11');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('foo\$bar'));
+    });
+  });
+
+  group('.parseFriendly', () {
+    test('parses a simple stack frame correctly', () {
+      var frame = Frame.parseFriendly(
+          'https://dart.dev/foo/bar.dart 10:11  Foo.<fn>.bar');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('Foo.<fn>.bar'));
+    });
+
+    test('parses a stack frame with no line or column correctly', () {
+      var frame =
+          Frame.parseFriendly('https://dart.dev/foo/bar.dart  Foo.<fn>.bar');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('Foo.<fn>.bar'));
+    });
+
+    test('parses a stack frame with no column correctly', () {
+      var frame =
+          Frame.parseFriendly('https://dart.dev/foo/bar.dart 10  Foo.<fn>.bar');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('Foo.<fn>.bar'));
+    });
+
+    test('parses a stack frame with a relative path correctly', () {
+      var frame = Frame.parseFriendly('foo/bar.dart 10:11    Foo.<fn>.bar');
+      expect(frame.uri,
+          equals(path.toUri(path.absolute(path.join('foo', 'bar.dart')))));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('Foo.<fn>.bar'));
+    });
+
+    test('returns an UnparsedFrame for malformed frames', () {
+      expectIsUnparsed(Frame.parseFriendly, '');
+      expectIsUnparsed(Frame.parseFriendly, 'foo/bar.dart');
+      expectIsUnparsed(Frame.parseFriendly, 'foo/bar.dart 10:11');
+    });
+
+    test('parses a data url stack frame with no line or column correctly', () {
+      var frame = Frame.parseFriendly('data:...  main');
+      expect(frame.uri.scheme, equals('data'));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('main'));
+    });
+
+    test('parses a data url stack frame correctly', () {
+      var frame = Frame.parseFriendly('data:... 10:11    main');
+      expect(frame.uri.scheme, equals('data'));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('main'));
+    });
+
+    test('parses a stack frame with spaces in the member name correctly', () {
+      var frame = Frame.parseFriendly(
+          'foo/bar.dart 10:11    (anonymous function).dart.fn');
+      expect(frame.uri,
+          equals(path.toUri(path.absolute(path.join('foo', 'bar.dart')))));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('(anonymous function).dart.fn'));
+    });
+
+    test(
+        'parses a stack frame with spaces in the member name and no line or '
+        'column correctly', () {
+      var frame = Frame.parseFriendly(
+          'https://dart.dev/foo/bar.dart  (anonymous function).dart.fn');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('(anonymous function).dart.fn'));
+    });
+  });
+
+  test('only considers dart URIs to be core', () {
+    bool isCore(String library) =>
+        Frame.parseVM('#0 Foo ($library:0:0)').isCore;
+
+    expect(isCore('dart:core'), isTrue);
+    expect(isCore('dart:async'), isTrue);
+    expect(isCore('dart:core/uri.dart'), isTrue);
+    expect(isCore('dart:async/future.dart'), isTrue);
+    expect(isCore('bart:core'), isFalse);
+    expect(isCore('sdart:core'), isFalse);
+    expect(isCore('darty:core'), isFalse);
+    expect(isCore('bart:core/uri.dart'), isFalse);
+  });
+
+  group('.library', () {
+    test('returns the URI string for non-file URIs', () {
+      expect(Frame.parseVM('#0 Foo (dart:async/future.dart:0:0)').library,
+          equals('dart:async/future.dart'));
+      expect(
+          Frame.parseVM('#0 Foo '
+                  '(https://dart.dev/stuff/thing.dart:0:0)')
+              .library,
+          equals('https://dart.dev/stuff/thing.dart'));
+    });
+
+    test('returns the relative path for file URIs', () {
+      expect(Frame.parseVM('#0 Foo (foo/bar.dart:0:0)').library,
+          equals(path.join('foo', 'bar.dart')));
+    });
+
+    test('truncates legacy data: URIs', () {
+      var frame = Frame.parseVM(
+          '#0 Foo (data:application/dart;charset=utf-8,blah:0:0)');
+      expect(frame.library, equals('data:...'));
+    });
+
+    test('truncates data: URIs', () {
+      var frame = Frame.parseVM(
+          '#0      main (<data:application/dart;charset=utf-8>:1:15)');
+      expect(frame.library, equals('data:...'));
+    });
+  });
+
+  group('.location', () {
+    test(
+        'returns the library and line/column numbers for non-core '
+        'libraries', () {
+      expect(
+          Frame.parseVM('#0 Foo '
+                  '(https://dart.dev/thing.dart:5:10)')
+              .location,
+          equals('https://dart.dev/thing.dart 5:10'));
+      expect(Frame.parseVM('#0 Foo (foo/bar.dart:1:2)').location,
+          equals('${path.join('foo', 'bar.dart')} 1:2'));
+    });
+  });
+
+  group('.package', () {
+    test('returns null for non-package URIs', () {
+      expect(
+          Frame.parseVM('#0 Foo (dart:async/future.dart:0:0)').package, isNull);
+      expect(
+          Frame.parseVM('#0 Foo '
+                  '(https://dart.dev/stuff/thing.dart:0:0)')
+              .package,
+          isNull);
+    });
+
+    test('returns the package name for package: URIs', () {
+      expect(Frame.parseVM('#0 Foo (package:foo/foo.dart:0:0)').package,
+          equals('foo'));
+      expect(Frame.parseVM('#0 Foo (package:foo/zap/bar.dart:0:0)').package,
+          equals('foo'));
+    });
+  });
+
+  group('.toString()', () {
+    test(
+        'returns the library and line/column numbers for non-core '
+        'libraries', () {
+      expect(
+          Frame.parseVM('#0 Foo (https://dart.dev/thing.dart:5:10)').toString(),
+          equals('https://dart.dev/thing.dart 5:10 in Foo'));
+    });
+
+    test('converts "<anonymous closure>" to "<fn>"', () {
+      expect(
+          Frame.parseVM('#0 Foo.<anonymous closure> '
+                  '(dart:core/uri.dart:5:10)')
+              .toString(),
+          equals('dart:core/uri.dart 5:10 in Foo.<fn>'));
+    });
+
+    test('prints a frame without a column correctly', () {
+      expect(Frame.parseVM('#0 Foo (dart:core/uri.dart:5)').toString(),
+          equals('dart:core/uri.dart 5 in Foo'));
+    });
+
+    test('prints relative paths as relative', () {
+      var relative = path.normalize('relative/path/to/foo.dart');
+      expect(Frame.parseFriendly('$relative 5:10  Foo').toString(),
+          equals('$relative 5:10 in Foo'));
+    });
+  });
+
+  test('parses a V8 Wasm frame with a name', () {
+    var frame = Frame.parseV8('    at Error._throwWithCurrentStackTrace '
+        '(wasm://wasm/0006d966:wasm-function[119]:0xbb13)');
+    expect(frame.uri, Uri.parse('wasm://wasm/0006d966'));
+    expect(frame.line, 1);
+    expect(frame.column, 0xbb13 + 1);
+    expect(frame.member, 'Error._throwWithCurrentStackTrace');
+  });
+
+  test('parses a V8 Wasm frame with a name with spaces', () {
+    var frame = Frame.parseV8('   at main tear-off trampoline '
+        '(wasm://wasm/0017fbea:wasm-function[863]:0x23cc8)');
+    expect(frame.uri, Uri.parse('wasm://wasm/0017fbea'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x23cc8 + 1);
+    expect(frame.member, 'main tear-off trampoline');
+  });
+
+  test('parses a V8 Wasm frame with a name with colons and parens', () {
+    var frame = Frame.parseV8('   at a::b::c() '
+        '(https://a.b.com/x/y/z.wasm:wasm-function[66334]:0x12c28ad)');
+    expect(frame.uri, Uri.parse('https://a.b.com/x/y/z.wasm'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x12c28ad + 1);
+    expect(frame.member, 'a::b::c()');
+  });
+
+  test('parses a V8 Wasm frame without a name', () {
+    var frame =
+        Frame.parseV8('    at wasm://wasm/0006d966:wasm-function[119]:0xbb13');
+    expect(frame.uri, Uri.parse('wasm://wasm/0006d966'));
+    expect(frame.line, 1);
+    expect(frame.column, 0xbb13 + 1);
+    expect(frame.member, '119');
+  });
+
+  test('parses a Firefox Wasm frame with a name', () {
+    var frame = Frame.parseFirefox(
+        'g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4');
+    expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x143b4 + 1);
+    expect(frame.member, 'g');
+  });
+
+  test('parses a Firefox Wasm frame with a name with spaces', () {
+    var frame = Frame.parseFirefox(
+        'main tear-off trampoline@http://localhost:8080/test.wasm:wasm-function[794]:0x14387');
+    expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x14387 + 1);
+    expect(frame.member, 'main tear-off trampoline');
+  });
+
+  test('parses a Firefox Wasm frame without a name', () {
+    var frame = Frame.parseFirefox(
+        '@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4');
+    expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x143b4 + 1);
+    expect(frame.member, '796');
+  });
+
+  test('parses a Safari Wasm frame with a name', () {
+    var frame = Frame.parseSafari('<?>.wasm-function[g]@[wasm code]');
+    expect(frame.uri, Uri.parse('wasm code'));
+    expect(frame.line, null);
+    expect(frame.column, null);
+    expect(frame.member, 'g');
+  });
+
+  test('parses a Safari Wasm frame with a name', () {
+    var frame = Frame.parseSafari(
+        '<?>.wasm-function[main tear-off trampoline]@[wasm code]');
+    expect(frame.uri, Uri.parse('wasm code'));
+    expect(frame.line, null);
+    expect(frame.column, null);
+    expect(frame.member, 'main tear-off trampoline');
+  });
+
+  test('parses a Safari Wasm frame without a name', () {
+    var frame = Frame.parseSafari('<?>.wasm-function[796]@[wasm code]');
+    expect(frame.uri, Uri.parse('wasm code'));
+    expect(frame.line, null);
+    expect(frame.column, null);
+    expect(frame.member, '796');
+  });
+}
+
+void expectIsUnparsed(Frame Function(String) constructor, String text) {
+  var frame = constructor(text);
+  expect(frame, isA<UnparsedFrame>());
+  expect(frame.toString(), equals(text));
+}
diff --git a/pkgs/stack_trace/test/trace_test.dart b/pkgs/stack_trace/test/trace_test.dart
new file mode 100644
index 0000000..e09de95
--- /dev/null
+++ b/pkgs/stack_trace/test/trace_test.dart
@@ -0,0 +1,615 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+  // This just shouldn't crash.
+  test('a native stack trace is parseable', Trace.current);
+
+  group('.parse', () {
+    test('.parse parses a V8 stack trace with eval statment correctly', () {
+      var trace = Trace.parse(r'''Error
+    at Object.eval (eval at Foo (main.dart.js:588), <anonymous>:3:47)''');
+      expect(trace.frames[0].uri, Uri.parse('main.dart.js'));
+      expect(trace.frames[0].member, equals('Object.eval'));
+      expect(trace.frames[0].line, equals(588));
+      expect(trace.frames[0].column, isNull);
+    });
+
+    test('.parse parses a VM stack trace correctly', () {
+      var trace = Trace.parse(
+        '#0      Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)\n'
+        '#1      zip.<anonymous closure>.zap (dart:async/future.dart:0:2)\n'
+        '#2      zip.<anonymous closure>.zap (https://pub.dev/thing.dart:1:100)',
+      );
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+      expect(trace.frames[1].uri, equals(Uri.parse('dart:async/future.dart')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.dart')));
+    });
+
+    test('parses a V8 stack trace correctly', () {
+      var trace = Trace.parse('Error\n'
+          '    at Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '    at https://example.com/stuff.js:0:2\n'
+          '    at zip.<anonymous>.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('Exception: foo\n'
+          '    at Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '    at https://example.com/stuff.js:0:2\n'
+          '    at zip.<anonymous>.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('Exception: foo\n'
+          '    bar\n'
+          '    at Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '    at https://example.com/stuff.js:0:2\n'
+          '    at zip.<anonymous>.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('Exception: foo\n'
+          '    bar\n'
+          '    at Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '    at https://example.com/stuff.js:0:2\n'
+          '    at (anonymous function).zip.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].member, equals('<fn>'));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+      expect(trace.frames[2].member, equals('<fn>.zip.zap'));
+    });
+
+    // JavaScriptCore traces are just like V8, except that it doesn't have a
+    // header and it starts with a tab rather than spaces.
+    test('parses a JavaScriptCore stack trace correctly', () {
+      var trace =
+          Trace.parse('\tat Foo._bar (https://example.com/stuff.js:42:21)\n'
+              '\tat https://example.com/stuff.js:0:2\n'
+              '\tat zip.<anonymous>.zap '
+              '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('\tat Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '\tat \n'
+          '\tat zip.<anonymous>.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[1].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+    });
+
+    test('parses a Firefox/Safari stack trace correctly', () {
+      var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('zip/<@https://example.com/stuff.js:0\n'
+          'Foo._bar@https://example.com/stuff.js:42\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'Foo._bar@https://example.com/stuff.js:42');
+
+      expect(
+          trace.frames[0].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[2].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+    });
+
+    test('parses a Firefox/Safari stack trace containing native code correctly',
+        () {
+      var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1\n'
+          '[native code]');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+      expect(trace.frames.length, equals(3));
+    });
+
+    test('parses a Firefox/Safari stack trace without a method name correctly',
+        () {
+      var trace = Trace.parse('https://example.com/stuff.js:42\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[0].member, equals('<fn>'));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+    });
+
+    test('parses a Firefox/Safari stack trace with an empty line correctly',
+        () {
+      var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+          '\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+    });
+
+    test('parses a Firefox/Safari stack trace with a column number correctly',
+        () {
+      var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42:2\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[0].line, equals(42));
+      expect(trace.frames[0].column, equals(2));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+    });
+
+    test('parses a package:stack_trace stack trace correctly', () {
+      var trace =
+          Trace.parse('https://dart.dev/foo/bar.dart 10:11  Foo.<fn>.bar\n'
+              'https://dart.dev/foo/baz.dart        Foo.<fn>.bar');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+    });
+
+    test('parses a package:stack_trace stack chain correctly', () {
+      var trace =
+          Trace.parse('https://dart.dev/foo/bar.dart 10:11  Foo.<fn>.bar\n'
+              'https://dart.dev/foo/baz.dart        Foo.<fn>.bar\n'
+              '===== asynchronous gap ===========================\n'
+              'https://dart.dev/foo/bang.dart 10:11  Foo.<fn>.bar\n'
+              'https://dart.dev/foo/quux.dart        Foo.<fn>.bar');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+      expect(trace.frames[2].uri,
+          equals(Uri.parse('https://dart.dev/foo/bang.dart')));
+      expect(trace.frames[3].uri,
+          equals(Uri.parse('https://dart.dev/foo/quux.dart')));
+    });
+
+    test('parses a package:stack_trace stack chain with end gap correctly', () {
+      var trace = Trace.parse(
+        'https://dart.dev/foo/bar.dart 10:11  Foo.<fn>.bar\n'
+        'https://dart.dev/foo/baz.dart        Foo.<fn>.bar\n'
+        'https://dart.dev/foo/bang.dart 10:11  Foo.<fn>.bar\n'
+        'https://dart.dev/foo/quux.dart        Foo.<fn>.bar===== asynchronous gap ===========================\n',
+      );
+
+      expect(trace.frames.length, 4);
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+      expect(trace.frames[2].uri,
+          equals(Uri.parse('https://dart.dev/foo/bang.dart')));
+      expect(trace.frames[3].uri,
+          equals(Uri.parse('https://dart.dev/foo/quux.dart')));
+    });
+
+    test('parses a real package:stack_trace stack trace correctly', () {
+      var traceString = Trace.current().toString();
+      expect(Trace.parse(traceString).toString(), equals(traceString));
+    });
+
+    test('parses an empty string correctly', () {
+      var trace = Trace.parse('');
+      expect(trace.frames, isEmpty);
+      expect(trace.toString(), equals(''));
+    });
+
+    test('parses trace with async gap correctly', () {
+      var trace = Trace.parse('#0      bop (file:///pull.dart:42:23)\n'
+          '<asynchronous suspension>\n'
+          '#1      twist (dart:the/future.dart:0:2)\n'
+          '#2      main (dart:my/file.dart:4:6)\n');
+
+      expect(trace.frames.length, 3);
+      expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+      expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+      expect(trace.frames[2].uri, equals(Uri.parse('dart:my/file.dart')));
+    });
+
+    test('parses trace with async gap at end correctly', () {
+      var trace = Trace.parse('#0      bop (file:///pull.dart:42:23)\n'
+          '#1      twist (dart:the/future.dart:0:2)\n'
+          '<asynchronous suspension>\n');
+
+      expect(trace.frames.length, 2);
+      expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+      expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+    });
+
+    test('parses a V8 stack frace with Wasm frames correctly', () {
+      var trace = Trace.parse(
+          '\tat Error._throwWithCurrentStackTrace (wasm://wasm/0006d892:wasm-function[119]:0xbaf8)\n'
+          '\tat main (wasm://wasm/0006d892:wasm-function[792]:0x14378)\n'
+          '\tat main tear-off trampoline (wasm://wasm/0006d892:wasm-function[794]:0x14387)\n'
+          '\tat _invokeMain (wasm://wasm/0006d892:wasm-function[70]:0xa56c)\n'
+          '\tat InstantiatedApp.invokeMain (/home/user/test.mjs:361:37)\n'
+          '\tat main (/home/user/run_wasm.js:416:21)\n'
+          '\tat async action (/home/user/run_wasm.js:353:38)\n'
+          '\tat async eventLoop (/home/user/run_wasm.js:329:9)');
+
+      expect(trace.frames.length, 8);
+
+      for (final frame in trace.frames) {
+        expect(frame is UnparsedFrame, false);
+      }
+
+      expect(trace.frames[0].uri, Uri.parse('wasm://wasm/0006d892'));
+      expect(trace.frames[0].line, 1);
+      expect(trace.frames[0].column, 0xbaf8 + 1);
+      expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+      expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+      expect(trace.frames[4].line, 361);
+      expect(trace.frames[4].column, 37);
+      expect(trace.frames[4].member, 'InstantiatedApp.invokeMain');
+
+      expect(trace.frames[5].uri, Uri.parse('file:///home/user/run_wasm.js'));
+      expect(trace.frames[5].line, 416);
+      expect(trace.frames[5].column, 21);
+      expect(trace.frames[5].member, 'main');
+    });
+
+    test('parses Firefox stack frace with Wasm frames correctly', () {
+      var trace = Trace.parse(
+          'Error._throwWithCurrentStackTrace@http://localhost:8080/test.wasm:wasm-function[119]:0xbaf8\n'
+          'main@http://localhost:8080/test.wasm:wasm-function[792]:0x14378\n'
+          'main tear-off trampoline@http://localhost:8080/test.wasm:wasm-function[794]:0x14387\n'
+          '_invokeMain@http://localhost:8080/test.wasm:wasm-function[70]:0xa56c\n'
+          'invoke@http://localhost:8080/test.mjs:48:26');
+
+      expect(trace.frames.length, 5);
+
+      for (final frame in trace.frames) {
+        expect(frame is UnparsedFrame, false);
+      }
+
+      expect(trace.frames[0].uri, Uri.parse('http://localhost:8080/test.wasm'));
+      expect(trace.frames[0].line, 1);
+      expect(trace.frames[0].column, 0xbaf8 + 1);
+      expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+      expect(trace.frames[4].uri, Uri.parse('http://localhost:8080/test.mjs'));
+      expect(trace.frames[4].line, 48);
+      expect(trace.frames[4].column, 26);
+      expect(trace.frames[4].member, 'invoke');
+    });
+
+    test('parses JSShell stack frace with Wasm frames correctly', () {
+      var trace = Trace.parse(
+          'Error._throwWithCurrentStackTrace@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[119]:0xbaf8\n'
+          'main@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378\n'
+          'main tear-off trampoline@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[794]:0x14387\n'
+          '_invokeMain@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[70]:0xa56c\n'
+          'invokeMain@/home/user/test.mjs:361:37\n'
+          'main@/home/user/run_wasm.js:416:21\n'
+          'async*action@/home/user/run_wasm.js:353:44\n'
+          'eventLoop@/home/user/run_wasm.js:329:15\n'
+          'self.dartMainRunner@/home/user/run_wasm.js:354:14\n'
+          '@/home/user/run_wasm.js:419:15');
+
+      expect(trace.frames.length, 10);
+
+      for (final frame in trace.frames) {
+        expect(frame is UnparsedFrame, false);
+      }
+
+      expect(trace.frames[0].uri, Uri.parse('file:///home/user/test.mjs'));
+      expect(trace.frames[0].line, 1);
+      expect(trace.frames[0].column, 0xbaf8 + 1);
+      expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+      expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+      expect(trace.frames[4].line, 361);
+      expect(trace.frames[4].column, 37);
+      expect(trace.frames[4].member, 'invokeMain');
+
+      expect(trace.frames[9].uri, Uri.parse('file:///home/user/run_wasm.js'));
+      expect(trace.frames[9].line, 419);
+      expect(trace.frames[9].column, 15);
+      expect(trace.frames[9].member, '<fn>');
+    });
+
+    test('parses Safari stack frace with Wasm frames correctly', () {
+      var trace = Trace.parse(
+          '<?>.wasm-function[Error._throwWithCurrentStackTrace]@[wasm code]\n'
+          '<?>.wasm-function[main]@[wasm code]\n'
+          '<?>.wasm-function[main tear-off trampoline]@[wasm code]\n'
+          '<?>.wasm-function[_invokeMain]@[wasm code]\n'
+          'invokeMain@/home/user/test.mjs:361:48\n'
+          '@/home/user/run_wasm.js:416:31');
+
+      expect(trace.frames.length, 6);
+
+      for (final frame in trace.frames) {
+        expect(frame is UnparsedFrame, false);
+      }
+
+      expect(trace.frames[0].uri, Uri.parse('wasm code'));
+      expect(trace.frames[0].line, null);
+      expect(trace.frames[0].column, null);
+      expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+      expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+      expect(trace.frames[4].line, 361);
+      expect(trace.frames[4].column, 48);
+      expect(trace.frames[4].member, 'invokeMain');
+
+      expect(trace.frames[5].uri, Uri.parse('file:///home/user/run_wasm.js'));
+      expect(trace.frames[5].line, 416);
+      expect(trace.frames[5].column, 31);
+      expect(trace.frames[5].member, '<fn>');
+    });
+  });
+
+  test('.toString() nicely formats the stack trace', () {
+    var trace = Trace.parse('''
+#0      Foo._bar (foo/bar.dart:42:21)
+#1      zip.<anonymous closure>.zap (dart:async/future.dart:0:2)
+#2      zip.<anonymous closure>.zap (https://pub.dev/thing.dart:1:100)
+''');
+
+    expect(trace.toString(), equals('''
+${path.join('foo', 'bar.dart')} 42:21                Foo._bar
+dart:async/future.dart 0:2        zip.<fn>.zap
+https://pub.dev/thing.dart 1:100  zip.<fn>.zap
+'''));
+  });
+
+  test('.vmTrace returns a native-style trace', () {
+    var uri = path.toUri(path.absolute('foo'));
+    var trace = Trace([
+      Frame(uri, 10, 20, 'Foo.<fn>'),
+      Frame(Uri.parse('https://dart.dev/foo.dart'), null, null, 'bar'),
+      Frame(Uri.parse('dart:async'), 15, null, 'baz'),
+    ]);
+
+    expect(
+        trace.vmTrace.toString(),
+        equals('#1      Foo.<anonymous closure> ($uri:10:20)\n'
+            '#2      bar (https://dart.dev/foo.dart:0:0)\n'
+            '#3      baz (dart:async:15:0)\n'));
+  });
+
+  group('folding', () {
+    group('.terse', () {
+      test('folds core frames together bottom-up', () {
+        var trace = Trace.parse('''
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+#0 notCore (foo.dart:42:21)
+#3 top (dart:io:5:10)
+#4 bottom (dart:async-patch/future.dart:9:11)
+#5 alsoNotCore (bar.dart:10:20)
+''');
+
+        expect(trace.terse.toString(), equals('''
+dart:core       bottom
+foo.dart 42:21  notCore
+dart:async      bottom
+bar.dart 10:20  alsoNotCore
+'''));
+      });
+
+      test('folds empty async frames', () {
+        var trace = Trace.parse('''
+#0 top (dart:async/future.dart:0:2)
+#1 empty.<<anonymous closure>_async_body> (bar.dart)
+#2 bottom (dart:async-patch/future.dart:9:11)
+#3 notCore (foo.dart:42:21)
+''');
+
+        expect(trace.terse.toString(), equals('''
+dart:async      bottom
+foo.dart 42:21  notCore
+'''));
+      });
+
+      test('removes the bottom-most async frame', () {
+        var trace = Trace.parse('''
+#0 notCore (foo.dart:42:21)
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+#3 top (dart:io:5:10)
+#4 bottom (dart:async-patch/future.dart:9:11)
+''');
+
+        expect(trace.terse.toString(), equals('''
+foo.dart 42:21  notCore
+'''));
+      });
+
+      test("won't make a trace empty", () {
+        var trace = Trace.parse('''
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+''');
+
+        expect(trace.terse.toString(), equals('''
+dart:core  bottom
+'''));
+      });
+
+      test("won't panic on an empty trace", () {
+        expect(Trace.parse('').terse.toString(), equals(''));
+      });
+    });
+
+    group('.foldFrames', () {
+      test('folds frames together bottom-up', () {
+        var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 fooBottom (foo.dart:1:100)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (dart:io/socket.dart:5:10)
+#5 fooBottom (dart:async-patch/future.dart:9:11)
+''');
+
+        var folded =
+            trace.foldFrames((frame) => frame.member!.startsWith('foo'));
+        expect(folded.toString(), equals('''
+foo.dart 42:21                     notFoo
+foo.dart 1:100                     fooBottom
+bar.dart 10:20                     alsoNotFoo
+dart:async-patch/future.dart 9:11  fooBottom
+'''));
+      });
+
+      test('will never fold unparsed frames', () {
+        var trace = Trace.parse(r'''
+.g"cs$#:b";a#>sw{*{ul$"$xqwr`p
+%+j-?uppx<([j@#nu{{>*+$%x-={`{
+!e($b{nj)zs?cgr%!;bmw.+$j+pfj~
+''');
+
+        expect(trace.foldFrames((frame) => true).toString(), equals(r'''
+.g"cs$#:b";a#>sw{*{ul$"$xqwr`p
+%+j-?uppx<([j@#nu{{>*+$%x-={`{
+!e($b{nj)zs?cgr%!;bmw.+$j+pfj~
+'''));
+      });
+
+      group('with terse: true', () {
+        test('folds core frames as well', () {
+          var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 coreBottom (dart:async/future.dart:0:2)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (foo.dart:9:11)
+#5 coreBottom (dart:async-patch/future.dart:9:11)
+''');
+
+          var folded = trace.foldFrames(
+              (frame) => frame.member!.startsWith('foo'),
+              terse: true);
+          expect(folded.toString(), equals('''
+foo.dart 42:21  notFoo
+dart:async      coreBottom
+bar.dart 10:20  alsoNotFoo
+'''));
+        });
+
+        test('shortens folded frames', () {
+          var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 fooBottom (package:foo/bar.dart:0:2)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (foo.dart:9:11)
+#5 fooBottom (foo/bar.dart:9:11)
+#6 againNotFoo (bar.dart:20:20)
+''');
+
+          var folded = trace.foldFrames(
+              (frame) => frame.member!.startsWith('foo'),
+              terse: true);
+          expect(folded.toString(), equals('''
+foo.dart 42:21  notFoo
+package:foo     fooBottom
+bar.dart 10:20  alsoNotFoo
+foo             fooBottom
+bar.dart 20:20  againNotFoo
+'''));
+        });
+
+        test('removes the bottom-most folded frame', () {
+          var trace = Trace.parse('''
+#2 fooTop (package:foo/bar.dart:0:2)
+#3 notFoo (bar.dart:10:20)
+#5 fooBottom (foo/bar.dart:9:11)
+''');
+
+          var folded = trace.foldFrames(
+              (frame) => frame.member!.startsWith('foo'),
+              terse: true);
+          expect(folded.toString(), equals('''
+package:foo     fooTop
+bar.dart 10:20  notFoo
+'''));
+        });
+      });
+    });
+  });
+}
diff --git a/pkgs/stack_trace/test/utils.dart b/pkgs/stack_trace/test/utils.dart
new file mode 100644
index 0000000..98cb5ed
--- /dev/null
+++ b/pkgs/stack_trace/test/utils.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+/// Returns a matcher that runs [matcher] against a [Frame]'s `member` field.
+Matcher frameMember(Object? matcher) =>
+    isA<Frame>().having((p0) => p0.member, 'member', matcher);
+
+/// Returns a matcher that runs [matcher] against a [Frame]'s `library` field.
+Matcher frameLibrary(Object? matcher) =>
+    isA<Frame>().having((p0) => p0.library, 'library', matcher);
diff --git a/pkgs/stack_trace/test/vm_test.dart b/pkgs/stack_trace/test/vm_test.dart
new file mode 100644
index 0000000..70ac014
--- /dev/null
+++ b/pkgs/stack_trace/test/vm_test.dart
@@ -0,0 +1,112 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// This file tests stack_trace's ability to parse live stack traces. It's a
+/// dual of dartium_test.dart, since method names can differ somewhat from
+/// platform to platform. No similar file exists for dart2js since the specific
+/// method names there are implementation details.
+@TestOn('vm')
+library;
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+// The name of this (trivial) function is verified as part of the test
+String getStackTraceString() => StackTrace.current.toString();
+
+// The name of this (trivial) function is verified as part of the test
+StackTrace getStackTraceObject() => StackTrace.current;
+
+Frame getCaller([int? level]) {
+  if (level == null) return Frame.caller();
+  return Frame.caller(level);
+}
+
+Frame nestedGetCaller(int level) => getCaller(level);
+
+Trace getCurrentTrace([int level = 0]) => Trace.current(level);
+
+Trace nestedGetCurrentTrace(int level) => getCurrentTrace(level);
+
+void main() {
+  group('Trace', () {
+    test('.parse parses a real stack trace correctly', () {
+      var string = getStackTraceString();
+      var trace = Trace.parse(string);
+      expect(path.url.basename(trace.frames.first.uri.path),
+          equals('vm_test.dart'));
+      expect(trace.frames.first.member, equals('getStackTraceString'));
+    });
+
+    test('converts from a native stack trace correctly', () {
+      var trace = Trace.from(getStackTraceObject());
+      expect(path.url.basename(trace.frames.first.uri.path),
+          equals('vm_test.dart'));
+      expect(trace.frames.first.member, equals('getStackTraceObject'));
+    });
+
+    test('.from handles a stack overflow trace correctly', () {
+      void overflow() => overflow();
+
+      late Trace? trace;
+      try {
+        overflow();
+      } catch (_, stackTrace) {
+        trace = Trace.from(stackTrace);
+      }
+
+      expect(trace!.frames.first.member, equals('main.<fn>.<fn>.overflow'));
+    });
+
+    group('.current()', () {
+      test('with no argument returns a trace starting at the current frame',
+          () {
+        var trace = Trace.current();
+        expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+      });
+
+      test('at level 0 returns a trace starting at the current frame', () {
+        var trace = Trace.current();
+        expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+      });
+
+      test('at level 1 returns a trace starting at the parent frame', () {
+        var trace = getCurrentTrace(1);
+        expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+      });
+
+      test('at level 2 returns a trace starting at the grandparent frame', () {
+        var trace = nestedGetCurrentTrace(2);
+        expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+      });
+
+      test('throws an ArgumentError for negative levels', () {
+        expect(() => Trace.current(-1), throwsArgumentError);
+      });
+    });
+  });
+
+  group('Frame.caller()', () {
+    test('with no argument returns the parent frame', () {
+      expect(getCaller().member, equals('main.<fn>.<fn>'));
+    });
+
+    test('at level 0 returns the current frame', () {
+      expect(getCaller(0).member, equals('getCaller'));
+    });
+
+    test('at level 1 returns the current frame', () {
+      expect(getCaller(1).member, equals('main.<fn>.<fn>'));
+    });
+
+    test('at level 2 returns the grandparent frame', () {
+      expect(nestedGetCaller(2).member, equals('main.<fn>.<fn>'));
+    });
+
+    test('throws an ArgumentError for negative levels', () {
+      expect(() => Frame.caller(-1), throwsArgumentError);
+    });
+  });
+}
diff --git a/pkgs/stream_channel/.gitignore b/pkgs/stream_channel/.gitignore
new file mode 100644
index 0000000..1447012
--- /dev/null
+++ b/pkgs/stream_channel/.gitignore
@@ -0,0 +1,10 @@
+.buildlog
+.dart_tool/
+.DS_Store
+.idea
+.pub/
+.settings/
+build/
+packages
+.packages
+pubspec.lock
diff --git a/pkgs/stream_channel/AUTHORS b/pkgs/stream_channel/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/stream_channel/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/stream_channel/CHANGELOG.md b/pkgs/stream_channel/CHANGELOG.md
new file mode 100644
index 0000000..9dd3990
--- /dev/null
+++ b/pkgs/stream_channel/CHANGELOG.md
@@ -0,0 +1,162 @@
+## 2.1.4
+
+* Fix `StreamChannelMixin` so that it can be used as a mixin again.
+
+## 2.1.3
+
+* Require Dart 3.3
+* Move to `dart-lang/tools` monorepo.
+
+## 2.1.2
+
+* Require Dart 2.19
+* Add an example.
+* Fix a race condition in `IsolateChannel.connectReceive()` where the channel
+  could hang forever if its sink was closed before the connection was established.
+
+## 2.1.1
+
+* Require Dart 2.14
+* Populate the pubspec `repository` field.
+* Handle multichannel messages where the ID element is a `double` at runtime
+  instead of an `int`. When reading an array with `dart2wasm` numbers within the
+  array are parsed as `double`.
+
+## 2.1.0
+
+* Stable release for null safety.
+
+## 2.0.0
+
+**Breaking changes**
+
+* `IsolateChannel` requires a separate import
+  `package:stram_channel/isolate_channel.dart`.
+  `package:stream_channel/stream_channel.dart` will now not trigger any platform
+  concerns due to importing `dart:isolate`.
+* Remove `JsonDocumentTransformer` class. The `jsonDocument` top level is still
+  available.
+* Remove `StreamChannelTransformer.typed`. Use `.cast` on the transformed
+  channel instead.
+* Change `Future<dynamic>` returns to `Future<void>`.
+
+## 1.7.0
+
+* Make `IsolateChannel` available through
+  `package:stream_channel/isolate_channel.dart`. This will be the required
+  import in the next release.
+* Require `2.0.0` or newer SDK.
+* Internal style changes.
+
+## 1.6.8
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.6.7+1
+
+* Fix Dart 2 runtime types in `IsolateChannel`.
+
+## 1.6.7
+
+* Update SDK version to 2.0.0-dev.17.0.
+* Add a type argument to `MultiChannel`.
+
+## 1.6.6
+
+* Fix a Dart 2 issue with inner stream transformation in `GuaranteeChannel`.
+
+* Fix a Dart 2 issue with `StreamChannelTransformer.fromCodec()`.
+
+## 1.6.5
+
+* Fix an issue with `JsonDocumentTransformer.bind` where it created an internal
+  stream channel which didn't get a properly inferred type for its `sink`.
+
+## 1.6.4
+
+* Fix a race condition in `MultiChannel` where messages from a remote virtual
+  channel could get dropped if the corresponding local channel wasn't registered
+  quickly enough.
+
+## 1.6.3
+
+* Use `pumpEventQueue()` from test.
+
+## 1.6.2
+
+* Declare support for `async` 2.0.0.
+
+## 1.6.1
+
+* Fix the type of `StreamChannel.transform()`. This previously inverted the
+  generic parameters, so it only really worked with transformers where both
+  generic types were identical.
+
+## 1.6.0
+
+* `Disconnector.disconnect()` now returns a future that completes when all the
+  inner `StreamSink.close()` futures have completed.
+
+## 1.5.0
+
+* Add `new StreamChannel.withCloseGuarantee()` to provide the specific guarantee
+  that closing the sink causes the stream to close before it emits any more
+  events. This is the only guarantee that isn't automatically preserved when
+  transforming a channel.
+
+* `StreamChannelTransformer`s provided by the `stream_channel` package now
+  properly provide the guarantee that closing the sink causes the stream to
+  close before it emits any more events
+
+## 1.4.0
+
+* Add `StreamChannel.cast()`, which soundly coerces the generic type of a
+  channel.
+
+* Add `StreamChannelTransformer.typed()`, which soundly coerces the generic type
+  of a transformer.
+
+## 1.3.2
+
+* Fix all strong-mode errors and warnings.
+
+## 1.3.1
+
+* Make `IsolateChannel` slightly more efficient.
+
+* Make `MultiChannel` follow the stream channel rules.
+
+## 1.3.0
+
+* Add `Disconnector`, a transformer that allows the caller to disconnect the
+  transformed channel.
+
+## 1.2.0
+
+* Add `new StreamChannel.withGuarantees()`, which creates a channel with extra
+  wrapping to ensure that it obeys the stream channel guarantees.
+
+* Add `StreamChannelController`, which can be used to create custom
+  `StreamChannel` objects.
+
+## 1.1.1
+
+* Fix the type annotation for `StreamChannel.transform()`'s parameter.
+
+## 1.1.0
+
+* Add `StreamChannel.transformStream()`, `StreamChannel.transformSink()`,
+  `StreamChannel.changeStream()`, and `StreamChannel.changeSink()` to support
+  changing only the stream or only the sink of a channel.
+
+* Be more explicit about `JsonDocumentTransformer`'s error-handling behavior.
+
+## 1.0.1
+
+* Fix `MultiChannel`'s constructor to take a `StreamChannel`. This is
+  technically a breaking change, but since 1.0.0 was only released an hour ago,
+  we're treating it as a bug fix.
+
+## 1.0.0
+
+* Initial version
diff --git a/pkgs/stream_channel/LICENSE b/pkgs/stream_channel/LICENSE
new file mode 100644
index 0000000..dbd2843
--- /dev/null
+++ b/pkgs/stream_channel/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2015, 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/stream_channel/README.md b/pkgs/stream_channel/README.md
new file mode 100644
index 0000000..3677ccf
--- /dev/null
+++ b/pkgs/stream_channel/README.md
@@ -0,0 +1,20 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/stream_channel.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/stream_channel.yaml)
+[![pub package](https://img.shields.io/pub/v/stream_channel.svg)](https://pub.dev/packages/stream_channel)
+[![package publisher](https://img.shields.io/pub/publisher/stream_channel.svg)](https://pub.dev/packages/stream_channel/publisher)
+
+This package exposes the `StreamChannel` interface, which represents a two-way
+communication channel. Each `StreamChannel` exposes a `Stream` for receiving
+data and a `StreamSink` for sending it. 
+
+`StreamChannel` helps abstract communication logic away from the underlying
+protocol. For example, the [`test`][test] package re-uses its test suite
+communication protocol for both WebSocket connections to browser suites and
+Isolate connections to VM tests.
+
+[test]: https://pub.dev/packages/test
+
+This package also contains utilities for dealing with `StreamChannel`s and with
+two-way communications in general. For documentation of these utilities, see
+[the API docs][api].
+
+[api]: https://pub.dev/documentation/stream_channel/latest/
diff --git a/pkgs/stream_channel/analysis_options.yaml b/pkgs/stream_channel/analysis_options.yaml
new file mode 100644
index 0000000..44cda4d
--- /dev/null
+++ b/pkgs/stream_channel/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/stream_channel/example/example.dart b/pkgs/stream_channel/example/example.dart
new file mode 100644
index 0000000..b41d8d9
--- /dev/null
+++ b/pkgs/stream_channel/example/example.dart
@@ -0,0 +1,110 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:stream_channel/isolate_channel.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+Future<void> main() async {
+  // A StreamChannel<T>, is in simplest terms, a wrapper around a Stream<T> and
+  // a StreamSink<T>. For example, you can create a channel that wraps standard
+  // IO:
+  var stdioChannel = StreamChannel(stdin, stdout);
+  stdioChannel.sink.add('Hello!\n'.codeUnits);
+
+  // Like a Stream<T> can be transformed with a StreamTransformer<T>, a
+  // StreamChannel<T> can be transformed with a StreamChannelTransformer<T>.
+  // For example, we can handle standard input as strings:
+  var stringChannel = stdioChannel
+      .transform(StreamChannelTransformer.fromCodec(utf8))
+      .transformStream(const LineSplitter());
+  stringChannel.sink.add('world!\n');
+
+  // You can implement StreamChannel<T> by extending StreamChannelMixin<T>, but
+  // it's much easier to use a StreamChannelController<T>. A controller has two
+  // StreamChannel<T> members: `local` and `foreign`. The creator of a
+  // controller should work with the `local` channel, while the recipient should
+  // work with the `foreign` channel, and usually will not have direct access to
+  // the underlying controller.
+  var ctrl = StreamChannelController<String>();
+  ctrl.local.stream.listen((event) {
+    // Do something useful here...
+  });
+
+  // You can also pipe events from one channel to another.
+  ctrl
+    ..foreign.pipe(stringChannel)
+    ..local.sink.add('Piped!\n');
+  await ctrl.local.sink.close();
+
+  // The StreamChannel<T> interface provides several guarantees, which can be
+  // found here:
+  // https://pub.dev/documentation/stream_channel/latest/stream_channel/StreamChannel-class.html
+  //
+  // By calling `StreamChannel<T>.withGuarantees()`, you can create a
+  // StreamChannel<T> that provides all guarantees.
+  var dummyCtrl0 = StreamChannelController<String>();
+  var guaranteedChannel = StreamChannel.withGuarantees(
+      dummyCtrl0.foreign.stream, dummyCtrl0.foreign.sink);
+
+  // To close a StreamChannel, use `sink.close()`.
+  await guaranteedChannel.sink.close();
+
+  // A MultiChannel<T> multiplexes multiple virtual channels across a single
+  // underlying transport layer. For example, an application listening over
+  // standard I/O can still support multiple clients if it has a mechanism to
+  // separate events from different clients.
+  //
+  // A MultiChannel<T> splits events into numbered channels, which are
+  // instances of VirtualChannel<T>.
+  var dummyCtrl1 = StreamChannelController<String>();
+  var multiChannel = MultiChannel<String>(dummyCtrl1.foreign);
+  var channel1 = multiChannel.virtualChannel();
+  await multiChannel.sink.close();
+
+  // The client/peer should also create its own MultiChannel<T>, connected to
+  // the underlying transport, use the corresponding ID's to handle events in
+  // their respective channels. It is up to you how to communicate channel ID's
+  // across different endpoints.
+  var dummyCtrl2 = StreamChannelController<String>();
+  var multiChannel2 = MultiChannel<String>(dummyCtrl2.foreign);
+  var channel2 = multiChannel2.virtualChannel(channel1.id);
+  await channel2.sink.close();
+  await multiChannel2.sink.close();
+
+  // Multiple instances of a Dart application can communicate easily across
+  // `SendPort`/`ReceivePort` pairs by means of the `IsolateChannel<T>` class.
+  // Typically, one endpoint will create a `ReceivePort`, and call the
+  // `IsolateChannel.connectReceive` constructor. The other endpoint will be
+  // given the corresponding `SendPort`, and then call
+  // `IsolateChannel.connectSend`.
+  var recv = ReceivePort();
+  var recvChannel = IsolateChannel<void>.connectReceive(recv);
+  var sendChannel = IsolateChannel<void>.connectSend(recv.sendPort);
+
+  // You must manually close `IsolateChannel<T>` sinks, however.
+  await recvChannel.sink.close();
+  await sendChannel.sink.close();
+
+  // You can use the `Disconnector` transformer to cause a channel to act as
+  // though the remote end of its transport had disconnected.
+  var disconnector = Disconnector<String>();
+  var disconnectable = stringChannel.transform(disconnector);
+  disconnectable.sink.add('Still connected!');
+  await disconnector.disconnect();
+
+  // Additionally:
+  //   * The `DelegatingStreamController<T>` class can be extended to build a
+  //     basis for wrapping other `StreamChannel<T>` objects.
+  //   * The `jsonDocument` transformer converts events to/from JSON, using
+  //     the `json` codec from `dart:convert`.
+  //   * `package:json_rpc_2` directly builds on top of
+  //     `package:stream_channel`, so any compatible transport can be used to
+  //      create interactive client/server or peer-to-peer applications (i.e.
+  //      language servers, microservices, etc.
+}
diff --git a/pkgs/stream_channel/lib/isolate_channel.dart b/pkgs/stream_channel/lib/isolate_channel.dart
new file mode 100644
index 0000000..5d9f6e1
--- /dev/null
+++ b/pkgs/stream_channel/lib/isolate_channel.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'src/isolate_channel.dart' show IsolateChannel;
diff --git a/pkgs/stream_channel/lib/src/close_guarantee_channel.dart b/pkgs/stream_channel/lib/src/close_guarantee_channel.dart
new file mode 100644
index 0000000..13432d1
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/close_guarantee_channel.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// A [StreamChannel] that specifically enforces the stream channel guarantee
+/// that closing the sink causes the stream to close before it emits any more
+/// events
+///
+/// This is exposed via [StreamChannel.withCloseGuarantee].
+class CloseGuaranteeChannel<T> extends StreamChannelMixin<T> {
+  @override
+  Stream<T> get stream => _stream;
+  late final _CloseGuaranteeStream<T> _stream;
+
+  @override
+  StreamSink<T> get sink => _sink;
+  late final _CloseGuaranteeSink<T> _sink;
+
+  /// The subscription to the inner stream.
+  StreamSubscription<T>? _subscription;
+
+  /// Whether the sink has closed, causing the underlying channel to disconnect.
+  bool _disconnected = false;
+
+  CloseGuaranteeChannel(Stream<T> innerStream, StreamSink<T> innerSink) {
+    _sink = _CloseGuaranteeSink<T>(innerSink, this);
+    _stream = _CloseGuaranteeStream<T>(innerStream, this);
+  }
+}
+
+/// The stream for [CloseGuaranteeChannel].
+///
+/// This wraps the inner stream to save the subscription on the channel when
+/// [listen] is called.
+class _CloseGuaranteeStream<T> extends Stream<T> {
+  /// The inner stream this is delegating to.
+  final Stream<T> _inner;
+
+  /// The [CloseGuaranteeChannel] this belongs to.
+  final CloseGuaranteeChannel<T> _channel;
+
+  _CloseGuaranteeStream(this._inner, this._channel);
+
+  @override
+  StreamSubscription<T> listen(void Function(T)? onData,
+      {Function? onError, void Function()? onDone, bool? cancelOnError}) {
+    // If the channel is already disconnected, we shouldn't dispatch anything
+    // but a done event.
+    if (_channel._disconnected) {
+      onData = null;
+      onError = null;
+    }
+
+    var subscription = _inner.listen(onData,
+        onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+    if (!_channel._disconnected) {
+      _channel._subscription = subscription;
+    }
+    return subscription;
+  }
+}
+
+/// The sink for [CloseGuaranteeChannel].
+///
+/// This wraps the inner sink to cancel the stream subscription when the sink is
+/// canceled.
+class _CloseGuaranteeSink<T> extends DelegatingStreamSink<T> {
+  /// The [CloseGuaranteeChannel] this belongs to.
+  final CloseGuaranteeChannel<T> _channel;
+
+  _CloseGuaranteeSink(super.inner, this._channel);
+
+  @override
+  Future<void> close() {
+    var done = super.close();
+    _channel._disconnected = true;
+    var subscription = _channel._subscription;
+    if (subscription != null) {
+      // Don't dispatch anything but a done event.
+      subscription.onData(null);
+      subscription.onError(null);
+    }
+    return done;
+  }
+}
diff --git a/pkgs/stream_channel/lib/src/delegating_stream_channel.dart b/pkgs/stream_channel/lib/src/delegating_stream_channel.dart
new file mode 100644
index 0000000..4484a59
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/delegating_stream_channel.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../stream_channel.dart';
+
+/// A simple delegating wrapper around [StreamChannel].
+///
+/// Subclasses can override individual methods, or use this to expose only
+/// [StreamChannel] methods.
+class DelegatingStreamChannel<T> extends StreamChannelMixin<T> {
+  /// The inner channel to which methods are forwarded.
+  final StreamChannel<T> _inner;
+
+  @override
+  Stream<T> get stream => _inner.stream;
+  @override
+  StreamSink<T> get sink => _inner.sink;
+
+  DelegatingStreamChannel(this._inner);
+}
diff --git a/pkgs/stream_channel/lib/src/disconnector.dart b/pkgs/stream_channel/lib/src/disconnector.dart
new file mode 100644
index 0000000..3414e9c
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/disconnector.dart
@@ -0,0 +1,153 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// Allows the caller to force a channel to disconnect.
+///
+/// When [disconnect] is called, the channel (or channels) transformed by this
+/// transformer will act as though the remote end had disconnected—the stream
+/// will emit a done event, and the sink will ignore future inputs. The inner
+/// sink will also be closed to notify the remote end of the disconnection.
+///
+/// If a channel is transformed after the [disconnect] has been called, it will
+/// be disconnected immediately.
+class Disconnector<T> implements StreamChannelTransformer<T, T> {
+  /// Whether [disconnect] has been called.
+  bool get isDisconnected => _disconnectMemo.hasRun;
+
+  /// The sinks for transformed channels.
+  ///
+  /// Note that we assume that transformed channels provide the stream channel
+  /// guarantees. This allows us to only track sinks, because we know closing
+  /// the underlying sink will cause the stream to emit a done event.
+  final _sinks = <_DisconnectorSink<T>>[];
+
+  /// Disconnects all channels that have been transformed.
+  ///
+  /// Returns a future that completes when all inner sinks' [StreamSink.close]
+  /// futures have completed. Note that a [StreamController]'s sink won't close
+  /// until the corresponding stream has a listener.
+  Future<void> disconnect() => _disconnectMemo.runOnce(() {
+        var futures = _sinks.map((sink) => sink._disconnect()).toList();
+        _sinks.clear();
+        return Future.wait(futures, eagerError: true);
+      });
+  final _disconnectMemo = AsyncMemoizer<List<void>>();
+
+  @override
+  StreamChannel<T> bind(StreamChannel<T> channel) {
+    return channel.changeSink((innerSink) {
+      var sink = _DisconnectorSink<T>(innerSink);
+
+      if (isDisconnected) {
+        // Ignore errors here, because otherwise there would be no way for the
+        // user to handle them gracefully.
+        sink._disconnect().catchError((_) {});
+      } else {
+        _sinks.add(sink);
+      }
+
+      return sink;
+    });
+  }
+}
+
+/// A sink wrapper that can force a disconnection.
+class _DisconnectorSink<T> implements StreamSink<T> {
+  /// The inner sink.
+  final StreamSink<T> _inner;
+
+  @override
+  Future<void> get done => _inner.done;
+
+  /// Whether [Disconnector.disconnect] has been called.
+  var _isDisconnected = false;
+
+  /// Whether the user has called [close].
+  var _closed = false;
+
+  /// The subscription to the stream passed to [addStream], if a stream is
+  /// currently being added.
+  StreamSubscription<T>? _addStreamSubscription;
+
+  /// The completer for the future returned by [addStream], if a stream is
+  /// currently being added.
+  Completer? _addStreamCompleter;
+
+  /// Whether we're currently adding a stream with [addStream].
+  bool get _inAddStream => _addStreamSubscription != null;
+
+  _DisconnectorSink(this._inner);
+
+  @override
+  void add(T data) {
+    if (_closed) throw StateError('Cannot add event after closing.');
+    if (_inAddStream) {
+      throw StateError('Cannot add event while adding stream.');
+    }
+    if (_isDisconnected) return;
+
+    _inner.add(data);
+  }
+
+  @override
+  void addError(Object error, [StackTrace? stackTrace]) {
+    if (_closed) throw StateError('Cannot add event after closing.');
+    if (_inAddStream) {
+      throw StateError('Cannot add event while adding stream.');
+    }
+    if (_isDisconnected) return;
+
+    _inner.addError(error, stackTrace);
+  }
+
+  @override
+  Future<void> addStream(Stream<T> stream) {
+    if (_closed) throw StateError('Cannot add stream after closing.');
+    if (_inAddStream) {
+      throw StateError('Cannot add stream while adding stream.');
+    }
+    if (_isDisconnected) return Future.value();
+
+    _addStreamCompleter = Completer.sync();
+    _addStreamSubscription = stream.listen(_inner.add,
+        onError: _inner.addError, onDone: _addStreamCompleter!.complete);
+    return _addStreamCompleter!.future.then((_) {
+      _addStreamCompleter = null;
+      _addStreamSubscription = null;
+    });
+  }
+
+  @override
+  Future<void> close() {
+    if (_inAddStream) {
+      throw StateError('Cannot close sink while adding stream.');
+    }
+
+    _closed = true;
+    return _inner.close();
+  }
+
+  /// Disconnects this sink.
+  ///
+  /// This closes the underlying sink and stops forwarding events. It returns
+  /// the [StreamSink.close] future for the underlying sink.
+  Future<void> _disconnect() {
+    _isDisconnected = true;
+    var future = _inner.close();
+
+    if (_inAddStream) {
+      _addStreamCompleter!.complete(_addStreamSubscription!.cancel());
+      _addStreamCompleter = null;
+      _addStreamSubscription = null;
+    }
+
+    return future;
+  }
+}
diff --git a/pkgs/stream_channel/lib/src/guarantee_channel.dart b/pkgs/stream_channel/lib/src/guarantee_channel.dart
new file mode 100644
index 0000000..30ebe2e
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/guarantee_channel.dart
@@ -0,0 +1,207 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// A [StreamChannel] that enforces the stream channel guarantees.
+///
+/// This is exposed via [StreamChannel.withGuarantees].
+class GuaranteeChannel<T> extends StreamChannelMixin<T> {
+  @override
+  Stream<T> get stream => _streamController.stream;
+
+  @override
+  StreamSink<T> get sink => _sink;
+  late final _GuaranteeSink<T> _sink;
+
+  /// The controller for [stream].
+  ///
+  /// This intermediate controller allows us to continue listening for a done
+  /// event even after the user has canceled their subscription, and to send our
+  /// own done event when the sink is closed.
+  late final StreamController<T> _streamController;
+
+  /// The subscription to the inner stream.
+  StreamSubscription<T>? _subscription;
+
+  /// Whether the sink has closed, causing the underlying channel to disconnect.
+  bool _disconnected = false;
+
+  GuaranteeChannel(Stream<T> innerStream, StreamSink<T> innerSink,
+      {bool allowSinkErrors = true}) {
+    _sink = _GuaranteeSink<T>(innerSink, this, allowErrors: allowSinkErrors);
+
+    // Enforce the single-subscription guarantee by changing a broadcast stream
+    // to single-subscription.
+    if (innerStream.isBroadcast) {
+      innerStream =
+          innerStream.transform(SingleSubscriptionTransformer<T, T>());
+    }
+
+    _streamController = StreamController<T>(
+        onListen: () {
+          // If the sink has disconnected, we've already called
+          // [_streamController.close].
+          if (_disconnected) return;
+
+          _subscription = innerStream.listen(_streamController.add,
+              onError: _streamController.addError, onDone: () {
+            _sink._onStreamDisconnected();
+            _streamController.close();
+          });
+        },
+        sync: true);
+  }
+
+  /// Called by [_GuaranteeSink] when the user closes it.
+  ///
+  /// The sink closing indicates that the connection is closed, so the stream
+  /// should stop emitting events.
+  void _onSinkDisconnected() {
+    _disconnected = true;
+    var subscription = _subscription;
+    if (subscription != null) subscription.cancel();
+    _streamController.close();
+  }
+}
+
+/// The sink for [GuaranteeChannel].
+///
+/// This wraps the inner sink to ignore events and cancel any in-progress
+/// [addStream] calls when the underlying channel closes.
+class _GuaranteeSink<T> implements StreamSink<T> {
+  /// The inner sink being wrapped.
+  final StreamSink<T> _inner;
+
+  /// The [GuaranteeChannel] this belongs to.
+  final GuaranteeChannel<T> _channel;
+
+  @override
+  Future<void> get done => _doneCompleter.future;
+  final _doneCompleter = Completer<void>();
+
+  /// Whether connection is disconnected.
+  ///
+  /// This can happen because the stream has emitted a done event, or because
+  /// the user added an error when [_allowErrors] is `false`.
+  bool _disconnected = false;
+
+  /// Whether the user has called [close].
+  bool _closed = false;
+
+  /// The subscription to the stream passed to [addStream], if a stream is
+  /// currently being added.
+  StreamSubscription<T>? _addStreamSubscription;
+
+  /// The completer for the future returned by [addStream], if a stream is
+  /// currently being added.
+  Completer? _addStreamCompleter;
+
+  /// Whether we're currently adding a stream with [addStream].
+  bool get _inAddStream => _addStreamSubscription != null;
+
+  /// Whether errors are passed on to the underlying sink.
+  ///
+  /// If this is `false`, any error passed to the sink is piped to [done] and
+  /// the underlying sink is closed.
+  final bool _allowErrors;
+
+  _GuaranteeSink(this._inner, this._channel, {bool allowErrors = true})
+      : _allowErrors = allowErrors;
+
+  @override
+  void add(T data) {
+    if (_closed) throw StateError('Cannot add event after closing.');
+    if (_inAddStream) {
+      throw StateError('Cannot add event while adding stream.');
+    }
+    if (_disconnected) return;
+
+    _inner.add(data);
+  }
+
+  @override
+  void addError(Object error, [StackTrace? stackTrace]) {
+    if (_closed) throw StateError('Cannot add event after closing.');
+    if (_inAddStream) {
+      throw StateError('Cannot add event while adding stream.');
+    }
+    if (_disconnected) return;
+
+    _addError(error, stackTrace);
+  }
+
+  /// Like [addError], but doesn't check to ensure that an error can be added.
+  ///
+  /// This is called from [addStream], so it shouldn't fail if a stream is being
+  /// added.
+  void _addError(Object error, [StackTrace? stackTrace]) {
+    if (_allowErrors) {
+      _inner.addError(error, stackTrace);
+      return;
+    }
+
+    _doneCompleter.completeError(error, stackTrace);
+
+    // Treat an error like both the stream and sink disconnecting.
+    _onStreamDisconnected();
+    _channel._onSinkDisconnected();
+
+    // Ignore errors from the inner sink. We're already surfacing one error, and
+    // if the user handles it we don't want them to have another top-level.
+    _inner.close().catchError((_) {});
+  }
+
+  @override
+  Future<void> addStream(Stream<T> stream) {
+    if (_closed) throw StateError('Cannot add stream after closing.');
+    if (_inAddStream) {
+      throw StateError('Cannot add stream while adding stream.');
+    }
+    if (_disconnected) return Future.value();
+
+    _addStreamCompleter = Completer.sync();
+    _addStreamSubscription = stream.listen(_inner.add,
+        onError: _addError, onDone: _addStreamCompleter!.complete);
+    return _addStreamCompleter!.future.then((_) {
+      _addStreamCompleter = null;
+      _addStreamSubscription = null;
+    });
+  }
+
+  @override
+  Future<void> close() {
+    if (_inAddStream) {
+      throw StateError('Cannot close sink while adding stream.');
+    }
+
+    if (_closed) return done;
+    _closed = true;
+
+    if (!_disconnected) {
+      _channel._onSinkDisconnected();
+      _doneCompleter.complete(_inner.close());
+    }
+
+    return done;
+  }
+
+  /// Called by [GuaranteeChannel] when the stream emits a done event.
+  ///
+  /// The stream being done indicates that the connection is closed, so the
+  /// sink should stop forwarding events.
+  void _onStreamDisconnected() {
+    _disconnected = true;
+    if (!_doneCompleter.isCompleted) _doneCompleter.complete();
+
+    if (!_inAddStream) return;
+    _addStreamCompleter!.complete(_addStreamSubscription!.cancel());
+    _addStreamCompleter = null;
+    _addStreamSubscription = null;
+  }
+}
diff --git a/pkgs/stream_channel/lib/src/isolate_channel.dart b/pkgs/stream_channel/lib/src/isolate_channel.dart
new file mode 100644
index 0000000..15c68a4
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/isolate_channel.dart
@@ -0,0 +1,115 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:isolate';
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// A [StreamChannel] that communicates over a [ReceivePort]/[SendPort] pair,
+/// presumably with another isolate.
+///
+/// The remote endpoint doesn't necessarily need to be running an
+/// [IsolateChannel]. This can be used with any two ports, although the
+/// [StreamChannel] semantics mean that this class will treat them as being
+/// paired (for example, closing the [sink] will cause the [stream] to stop
+/// emitting events).
+///
+/// The underlying isolate ports have no notion of closing connections. This
+/// means that [stream] won't close unless [sink] is closed, and that closing
+/// [sink] won't cause the remote endpoint to close. Users should take care to
+/// ensure that they always close the [sink] of every [IsolateChannel] they use
+/// to avoid leaving dangling [ReceivePort]s.
+class IsolateChannel<T> extends StreamChannelMixin<T> {
+  @override
+  final Stream<T> stream;
+  @override
+  final StreamSink<T> sink;
+
+  /// Connects to a remote channel that was created with
+  /// [IsolateChannel.connectSend].
+  ///
+  /// These constructors establish a connection using only a single
+  /// [SendPort]/[ReceivePort] pair, as long as each side uses one of the
+  /// connect constructors.
+  ///
+  /// The connection protocol is guaranteed to remain compatible across versions
+  /// at least until the next major version release. If the protocol is
+  /// violated, the resulting channel will emit a single value on its stream and
+  /// then close.
+  factory IsolateChannel.connectReceive(ReceivePort receivePort) {
+    // We can't use a [StreamChannelCompleter] here because we need the return
+    // value to be an [IsolateChannel].
+    var isCompleted = false;
+    var streamCompleter = StreamCompleter<T>();
+    var sinkCompleter = StreamSinkCompleter<T>();
+
+    var channel = IsolateChannel<T>._(streamCompleter.stream, sinkCompleter.sink
+        .transform(StreamSinkTransformer.fromHandlers(handleDone: (sink) {
+      if (!isCompleted) {
+        receivePort.close();
+        streamCompleter.setSourceStream(const Stream.empty());
+        sinkCompleter.setDestinationSink(NullStreamSink<T>());
+      }
+      sink.close();
+    })));
+
+    // The first message across the ReceivePort should be a SendPort pointing to
+    // the remote end. If it's not, we'll make the stream emit an error
+    // complaining.
+    late StreamSubscription<dynamic> subscription;
+    subscription = receivePort.listen((message) {
+      isCompleted = true;
+      if (message is SendPort) {
+        var controller =
+            StreamChannelController<T>(allowForeignErrors: false, sync: true);
+        SubscriptionStream(subscription).cast<T>().pipe(controller.local.sink);
+        controller.local.stream
+            .listen((data) => message.send(data), onDone: receivePort.close);
+
+        streamCompleter.setSourceStream(controller.foreign.stream);
+        sinkCompleter.setDestinationSink(controller.foreign.sink);
+        return;
+      }
+
+      streamCompleter.setError(
+          StateError('Unexpected Isolate response "$message".'),
+          StackTrace.current);
+      sinkCompleter.setDestinationSink(NullStreamSink<T>());
+      subscription.cancel();
+    });
+
+    return channel;
+  }
+
+  /// Connects to a remote channel that was created with
+  /// [IsolateChannel.connectReceive].
+  ///
+  /// These constructors establish a connection using only a single
+  /// [SendPort]/[ReceivePort] pair, as long as each side uses one of the
+  /// connect constructors.
+  ///
+  /// The connection protocol is guaranteed to remain compatible across versions
+  /// at least until the next major version release.
+  factory IsolateChannel.connectSend(SendPort sendPort) {
+    var receivePort = ReceivePort();
+    sendPort.send(receivePort.sendPort);
+    return IsolateChannel(receivePort, sendPort);
+  }
+
+  /// Creates a stream channel that receives messages from [receivePort] and
+  /// sends them over [sendPort].
+  factory IsolateChannel(ReceivePort receivePort, SendPort sendPort) {
+    var controller =
+        StreamChannelController<T>(allowForeignErrors: false, sync: true);
+    receivePort.cast<T>().pipe(controller.local.sink);
+    controller.local.stream
+        .listen((data) => sendPort.send(data), onDone: receivePort.close);
+    return IsolateChannel._(controller.foreign.stream, controller.foreign.sink);
+  }
+
+  IsolateChannel._(this.stream, this.sink);
+}
diff --git a/pkgs/stream_channel/lib/src/json_document_transformer.dart b/pkgs/stream_channel/lib/src/json_document_transformer.dart
new file mode 100644
index 0000000..3feda43
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/json_document_transformer.dart
@@ -0,0 +1,35 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// A [StreamChannelTransformer] that transforms JSON documents—strings that
+/// contain individual objects encoded as JSON—into decoded Dart objects.
+///
+/// This decodes JSON that's emitted by the transformed channel's stream, and
+/// encodes objects so that JSON is passed to the transformed channel's sink.
+///
+/// If the transformed channel emits invalid JSON, this emits a
+/// [FormatException]. If an unencodable object is added to the sink, it
+/// synchronously throws a [JsonUnsupportedObjectError].
+final StreamChannelTransformer<Object?, String> jsonDocument =
+    const _JsonDocument();
+
+class _JsonDocument implements StreamChannelTransformer<Object?, String> {
+  const _JsonDocument();
+
+  @override
+  StreamChannel<Object?> bind(StreamChannel<String> channel) {
+    var stream = channel.stream.map(jsonDecode);
+    var sink = StreamSinkTransformer<Object, String>.fromHandlers(
+        handleData: (data, sink) {
+      sink.add(jsonEncode(data));
+    }).bind(channel.sink);
+    return StreamChannel.withCloseGuarantee(stream, sink);
+  }
+}
diff --git a/pkgs/stream_channel/lib/src/multi_channel.dart b/pkgs/stream_channel/lib/src/multi_channel.dart
new file mode 100644
index 0000000..4894239
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/multi_channel.dart
@@ -0,0 +1,274 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// A class that multiplexes multiple virtual channels across a single
+/// underlying transport layer.
+///
+/// This should be connected to another [MultiChannel] on the other end of the
+/// underlying channel. It starts with a single default virtual channel,
+/// accessible via [stream] and [sink]. Additional virtual channels can be
+/// created with [virtualChannel].
+///
+/// When a virtual channel is created by one endpoint, the other must connect to
+/// it before messages may be sent through it. The first endpoint passes its
+/// [VirtualChannel.id] to the second, which then creates a channel from that id
+/// also using [virtualChannel]. For example:
+///
+/// ```dart
+/// // First endpoint
+/// var virtual = multiChannel.virtualChannel();
+/// multiChannel.sink.add({
+///   "channel": virtual.id
+/// });
+///
+/// // Second endpoint
+/// multiChannel.stream.listen((message) {
+///   var virtual = multiChannel.virtualChannel(message["channel"]);
+///   // ...
+/// });
+/// ```
+///
+/// Sending errors across a [MultiChannel] is not supported. Any errors from the
+/// underlying stream will be reported only via the default
+/// [MultiChannel.stream].
+///
+/// Each virtual channel may be closed individually. When all of them are
+/// closed, the underlying [StreamSink] is closed automatically.
+abstract class MultiChannel<T> implements StreamChannel<T> {
+  /// The default input stream.
+  ///
+  /// This connects to the remote [sink].
+  @override
+  Stream<T> get stream;
+
+  /// The default output stream.
+  ///
+  /// This connects to the remote [stream]. If this is closed, the remote
+  /// [stream] will close, but other virtual channels will remain open and new
+  /// virtual channels may be opened.
+  @override
+  StreamSink<T> get sink;
+
+  /// Creates a new [MultiChannel] that sends and receives messages over
+  /// [inner].
+  ///
+  /// The inner channel must take JSON-like objects.
+  factory MultiChannel(StreamChannel<dynamic> inner) => _MultiChannel<T>(inner);
+
+  /// Creates a new virtual channel.
+  ///
+  /// If [id] is not passed, this creates a virtual channel from scratch. Before
+  /// it's used, its [VirtualChannel.id] must be sent to the remote endpoint
+  /// where [virtualChannel] should be called with that id.
+  ///
+  /// If [id] is passed, this creates a virtual channel corresponding to the
+  /// channel with that id on the remote channel.
+  ///
+  /// Throws an [ArgumentError] if a virtual channel already exists for [id].
+  /// Throws a [StateError] if the underlying channel is closed.
+  VirtualChannel<T> virtualChannel([int? id]);
+}
+
+/// The implementation of [MultiChannel].
+///
+/// This is private so that [VirtualChannel] can inherit from [MultiChannel]
+/// without having to implement all the private members.
+class _MultiChannel<T> extends StreamChannelMixin<T>
+    implements MultiChannel<T> {
+  /// The inner channel over which all communication is conducted.
+  ///
+  /// This will be `null` if the underlying communication channel is closed.
+  StreamChannel<dynamic>? _inner;
+
+  /// The subscription to [_inner].stream.
+  StreamSubscription<dynamic>? _innerStreamSubscription;
+
+  @override
+  Stream<T> get stream => _mainController.foreign.stream;
+  @override
+  StreamSink<T> get sink => _mainController.foreign.sink;
+
+  /// The controller for this channel.
+  final _mainController = StreamChannelController<T>(sync: true);
+
+  /// A map from input IDs to [StreamChannelController]s that should be used to
+  /// communicate over those channels.
+  final _controllers = <int, StreamChannelController<T>>{};
+
+  /// Input IDs of controllers in [_controllers] that we've received messages
+  /// for but that have not yet had a local [virtualChannel] created.
+  final _pendingIds = <int>{};
+
+  /// Input IDs of virtual channels that used to exist but have since been
+  /// closed.
+  final _closedIds = <int>{};
+
+  /// The next id to use for a local virtual channel.
+  ///
+  /// Ids are used to identify virtual channels. Each message is tagged with an
+  /// id; the receiving [MultiChannel] uses this id to look up which
+  /// [VirtualChannel] the message should be dispatched to.
+  ///
+  /// The id scheme for virtual channels is somewhat complicated. This is
+  /// necessary to ensure that there are no conflicts even when both endpoints
+  /// have virtual channels with the same id; since both endpoints can send and
+  /// receive messages across each virtual channel, a naïve scheme would make it
+  /// impossible to tell whether a message was from a channel that originated in
+  /// the remote endpoint or a reply on a channel that originated in the local
+  /// endpoint.
+  ///
+  /// The trick is that each endpoint only uses odd ids for its own channels.
+  /// When sending a message over a channel that was created by the remote
+  /// endpoint, the channel's id plus one is used. This way each [MultiChannel]
+  /// knows that if an incoming message has an odd id, it's coming from a
+  /// channel that was originally created remotely, but if it has an even id,
+  /// it's coming from a channel that was originally created locally.
+  var _nextId = 1;
+
+  _MultiChannel(StreamChannel<dynamic> inner) : _inner = inner {
+    // The default connection is a special case which has id 0 on both ends.
+    // This allows it to begin connected without having to send over an id.
+    _controllers[0] = _mainController;
+    _mainController.local.stream.listen(
+        (message) => _inner!.sink.add(<Object?>[0, message]),
+        onDone: () => _closeChannel(0, 0));
+
+    _innerStreamSubscription = _inner!.stream.cast<List>().listen((message) {
+      var id = (message[0] as num).toInt();
+
+      // If the channel was closed before an incoming message was processed,
+      // ignore that message.
+      if (_closedIds.contains(id)) return;
+
+      var controller = _controllers.putIfAbsent(id, () {
+        // If we receive a message for a controller that doesn't have a local
+        // counterpart yet, create a controller for it to buffer incoming
+        // messages for when a local connection is created.
+        _pendingIds.add(id);
+        return StreamChannelController(sync: true);
+      });
+
+      if (message.length > 1) {
+        controller.local.sink.add(message[1] as T);
+      } else {
+        // A message without data indicates that the channel has been closed. We
+        // can just close the sink here without doing any more cleanup, because
+        // the sink closing will cause the stream to emit a done event which
+        // will trigger more cleanup.
+        controller.local.sink.close();
+      }
+    },
+        onDone: _closeInnerChannel,
+        onError: _mainController.local.sink.addError);
+  }
+
+  @override
+  VirtualChannel<T> virtualChannel([int? id]) {
+    int inputId;
+    int outputId;
+    if (id != null) {
+      // Since the user is passing in an id, we're connected to a remote
+      // VirtualChannel. This means messages they send over this channel will
+      // have the original odd id, but our replies will have an even id.
+      inputId = id;
+      outputId = id + 1;
+    } else {
+      // Since we're generating an id, we originated this VirtualChannel. This
+      // means messages we send over this channel will have the original odd id,
+      // but the remote channel's replies will have an even id.
+      inputId = _nextId + 1;
+      outputId = _nextId;
+      _nextId += 2;
+    }
+
+    // If the inner channel has already closed, create new virtual channels in a
+    // closed state.
+    if (_inner == null) {
+      return VirtualChannel._(
+          this, inputId, const Stream.empty(), NullStreamSink());
+    }
+
+    late StreamChannelController<T> controller;
+    if (_pendingIds.remove(inputId)) {
+      // If we've already received messages for this channel, use the controller
+      // where those messages are buffered.
+      controller = _controllers[inputId]!;
+    } else if (_controllers.containsKey(inputId) ||
+        _closedIds.contains(inputId)) {
+      throw ArgumentError('A virtual channel with id $id already exists.');
+    } else {
+      controller = StreamChannelController(sync: true);
+      _controllers[inputId] = controller;
+    }
+
+    controller.local.stream.listen(
+        (message) => _inner!.sink.add(<Object?>[outputId, message]),
+        onDone: () => _closeChannel(inputId, outputId));
+    return VirtualChannel._(
+        this, outputId, controller.foreign.stream, controller.foreign.sink);
+  }
+
+  /// Closes the virtual channel for which incoming messages have [inputId] and
+  /// outgoing messages have [outputId].
+  void _closeChannel(int inputId, int outputId) {
+    _closedIds.add(inputId);
+    var controller = _controllers.remove(inputId)!;
+    controller.local.sink.close();
+
+    if (_inner == null) return;
+
+    // A message without data indicates that the virtual channel has been
+    // closed.
+    _inner!.sink.add([outputId]);
+    if (_controllers.isEmpty) _closeInnerChannel();
+  }
+
+  /// Closes the underlying communication channel.
+  void _closeInnerChannel() {
+    _inner!.sink.close();
+    _innerStreamSubscription!.cancel();
+    _inner = null;
+
+    // Convert this to a list because the close is dispatched synchronously, and
+    // that could conceivably remove a controller from [_controllers].
+    for (var controller in _controllers.values.toList(growable: false)) {
+      controller.local.sink.close();
+    }
+    _controllers.clear();
+  }
+}
+
+/// A virtual channel created by [MultiChannel].
+///
+/// This implements [MultiChannel] for convenience.
+/// [VirtualChannel.virtualChannel] is semantically identical to the parent's
+/// [MultiChannel.virtualChannel].
+class VirtualChannel<T> extends StreamChannelMixin<T>
+    implements MultiChannel<T> {
+  /// The [MultiChannel] that created this.
+  final MultiChannel<T> _parent;
+
+  /// The identifier for this channel.
+  ///
+  /// This can be sent across the [MultiChannel] to provide the remote endpoint
+  /// a means to connect to this channel. Nothing about this is guaranteed
+  /// except that it will be JSON-serializable.
+  final int id;
+
+  @override
+  final Stream<T> stream;
+  @override
+  final StreamSink<T> sink;
+
+  VirtualChannel._(this._parent, this.id, this.stream, this.sink);
+
+  @override
+  VirtualChannel<T> virtualChannel([int? id]) => _parent.virtualChannel(id);
+}
diff --git a/pkgs/stream_channel/lib/src/stream_channel_completer.dart b/pkgs/stream_channel/lib/src/stream_channel_completer.dart
new file mode 100644
index 0000000..9d007eb
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/stream_channel_completer.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// A [channel] where the source and destination are provided later.
+///
+/// The [channel] is a normal channel that can be listened to and that events
+/// can be added to immediately, but until [setChannel] is called it won't emit
+/// any events and all events added to it will be buffered.
+class StreamChannelCompleter<T> {
+  /// The completer for this channel's stream.
+  final _streamCompleter = StreamCompleter<T>();
+
+  /// The completer for this channel's sink.
+  final _sinkCompleter = StreamSinkCompleter<T>();
+
+  /// The channel for this completer.
+  StreamChannel<T> get channel => _channel;
+  late final StreamChannel<T> _channel;
+
+  /// Whether [setChannel] has been called.
+  bool _set = false;
+
+  /// Convert a `Future<StreamChannel>` to a `StreamChannel`.
+  ///
+  /// This creates a channel using a channel completer, and sets the source
+  /// channel to the result of the future when the future completes.
+  ///
+  /// If the future completes with an error, the returned channel's stream will
+  /// instead contain just that error. The sink will silently discard all
+  /// events.
+  static StreamChannel fromFuture(Future<StreamChannel> channelFuture) {
+    var completer = StreamChannelCompleter<void>();
+    channelFuture.then(completer.setChannel, onError: completer.setError);
+    return completer.channel;
+  }
+
+  StreamChannelCompleter() {
+    _channel = StreamChannel<T>(_streamCompleter.stream, _sinkCompleter.sink);
+  }
+
+  /// Set a channel as the source and destination for [channel].
+  ///
+  /// A channel may be set at most once.
+  ///
+  /// Either [setChannel] or [setError] may be called at most once. Trying to
+  /// call either of them again will fail.
+  void setChannel(StreamChannel<T> channel) {
+    if (_set) throw StateError('The channel has already been set.');
+    _set = true;
+
+    _streamCompleter.setSourceStream(channel.stream);
+    _sinkCompleter.setDestinationSink(channel.sink);
+  }
+
+  /// Indicates that there was an error connecting the channel.
+  ///
+  /// This makes the stream emit [error] and close. It makes the sink discard
+  /// all its events.
+  ///
+  /// Either [setChannel] or [setError] may be called at most once. Trying to
+  /// call either of them again will fail.
+  void setError(Object error, [StackTrace? stackTrace]) {
+    if (_set) throw StateError('The channel has already been set.');
+    _set = true;
+
+    _streamCompleter.setError(error, stackTrace);
+    _sinkCompleter.setDestinationSink(NullStreamSink());
+  }
+}
diff --git a/pkgs/stream_channel/lib/src/stream_channel_controller.dart b/pkgs/stream_channel/lib/src/stream_channel_controller.dart
new file mode 100644
index 0000000..25d5239
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/stream_channel_controller.dart
@@ -0,0 +1,67 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// @docImport 'isolate_channel.dart';
+library;
+
+import 'dart:async';
+
+import '../stream_channel.dart';
+
+/// A controller for exposing a new [StreamChannel].
+///
+/// This exposes two connected [StreamChannel]s, [local] and [foreign]. The
+/// user's code should use [local] to emit and receive events. Then [foreign]
+/// can be returned for others to use. For example, here's a simplified version
+/// of the implementation of [IsolateChannel.new]:
+///
+/// ```dart
+/// StreamChannel isolateChannel(ReceivePort receivePort, SendPort sendPort) {
+///   var controller = new StreamChannelController(allowForeignErrors: false);
+///
+///   // Pipe all events from the receive port into the local sink...
+///   receivePort.pipe(controller.local.sink);
+///
+///   // ...and all events from the local stream into the send port.
+///   controller.local.stream.listen(sendPort.send, onDone: receivePort.close);
+///
+///   // Then return the foreign controller for your users to use.
+///   return controller.foreign;
+/// }
+/// ```
+class StreamChannelController<T> {
+  /// The local channel.
+  ///
+  /// This channel should be used directly by the creator of this
+  /// [StreamChannelController] to send and receive events.
+  StreamChannel<T> get local => _local;
+  late final StreamChannel<T> _local;
+
+  /// The foreign channel.
+  ///
+  /// This channel should be returned to external users so they can communicate
+  /// with [local].
+  StreamChannel<T> get foreign => _foreign;
+  late final StreamChannel<T> _foreign;
+
+  /// Creates a [StreamChannelController].
+  ///
+  /// If [sync] is true, events added to either channel's sink are synchronously
+  /// dispatched to the other channel's stream. This should only be done if the
+  /// source of those events is already asynchronous.
+  ///
+  /// If [allowForeignErrors] is `false`, errors are not allowed to be passed to
+  /// the foreign channel's sink. If any are, the connection will close and the
+  /// error will be forwarded to the foreign channel's [StreamSink.done] future.
+  /// This guarantees that the local stream will never emit errors.
+  StreamChannelController({bool allowForeignErrors = true, bool sync = false}) {
+    var localToForeignController = StreamController<T>(sync: sync);
+    var foreignToLocalController = StreamController<T>(sync: sync);
+    _local = StreamChannel<T>.withGuarantees(
+        foreignToLocalController.stream, localToForeignController.sink);
+    _foreign = StreamChannel<T>.withGuarantees(
+        localToForeignController.stream, foreignToLocalController.sink,
+        allowSinkErrors: allowForeignErrors);
+  }
+}
diff --git a/pkgs/stream_channel/lib/src/stream_channel_transformer.dart b/pkgs/stream_channel/lib/src/stream_channel_transformer.dart
new file mode 100644
index 0000000..cf62c76
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/stream_channel_transformer.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// A [StreamChannelTransformer] transforms the events being passed to and
+/// emitted by a [StreamChannel].
+///
+/// This works on the same principle as [StreamTransformer] and
+/// [StreamSinkTransformer]. Each transformer defines a [bind] method that takes
+/// in the original [StreamChannel] and returns the transformed version.
+///
+/// Transformers must be able to have [bind] called multiple times. If a
+/// subclass implements [bind] explicitly, it should be sure that the returned
+/// stream follows the second stream channel guarantee: closing the sink causes
+/// the stream to close before it emits any more events. This guarantee is
+/// invalidated when an asynchronous gap is added between the original stream's
+/// event dispatch and the returned stream's, for example by transforming it
+/// with a [StreamTransformer]. The guarantee can be easily preserved using
+/// [StreamChannel.withCloseGuarantee].
+class StreamChannelTransformer<S, T> {
+  /// The transformer to use on the channel's stream.
+  final StreamTransformer<T, S> _streamTransformer;
+
+  /// The transformer to use on the channel's sink.
+  final StreamSinkTransformer<S, T> _sinkTransformer;
+
+  /// Creates a [StreamChannelTransformer] from existing stream and sink
+  /// transformers.
+  const StreamChannelTransformer(
+      this._streamTransformer, this._sinkTransformer);
+
+  /// Creates a [StreamChannelTransformer] from a codec's encoder and decoder.
+  ///
+  /// All input to the inner channel's sink is encoded using [Codec.encoder],
+  /// and all output from its stream is decoded using [Codec.decoder].
+  StreamChannelTransformer.fromCodec(Codec<S, T> codec)
+      : this(codec.decoder,
+            StreamSinkTransformer.fromStreamTransformer(codec.encoder));
+
+  /// Transforms the events sent to and emitted by [channel].
+  ///
+  /// Creates a new channel. When events are passed to the returned channel's
+  /// sink, the transformer will transform them and pass the transformed
+  /// versions to `channel.sink`. When events are emitted from the
+  /// `channel.straem`, the transformer will transform them and pass the
+  /// transformed versions to the returned channel's stream.
+  StreamChannel<S> bind(StreamChannel<T> channel) =>
+      StreamChannel<S>.withCloseGuarantee(
+          channel.stream.transform(_streamTransformer),
+          _sinkTransformer.bind(channel.sink));
+}
diff --git a/pkgs/stream_channel/lib/stream_channel.dart b/pkgs/stream_channel/lib/stream_channel.dart
new file mode 100644
index 0000000..8e53096
--- /dev/null
+++ b/pkgs/stream_channel/lib/stream_channel.dart
@@ -0,0 +1,181 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+import 'src/close_guarantee_channel.dart';
+import 'src/guarantee_channel.dart';
+import 'src/stream_channel_transformer.dart';
+
+export 'src/delegating_stream_channel.dart';
+export 'src/disconnector.dart';
+export 'src/json_document_transformer.dart';
+export 'src/multi_channel.dart';
+export 'src/stream_channel_completer.dart';
+export 'src/stream_channel_controller.dart';
+export 'src/stream_channel_transformer.dart';
+
+/// An abstract class representing a two-way communication channel.
+///
+/// Users should consider the [stream] emitting a "done" event to be the
+/// canonical indicator that the channel has closed. If they wish to close the
+/// channel, they should close the [sink]—canceling the stream subscription is
+/// not sufficient. Protocol errors may be emitted through the stream or through
+/// [sink].done, depending on their underlying cause. Note that the sink may
+/// silently drop events if the channel closes before [sink].close is called.
+///
+/// Implementations are strongly encouraged to mix in or extend
+/// [StreamChannelMixin] to get default implementations of the various instance
+/// methods. Adding new methods to this interface will not be considered a
+/// breaking change if implementations are also added to [StreamChannelMixin].
+///
+/// Implementations must provide the following guarantees:
+///
+/// * The stream is single-subscription, and must follow all the guarantees of
+///   single-subscription streams.
+///
+/// * Closing the sink causes the stream to close before it emits any more
+///   events.
+///
+/// * After the stream closes, the sink is automatically closed. If this
+///   happens, sink methods should silently drop their arguments until
+///   [sink].close is called.
+///
+/// * If the stream closes before it has a listener, the sink should silently
+///   drop events if possible.
+///
+/// * Canceling the stream's subscription has no effect on the sink. The channel
+///   must still be able to respond to the other endpoint closing the channel
+///   even after the subscription has been canceled.
+///
+/// * The sink *either* forwards errors to the other endpoint *or* closes as
+///   soon as an error is added and forwards that error to the [sink].done
+///   future.
+///
+/// These guarantees allow users to interact uniformly with all implementations,
+/// and ensure that either endpoint closing the stream produces consistent
+/// behavior.
+abstract class StreamChannel<T> {
+  /// The single-subscription stream that emits values from the other endpoint.
+  Stream<T> get stream;
+
+  /// The sink for sending values to the other endpoint.
+  StreamSink<T> get sink;
+
+  /// Creates a new [StreamChannel] that communicates over [stream] and [sink].
+  ///
+  /// Note that this stream/sink pair must provide the guarantees listed in the
+  /// [StreamChannel] documentation. If they don't do so natively,
+  /// [StreamChannel.withGuarantees] should be used instead.
+  factory StreamChannel(Stream<T> stream, StreamSink<T> sink) =>
+      _StreamChannel<T>(stream, sink);
+
+  /// Creates a new [StreamChannel] that communicates over [stream] and [sink].
+  ///
+  /// Unlike [StreamChannel.new], this enforces the guarantees listed in the
+  /// [StreamChannel] documentation. This makes it somewhat less efficient than
+  /// just wrapping a stream and a sink directly, so [StreamChannel.new] should
+  /// be used when the guarantees are provided natively.
+  ///
+  /// If [allowSinkErrors] is `false`, errors are not allowed to be passed to
+  /// [sink]. If any are, the connection will close and the error will be
+  /// forwarded to [sink].done.
+  factory StreamChannel.withGuarantees(Stream<T> stream, StreamSink<T> sink,
+          {bool allowSinkErrors = true}) =>
+      GuaranteeChannel(stream, sink, allowSinkErrors: allowSinkErrors);
+
+  /// Creates a new [StreamChannel] that communicates over [stream] and [sink].
+  ///
+  /// This specifically enforces the second guarantee: closing the sink causes
+  /// the stream to close before it emits any more events. This guarantee is
+  /// invalidated when an asynchronous gap is added between the original
+  /// stream's event dispatch and the returned stream's, for example by
+  /// transforming it with a [StreamTransformer]. This is a lighter-weight way
+  /// of preserving that guarantee in particular than
+  /// [StreamChannel.withGuarantees].
+  factory StreamChannel.withCloseGuarantee(
+          Stream<T> stream, StreamSink<T> sink) =>
+      CloseGuaranteeChannel(stream, sink);
+
+  /// Connects this to [other], so that any values emitted by either are sent
+  /// directly to the other.
+  void pipe(StreamChannel<T> other);
+
+  /// Transforms this using [transformer].
+  ///
+  /// This is identical to calling `transformer.bind(channel)`.
+  StreamChannel<S> transform<S>(StreamChannelTransformer<S, T> transformer);
+
+  /// Transforms only the [stream] component of this using [transformer].
+  StreamChannel<T> transformStream(StreamTransformer<T, T> transformer);
+
+  /// Transforms only the [sink] component of this using [transformer].
+  StreamChannel<T> transformSink(StreamSinkTransformer<T, T> transformer);
+
+  /// Returns a copy of this with [stream] replaced by [change]'s return
+  /// value.
+  StreamChannel<T> changeStream(Stream<T> Function(Stream<T>) change);
+
+  /// Returns a copy of this with [sink] replaced by [change]'s return
+  /// value.
+  StreamChannel<T> changeSink(StreamSink<T> Function(StreamSink<T>) change);
+
+  /// Returns a copy of this with the generic type coerced to [S].
+  ///
+  /// If any events emitted by [stream] aren't of type [S], they're converted
+  /// into [TypeError] events (`CastError` on some SDK versions). Similarly, if
+  /// any events are added to [sink] that aren't of type [S], a [TypeError] is
+  /// thrown.
+  StreamChannel<S> cast<S>();
+}
+
+/// An implementation of [StreamChannel] that simply takes a stream and a sink
+/// as parameters.
+///
+/// This is distinct from [StreamChannel] so that it can use
+/// [StreamChannelMixin].
+class _StreamChannel<T> extends StreamChannelMixin<T> {
+  @override
+  final Stream<T> stream;
+  @override
+  final StreamSink<T> sink;
+
+  _StreamChannel(this.stream, this.sink);
+}
+
+/// A mixin that implements the instance methods of [StreamChannel] in terms of
+/// [stream] and [sink].
+abstract mixin class StreamChannelMixin<T> implements StreamChannel<T> {
+  @override
+  void pipe(StreamChannel<T> other) {
+    stream.pipe(other.sink);
+    other.stream.pipe(sink);
+  }
+
+  @override
+  StreamChannel<S> transform<S>(StreamChannelTransformer<S, T> transformer) =>
+      transformer.bind(this);
+
+  @override
+  StreamChannel<T> transformStream(StreamTransformer<T, T> transformer) =>
+      changeStream(transformer.bind);
+
+  @override
+  StreamChannel<T> transformSink(StreamSinkTransformer<T, T> transformer) =>
+      changeSink(transformer.bind);
+
+  @override
+  StreamChannel<T> changeStream(Stream<T> Function(Stream<T>) change) =>
+      StreamChannel.withCloseGuarantee(change(stream), sink);
+
+  @override
+  StreamChannel<T> changeSink(StreamSink<T> Function(StreamSink<T>) change) =>
+      StreamChannel.withCloseGuarantee(stream, change(sink));
+
+  @override
+  StreamChannel<S> cast<S>() => StreamChannel(
+      stream.cast(), StreamController(sync: true)..stream.cast<T>().pipe(sink));
+}
diff --git a/pkgs/stream_channel/pubspec.yaml b/pkgs/stream_channel/pubspec.yaml
new file mode 100644
index 0000000..f050fca
--- /dev/null
+++ b/pkgs/stream_channel/pubspec.yaml
@@ -0,0 +1,16 @@
+name: stream_channel
+version: 2.1.4
+description: >-
+  An abstraction for two-way communication channels based on the Dart Stream
+  class.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/stream_channel
+
+environment:
+  sdk: ^3.3.0
+
+dependencies:
+  async: ^2.5.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.6
diff --git a/pkgs/stream_channel/test/disconnector_test.dart b/pkgs/stream_channel/test/disconnector_test.dart
new file mode 100644
index 0000000..bbba568
--- /dev/null
+++ b/pkgs/stream_channel/test/disconnector_test.dart
@@ -0,0 +1,152 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late StreamController streamController;
+  late StreamController sinkController;
+  late Disconnector disconnector;
+  late StreamChannel channel;
+  setUp(() {
+    streamController = StreamController<void>();
+    sinkController = StreamController<void>();
+    disconnector = Disconnector();
+    channel = StreamChannel.withGuarantees(
+            streamController.stream, sinkController.sink)
+        .transform(disconnector);
+  });
+
+  group('before disconnection', () {
+    test('forwards events from the sink as normal', () {
+      channel.sink.add(1);
+      channel.sink.add(2);
+      channel.sink.add(3);
+      channel.sink.close();
+
+      expect(sinkController.stream.toList(), completion(equals([1, 2, 3])));
+    });
+
+    test('forwards events to the stream as normal', () {
+      streamController.add(1);
+      streamController.add(2);
+      streamController.add(3);
+      streamController.close();
+
+      expect(channel.stream.toList(), completion(equals([1, 2, 3])));
+    });
+
+    test("events can't be added when the sink is explicitly closed", () {
+      sinkController.stream.listen(null); // Work around sdk#19095.
+
+      expect(channel.sink.close(), completes);
+      expect(() => channel.sink.add(1), throwsStateError);
+      expect(() => channel.sink.addError('oh no'), throwsStateError);
+      expect(() => channel.sink.addStream(Stream.fromIterable([])),
+          throwsStateError);
+    });
+
+    test("events can't be added while a stream is being added", () {
+      var controller = StreamController<void>();
+      channel.sink.addStream(controller.stream);
+
+      expect(() => channel.sink.add(1), throwsStateError);
+      expect(() => channel.sink.addError('oh no'), throwsStateError);
+      expect(() => channel.sink.addStream(Stream.fromIterable([])),
+          throwsStateError);
+      expect(() => channel.sink.close(), throwsStateError);
+
+      controller.close();
+    });
+  });
+
+  test('cancels addStream when disconnected', () async {
+    var canceled = false;
+    var controller = StreamController<void>(onCancel: () {
+      canceled = true;
+    });
+    expect(channel.sink.addStream(controller.stream), completes);
+    unawaited(disconnector.disconnect());
+
+    await pumpEventQueue();
+    expect(canceled, isTrue);
+  });
+
+  test('disconnect() returns the close future from the inner sink', () async {
+    var streamController = StreamController<void>();
+    var sinkController = StreamController<void>();
+    var disconnector = Disconnector<void>();
+    var sink = _CloseCompleterSink(sinkController.sink);
+    StreamChannel.withGuarantees(streamController.stream, sink)
+        .transform(disconnector);
+
+    var disconnectFutureFired = false;
+    expect(
+        disconnector.disconnect().then((_) {
+          disconnectFutureFired = true;
+        }),
+        completes);
+
+    // Give the future time to fire early if it's going to.
+    await pumpEventQueue();
+    expect(disconnectFutureFired, isFalse);
+
+    // When the inner sink's close future completes, so should the
+    // disconnector's.
+    sink.completer.complete();
+    await pumpEventQueue();
+    expect(disconnectFutureFired, isTrue);
+  });
+
+  group('after disconnection', () {
+    setUp(() {
+      disconnector.disconnect();
+    });
+
+    test('closes the inner sink and ignores events to the outer sink', () {
+      channel.sink.add(1);
+      channel.sink.add(2);
+      channel.sink.add(3);
+      channel.sink.close();
+
+      expect(sinkController.stream.toList(), completion(isEmpty));
+    });
+
+    test('closes the stream', () {
+      expect(channel.stream.toList(), completion(isEmpty));
+    });
+
+    test('completes done', () {
+      sinkController.stream.listen(null); // Work around sdk#19095.
+      expect(channel.sink.done, completes);
+    });
+
+    test('still emits state errors after explicit close', () {
+      sinkController.stream.listen(null); // Work around sdk#19095.
+      expect(channel.sink.close(), completes);
+
+      expect(() => channel.sink.add(1), throwsStateError);
+      expect(() => channel.sink.addError('oh no'), throwsStateError);
+    });
+  });
+}
+
+/// A [StreamSink] wrapper that adds the ability to manually complete the Future
+/// returned by [close] using [completer].
+class _CloseCompleterSink extends DelegatingStreamSink {
+  /// The completer for the future returned by [close].
+  final completer = Completer<void>();
+
+  _CloseCompleterSink(super.inner);
+
+  @override
+  Future<void> close() {
+    super.close();
+    return completer.future;
+  }
+}
diff --git a/pkgs/stream_channel/test/isolate_channel_test.dart b/pkgs/stream_channel/test/isolate_channel_test.dart
new file mode 100644
index 0000000..3a8b42e
--- /dev/null
+++ b/pkgs/stream_channel/test/isolate_channel_test.dart
@@ -0,0 +1,174 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+import 'dart:isolate';
+
+import 'package:stream_channel/isolate_channel.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late ReceivePort receivePort;
+  late SendPort sendPort;
+  late StreamChannel channel;
+  setUp(() {
+    receivePort = ReceivePort();
+    var receivePortForSend = ReceivePort();
+    sendPort = receivePortForSend.sendPort;
+    channel = IsolateChannel(receivePortForSend, receivePort.sendPort);
+  });
+
+  tearDown(() {
+    receivePort.close();
+    channel.sink.close();
+  });
+
+  test('the channel can send messages', () {
+    channel.sink.add(1);
+    channel.sink.add(2);
+    channel.sink.add(3);
+
+    expect(receivePort.take(3).toList(), completion(equals([1, 2, 3])));
+  });
+
+  test('the channel can receive messages', () {
+    sendPort.send(1);
+    sendPort.send(2);
+    sendPort.send(3);
+
+    expect(channel.stream.take(3).toList(), completion(equals([1, 2, 3])));
+  });
+
+  test("events can't be added to an explicitly-closed sink", () {
+    expect(channel.sink.close(), completes);
+    expect(() => channel.sink.add(1), throwsStateError);
+    expect(() => channel.sink.addError('oh no'), throwsStateError);
+    expect(() => channel.sink.addStream(Stream.fromIterable([])),
+        throwsStateError);
+  });
+
+  test("events can't be added while a stream is being added", () {
+    var controller = StreamController<void>();
+    channel.sink.addStream(controller.stream);
+
+    expect(() => channel.sink.add(1), throwsStateError);
+    expect(() => channel.sink.addError('oh no'), throwsStateError);
+    expect(() => channel.sink.addStream(Stream.fromIterable([])),
+        throwsStateError);
+    expect(() => channel.sink.close(), throwsStateError);
+
+    controller.close();
+  });
+
+  group('stream channel rules', () {
+    test(
+        'closing the sink causes the stream to close before it emits any more '
+        'events', () {
+      sendPort.send(1);
+      sendPort.send(2);
+      sendPort.send(3);
+      sendPort.send(4);
+      sendPort.send(5);
+
+      channel.stream.listen(expectAsync1((message) {
+        expect(message, equals(1));
+        channel.sink.close();
+      }, count: 1));
+    });
+
+    test("cancelling the stream's subscription has no effect on the sink",
+        () async {
+      unawaited(channel.stream.listen(null).cancel());
+      await pumpEventQueue();
+
+      channel.sink.add(1);
+      channel.sink.add(2);
+      channel.sink.add(3);
+      expect(receivePort.take(3).toList(), completion(equals([1, 2, 3])));
+    });
+
+    test('the sink closes as soon as an error is added', () async {
+      channel.sink.addError('oh no');
+      channel.sink.add(1);
+      expect(channel.sink.done, throwsA('oh no'));
+
+      // Since the sink is closed, the stream should also be closed.
+      expect(channel.stream.isEmpty, completion(isTrue));
+
+      // The other end shouldn't receive the next event, since the sink was
+      // closed. Pump the event queue to give it a chance to.
+      receivePort.listen(expectAsync1((_) {}, count: 0));
+      await pumpEventQueue();
+    });
+
+    test('the sink closes as soon as an error is added via addStream',
+        () async {
+      var canceled = false;
+      var controller = StreamController<void>(onCancel: () {
+        canceled = true;
+      });
+
+      // This future shouldn't get the error, because it's sent to [Sink.done].
+      expect(channel.sink.addStream(controller.stream), completes);
+
+      controller.addError('oh no');
+      expect(channel.sink.done, throwsA('oh no'));
+      await pumpEventQueue();
+      expect(canceled, isTrue);
+
+      // Even though the sink is closed, this shouldn't throw an error because
+      // the user didn't explicitly close it.
+      channel.sink.add(1);
+    });
+  });
+
+  group('connect constructors', () {
+    late ReceivePort connectPort;
+    setUp(() {
+      connectPort = ReceivePort();
+    });
+
+    tearDown(() {
+      connectPort.close();
+    });
+
+    test('create a connected pair of channels', () async {
+      var channel1 = IsolateChannel<int>.connectReceive(connectPort);
+      var channel2 = IsolateChannel<int>.connectSend(connectPort.sendPort);
+
+      channel1.sink.add(1);
+      channel1.sink.add(2);
+      channel1.sink.add(3);
+      expect(await channel2.stream.take(3).toList(), equals([1, 2, 3]));
+
+      channel2.sink.add(4);
+      channel2.sink.add(5);
+      channel2.sink.add(6);
+      expect(await channel1.stream.take(3).toList(), equals([4, 5, 6]));
+
+      await channel2.sink.close();
+    });
+
+    test('the receiving channel produces an error if it gets the wrong message',
+        () {
+      var connectedChannel = IsolateChannel<int>.connectReceive(connectPort);
+      connectPort.sendPort.send('wrong value');
+
+      expect(connectedChannel.stream.toList(), throwsStateError);
+      expect(connectedChannel.sink.done, completes);
+    });
+
+    test('the receiving channel closes gracefully without a connection',
+        () async {
+      var connectedChannel = IsolateChannel<int>.connectReceive(connectPort);
+      await connectedChannel.sink.close();
+      await expectLater(connectedChannel.stream.toList(), completion(isEmpty));
+      await expectLater(connectedChannel.sink.done, completes);
+    });
+  });
+}
diff --git a/pkgs/stream_channel/test/json_document_transformer_test.dart b/pkgs/stream_channel/test/json_document_transformer_test.dart
new file mode 100644
index 0000000..290c4e2
--- /dev/null
+++ b/pkgs/stream_channel/test/json_document_transformer_test.dart
@@ -0,0 +1,46 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late StreamController<String> streamController;
+  late StreamController<String> sinkController;
+  late StreamChannel<String> channel;
+  setUp(() {
+    streamController = StreamController<String>();
+    sinkController = StreamController<String>();
+    channel =
+        StreamChannel<String>(streamController.stream, sinkController.sink);
+  });
+
+  test('decodes JSON emitted by the channel', () {
+    var transformed = channel.transform(jsonDocument);
+    streamController.add('{"foo": "bar"}');
+    expect(transformed.stream.first, completion(equals({'foo': 'bar'})));
+  });
+
+  test('encodes objects added to the channel', () {
+    var transformed = channel.transform(jsonDocument);
+    transformed.sink.add({'foo': 'bar'});
+    expect(sinkController.stream.first,
+        completion(equals(jsonEncode({'foo': 'bar'}))));
+  });
+
+  test('emits a stream error when incoming JSON is malformed', () {
+    var transformed = channel.transform(jsonDocument);
+    streamController.add('{invalid');
+    expect(transformed.stream.first, throwsFormatException);
+  });
+
+  test('synchronously throws if an unencodable object is added', () {
+    var transformed = channel.transform(jsonDocument);
+    expect(() => transformed.sink.add(Object()),
+        throwsA(const TypeMatcher<JsonUnsupportedObjectError>()));
+  });
+}
diff --git a/pkgs/stream_channel/test/multi_channel_test.dart b/pkgs/stream_channel/test/multi_channel_test.dart
new file mode 100644
index 0000000..ee6f8d2
--- /dev/null
+++ b/pkgs/stream_channel/test/multi_channel_test.dart
@@ -0,0 +1,478 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late StreamChannelController controller;
+  late MultiChannel channel1;
+  late MultiChannel channel2;
+  setUp(() {
+    controller = StreamChannelController();
+    channel1 = MultiChannel<int>(controller.local);
+    channel2 = MultiChannel<int>(controller.foreign);
+  });
+
+  group('the default virtual channel', () {
+    test('begins connected', () {
+      var first = true;
+      channel2.stream.listen(expectAsync1((message) {
+        if (first) {
+          expect(message, equals(1));
+          first = false;
+        } else {
+          expect(message, equals(2));
+        }
+      }, count: 2));
+
+      channel1.sink.add(1);
+      channel1.sink.add(2);
+    });
+
+    test('closes the remote virtual channel when it closes', () {
+      expect(channel2.stream.toList(), completion(isEmpty));
+      expect(channel2.sink.done, completes);
+
+      channel1.sink.close();
+    });
+
+    test('closes the local virtual channel when it closes', () {
+      expect(channel1.stream.toList(), completion(isEmpty));
+      expect(channel1.sink.done, completes);
+
+      channel1.sink.close();
+    });
+
+    test(
+        "doesn't closes the local virtual channel when the stream "
+        'subscription is canceled', () {
+      channel1.sink.done.then(expectAsync1((_) {}, count: 0));
+
+      channel1.stream.listen((_) {}).cancel();
+
+      // Ensure that there's enough time for the channel to close if it's going
+      // to.
+      return pumpEventQueue();
+    });
+
+    test(
+        'closes the underlying channel when it closes without any other '
+        'virtual channels', () {
+      expect(controller.local.sink.done, completes);
+      expect(controller.foreign.sink.done, completes);
+
+      channel1.sink.close();
+    });
+
+    test(
+        "doesn't close the underlying channel when it closes with other "
+        'virtual channels', () {
+      controller.local.sink.done.then(expectAsync1((_) {}, count: 0));
+      controller.foreign.sink.done.then(expectAsync1((_) {}, count: 0));
+
+      // Establish another virtual connection which should keep the underlying
+      // connection open.
+      channel2.virtualChannel(channel1.virtualChannel().id);
+      channel1.sink.close();
+
+      // Ensure that there's enough time for the underlying channel to complete
+      // if it's going to.
+      return pumpEventQueue();
+    });
+  });
+
+  group('a locally-created virtual channel', () {
+    late VirtualChannel virtual1;
+    late VirtualChannel virtual2;
+    setUp(() {
+      virtual1 = channel1.virtualChannel();
+      virtual2 = channel2.virtualChannel(virtual1.id);
+    });
+
+    test('sends messages only to the other virtual channel', () {
+      var first = true;
+      virtual2.stream.listen(expectAsync1((message) {
+        if (first) {
+          expect(message, equals(1));
+          first = false;
+        } else {
+          expect(message, equals(2));
+        }
+      }, count: 2));
+
+      // No other virtual channels should receive the message.
+      for (var i = 0; i < 10; i++) {
+        var virtual = channel2.virtualChannel(channel1.virtualChannel().id);
+        virtual.stream.listen(expectAsync1((_) {}, count: 0));
+      }
+      channel2.stream.listen(expectAsync1((_) {}, count: 0));
+
+      virtual1.sink.add(1);
+      virtual1.sink.add(2);
+    });
+
+    test('closes the remote virtual channel when it closes', () {
+      expect(virtual2.stream.toList(), completion(isEmpty));
+      expect(virtual2.sink.done, completes);
+
+      virtual1.sink.close();
+    });
+
+    test('closes the local virtual channel when it closes', () {
+      expect(virtual1.stream.toList(), completion(isEmpty));
+      expect(virtual1.sink.done, completes);
+
+      virtual1.sink.close();
+    });
+
+    test(
+        "doesn't closes the local virtual channel when the stream "
+        'subscription is canceled', () {
+      virtual1.sink.done.then(expectAsync1((_) {}, count: 0));
+      virtual1.stream.listen((_) {}).cancel();
+
+      // Ensure that there's enough time for the channel to close if it's going
+      // to.
+      return pumpEventQueue();
+    });
+
+    test(
+        'closes the underlying channel when it closes without any other '
+        'virtual channels', () async {
+      // First close the default channel so we can test the new channel as the
+      // last living virtual channel.
+      unawaited(channel1.sink.close());
+
+      await channel2.stream.toList();
+      expect(controller.local.sink.done, completes);
+      expect(controller.foreign.sink.done, completes);
+
+      unawaited(virtual1.sink.close());
+    });
+
+    test(
+        "doesn't close the underlying channel when it closes with other "
+        'virtual channels', () {
+      controller.local.sink.done.then(expectAsync1((_) {}, count: 0));
+      controller.foreign.sink.done.then(expectAsync1((_) {}, count: 0));
+
+      virtual1.sink.close();
+
+      // Ensure that there's enough time for the underlying channel to complete
+      // if it's going to.
+      return pumpEventQueue();
+    });
+
+    test("doesn't conflict with a remote virtual channel", () {
+      var virtual3 = channel2.virtualChannel();
+      var virtual4 = channel1.virtualChannel(virtual3.id);
+
+      // This is an implementation detail, but we assert it here to make sure
+      // we're properly testing two channels with the same id.
+      expect(virtual1.id, equals(virtual3.id));
+
+      virtual2.stream
+          .listen(expectAsync1((message) => expect(message, equals(1))));
+      virtual4.stream
+          .listen(expectAsync1((message) => expect(message, equals(2))));
+
+      virtual1.sink.add(1);
+      virtual3.sink.add(2);
+    });
+  });
+
+  group('a remotely-created virtual channel', () {
+    late VirtualChannel virtual1;
+    late VirtualChannel virtual2;
+    setUp(() {
+      virtual1 = channel1.virtualChannel();
+      virtual2 = channel2.virtualChannel(virtual1.id);
+    });
+
+    test('sends messages only to the other virtual channel', () {
+      var first = true;
+      virtual1.stream.listen(expectAsync1((message) {
+        if (first) {
+          expect(message, equals(1));
+          first = false;
+        } else {
+          expect(message, equals(2));
+        }
+      }, count: 2));
+
+      // No other virtual channels should receive the message.
+      for (var i = 0; i < 10; i++) {
+        var virtual = channel2.virtualChannel(channel1.virtualChannel().id);
+        virtual.stream.listen(expectAsync1((_) {}, count: 0));
+      }
+      channel1.stream.listen(expectAsync1((_) {}, count: 0));
+
+      virtual2.sink.add(1);
+      virtual2.sink.add(2);
+    });
+
+    test('closes the remote virtual channel when it closes', () {
+      expect(virtual1.stream.toList(), completion(isEmpty));
+      expect(virtual1.sink.done, completes);
+
+      virtual2.sink.close();
+    });
+
+    test('closes the local virtual channel when it closes', () {
+      expect(virtual2.stream.toList(), completion(isEmpty));
+      expect(virtual2.sink.done, completes);
+
+      virtual2.sink.close();
+    });
+
+    test(
+        "doesn't closes the local virtual channel when the stream "
+        'subscription is canceled', () {
+      virtual2.sink.done.then(expectAsync1((_) {}, count: 0));
+      virtual2.stream.listen((_) {}).cancel();
+
+      // Ensure that there's enough time for the channel to close if it's going
+      // to.
+      return pumpEventQueue();
+    });
+
+    test(
+        'closes the underlying channel when it closes without any other '
+        'virtual channels', () async {
+      // First close the default channel so we can test the new channel as the
+      // last living virtual channel.
+      unawaited(channel2.sink.close());
+
+      await channel1.stream.toList();
+      expect(controller.local.sink.done, completes);
+      expect(controller.foreign.sink.done, completes);
+
+      unawaited(virtual2.sink.close());
+    });
+
+    test(
+        "doesn't close the underlying channel when it closes with other "
+        'virtual channels', () {
+      controller.local.sink.done.then(expectAsync1((_) {}, count: 0));
+      controller.foreign.sink.done.then(expectAsync1((_) {}, count: 0));
+
+      virtual2.sink.close();
+
+      // Ensure that there's enough time for the underlying channel to complete
+      // if it's going to.
+      return pumpEventQueue();
+    });
+
+    test("doesn't allow another virtual channel with the same id", () {
+      expect(() => channel2.virtualChannel(virtual1.id), throwsArgumentError);
+    });
+
+    test('dispatches events received before the virtual channel is created',
+        () async {
+      virtual1 = channel1.virtualChannel();
+
+      virtual1.sink.add(1);
+      await pumpEventQueue();
+
+      virtual1.sink.add(2);
+      await pumpEventQueue();
+
+      expect(channel2.virtualChannel(virtual1.id).stream, emitsInOrder([1, 2]));
+    });
+
+    test(
+        'dispatches close events received before the virtual channel is '
+        'created', () async {
+      virtual1 = channel1.virtualChannel();
+
+      unawaited(virtual1.sink.close());
+      await pumpEventQueue();
+
+      expect(channel2.virtualChannel(virtual1.id).stream.toList(),
+          completion(isEmpty));
+    });
+  });
+
+  group('when the underlying stream', () {
+    late VirtualChannel virtual1;
+    late VirtualChannel virtual2;
+    setUp(() {
+      virtual1 = channel1.virtualChannel();
+      virtual2 = channel2.virtualChannel(virtual1.id);
+    });
+
+    test('closes, all virtual channels close', () {
+      expect(channel1.stream.toList(), completion(isEmpty));
+      expect(channel1.sink.done, completes);
+      expect(channel2.stream.toList(), completion(isEmpty));
+      expect(channel2.sink.done, completes);
+      expect(virtual1.stream.toList(), completion(isEmpty));
+      expect(virtual1.sink.done, completes);
+      expect(virtual2.stream.toList(), completion(isEmpty));
+      expect(virtual2.sink.done, completes);
+
+      controller.local.sink.close();
+    });
+
+    test('closes, more virtual channels are created closed', () async {
+      unawaited(channel2.sink.close());
+      unawaited(virtual2.sink.close());
+
+      // Wait for the existing channels to emit done events.
+      await channel1.stream.toList();
+      await virtual1.stream.toList();
+
+      var virtual = channel1.virtualChannel();
+      expect(virtual.stream.toList(), completion(isEmpty));
+      expect(virtual.sink.done, completes);
+
+      virtual = channel1.virtualChannel();
+      expect(virtual.stream.toList(), completion(isEmpty));
+      expect(virtual.sink.done, completes);
+    });
+
+    test('emits an error, the error is sent only to the default channel', () {
+      channel1.stream.listen(expectAsync1((_) {}, count: 0),
+          onError: expectAsync1((error) => expect(error, equals('oh no'))));
+      virtual1.stream.listen(expectAsync1((_) {}, count: 0),
+          onError: expectAsync1((_) {}, count: 0));
+
+      controller.foreign.sink.addError('oh no');
+    });
+  });
+
+  group('stream channel rules', () {
+    group('for the main stream:', () {
+      test(
+          'closing the sink causes the stream to close before it emits any '
+          'more events', () {
+        channel1.sink.add(1);
+        channel1.sink.add(2);
+        channel1.sink.add(3);
+
+        channel2.stream.listen(expectAsync1((message) {
+          expect(message, equals(1));
+          channel2.sink.close();
+        }, count: 1));
+      });
+
+      test('after the stream closes, the sink ignores events', () async {
+        unawaited(channel1.sink.close());
+
+        // Wait for the done event to be delivered.
+        await channel2.stream.toList();
+        channel2.sink.add(1);
+        channel2.sink.add(2);
+        channel2.sink.add(3);
+        unawaited(channel2.sink.close());
+
+        // None of our channel.sink additions should make it to the other
+        // endpoint.
+        channel1.stream.listen(expectAsync1((_) {}, count: 0));
+        await pumpEventQueue();
+      });
+
+      test("canceling the stream's subscription has no effect on the sink",
+          () async {
+        unawaited(channel1.stream.listen(null).cancel());
+        await pumpEventQueue();
+
+        channel1.sink.add(1);
+        channel1.sink.add(2);
+        channel1.sink.add(3);
+        unawaited(channel1.sink.close());
+        expect(channel2.stream.toList(), completion(equals([1, 2, 3])));
+      });
+
+      test("canceling the stream's subscription doesn't stop a done event",
+          () async {
+        unawaited(channel1.stream.listen(null).cancel());
+        await pumpEventQueue();
+
+        unawaited(channel2.sink.close());
+        await pumpEventQueue();
+
+        channel1.sink.add(1);
+        channel1.sink.add(2);
+        channel1.sink.add(3);
+        unawaited(channel1.sink.close());
+
+        // The sink should be ignoring events because the channel closed.
+        channel2.stream.listen(expectAsync1((_) {}, count: 0));
+        await pumpEventQueue();
+      });
+    });
+
+    group('for a virtual channel:', () {
+      late VirtualChannel virtual1;
+      late VirtualChannel virtual2;
+      setUp(() {
+        virtual1 = channel1.virtualChannel();
+        virtual2 = channel2.virtualChannel(virtual1.id);
+      });
+
+      test(
+          'closing the sink causes the stream to close before it emits any '
+          'more events', () {
+        virtual1.sink.add(1);
+        virtual1.sink.add(2);
+        virtual1.sink.add(3);
+
+        virtual2.stream.listen(expectAsync1((message) {
+          expect(message, equals(1));
+          virtual2.sink.close();
+        }, count: 1));
+      });
+
+      test('after the stream closes, the sink ignores events', () async {
+        unawaited(virtual1.sink.close());
+
+        // Wait for the done event to be delivered.
+        await virtual2.stream.toList();
+        virtual2.sink.add(1);
+        virtual2.sink.add(2);
+        virtual2.sink.add(3);
+        unawaited(virtual2.sink.close());
+
+        // None of our virtual.sink additions should make it to the other
+        // endpoint.
+        virtual1.stream.listen(expectAsync1((_) {}, count: 0));
+        await pumpEventQueue();
+      });
+
+      test("canceling the stream's subscription has no effect on the sink",
+          () async {
+        unawaited(virtual1.stream.listen(null).cancel());
+        await pumpEventQueue();
+
+        virtual1.sink.add(1);
+        virtual1.sink.add(2);
+        virtual1.sink.add(3);
+        unawaited(virtual1.sink.close());
+        expect(virtual2.stream.toList(), completion(equals([1, 2, 3])));
+      });
+
+      test("canceling the stream's subscription doesn't stop a done event",
+          () async {
+        unawaited(virtual1.stream.listen(null).cancel());
+        await pumpEventQueue();
+
+        unawaited(virtual2.sink.close());
+        await pumpEventQueue();
+
+        virtual1.sink.add(1);
+        virtual1.sink.add(2);
+        virtual1.sink.add(3);
+        unawaited(virtual1.sink.close());
+
+        // The sink should be ignoring events because the stream closed.
+        virtual2.stream.listen(expectAsync1((_) {}, count: 0));
+        await pumpEventQueue();
+      });
+    });
+  });
+}
diff --git a/pkgs/stream_channel/test/stream_channel_completer_test.dart b/pkgs/stream_channel/test/stream_channel_completer_test.dart
new file mode 100644
index 0000000..c6fddc0
--- /dev/null
+++ b/pkgs/stream_channel/test/stream_channel_completer_test.dart
@@ -0,0 +1,120 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late StreamChannelCompleter completer;
+  late StreamController streamController;
+  late StreamController sinkController;
+  late StreamChannel innerChannel;
+  setUp(() {
+    completer = StreamChannelCompleter();
+    streamController = StreamController<void>();
+    sinkController = StreamController<void>();
+    innerChannel = StreamChannel(streamController.stream, sinkController.sink);
+  });
+
+  group('when a channel is set before accessing', () {
+    test('forwards events through the stream', () {
+      completer.setChannel(innerChannel);
+      expect(completer.channel.stream.toList(), completion(equals([1, 2, 3])));
+
+      streamController.add(1);
+      streamController.add(2);
+      streamController.add(3);
+      streamController.close();
+    });
+
+    test('forwards events through the sink', () {
+      completer.setChannel(innerChannel);
+      expect(sinkController.stream.toList(), completion(equals([1, 2, 3])));
+
+      completer.channel.sink.add(1);
+      completer.channel.sink.add(2);
+      completer.channel.sink.add(3);
+      completer.channel.sink.close();
+    });
+
+    test('forwards an error through the stream', () {
+      completer.setError('oh no');
+      expect(completer.channel.stream.first, throwsA('oh no'));
+    });
+
+    test('drops sink events', () {
+      completer.setError('oh no');
+      expect(completer.channel.sink.done, completes);
+      completer.channel.sink.add(1);
+      completer.channel.sink.addError('oh no');
+    });
+  });
+
+  group('when a channel is set after accessing', () {
+    test('forwards events through the stream', () async {
+      expect(completer.channel.stream.toList(), completion(equals([1, 2, 3])));
+      await pumpEventQueue();
+
+      completer.setChannel(innerChannel);
+      streamController.add(1);
+      streamController.add(2);
+      streamController.add(3);
+      unawaited(streamController.close());
+    });
+
+    test('forwards events through the sink', () async {
+      completer.channel.sink.add(1);
+      completer.channel.sink.add(2);
+      completer.channel.sink.add(3);
+      unawaited(completer.channel.sink.close());
+      await pumpEventQueue();
+
+      completer.setChannel(innerChannel);
+      expect(sinkController.stream.toList(), completion(equals([1, 2, 3])));
+    });
+
+    test('forwards an error through the stream', () async {
+      expect(completer.channel.stream.first, throwsA('oh no'));
+      await pumpEventQueue();
+
+      completer.setError('oh no');
+    });
+
+    test('drops sink events', () async {
+      expect(completer.channel.sink.done, completes);
+      completer.channel.sink.add(1);
+      completer.channel.sink.addError('oh no');
+      await pumpEventQueue();
+
+      completer.setError('oh no');
+    });
+  });
+
+  group('forFuture', () {
+    test('forwards a StreamChannel', () {
+      var channel =
+          StreamChannelCompleter.fromFuture(Future.value(innerChannel));
+      channel.sink.add(1);
+      channel.sink.close();
+      streamController.sink.add(2);
+      streamController.sink.close();
+
+      expect(sinkController.stream.toList(), completion(equals([1])));
+      expect(channel.stream.toList(), completion(equals([2])));
+    });
+
+    test('forwards an error', () {
+      var channel = StreamChannelCompleter.fromFuture(Future.error('oh no'));
+      expect(channel.stream.toList(), throwsA('oh no'));
+    });
+  });
+
+  test("doesn't allow the channel to be set multiple times", () {
+    completer.setChannel(innerChannel);
+    expect(() => completer.setChannel(innerChannel), throwsStateError);
+    expect(() => completer.setChannel(innerChannel), throwsStateError);
+  });
+}
diff --git a/pkgs/stream_channel/test/stream_channel_controller_test.dart b/pkgs/stream_channel/test/stream_channel_controller_test.dart
new file mode 100644
index 0000000..3d661e3
--- /dev/null
+++ b/pkgs/stream_channel/test/stream_channel_controller_test.dart
@@ -0,0 +1,104 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('asynchronously', () {
+    late StreamChannelController controller;
+    setUp(() {
+      controller = StreamChannelController();
+    });
+
+    test('forwards events from the local sink to the foreign stream', () {
+      controller.local.sink
+        ..add(1)
+        ..add(2)
+        ..add(3)
+        ..close();
+      expect(controller.foreign.stream.toList(), completion(equals([1, 2, 3])));
+    });
+
+    test('forwards events from the foreign sink to the local stream', () {
+      controller.foreign.sink
+        ..add(1)
+        ..add(2)
+        ..add(3)
+        ..close();
+      expect(controller.local.stream.toList(), completion(equals([1, 2, 3])));
+    });
+
+    test(
+        'with allowForeignErrors: false, shuts down the connection if an '
+        'error is added to the foreign channel', () {
+      controller = StreamChannelController(allowForeignErrors: false);
+
+      controller.foreign.sink.addError('oh no');
+      expect(controller.foreign.sink.done, throwsA('oh no'));
+      expect(controller.foreign.stream.toList(), completion(isEmpty));
+      expect(controller.local.sink.done, completes);
+      expect(controller.local.stream.toList(), completion(isEmpty));
+    });
+  });
+
+  group('synchronously', () {
+    late StreamChannelController controller;
+    setUp(() {
+      controller = StreamChannelController(sync: true);
+    });
+
+    test(
+        'synchronously forwards events from the local sink to the foreign '
+        'stream', () {
+      var receivedEvent = false;
+      var receivedError = false;
+      var receivedDone = false;
+      controller.foreign.stream.listen(expectAsync1((event) {
+        expect(event, equals(1));
+        receivedEvent = true;
+      }), onError: expectAsync1((error) {
+        expect(error, equals('oh no'));
+        receivedError = true;
+      }), onDone: expectAsync0(() {
+        receivedDone = true;
+      }));
+
+      controller.local.sink.add(1);
+      expect(receivedEvent, isTrue);
+
+      controller.local.sink.addError('oh no');
+      expect(receivedError, isTrue);
+
+      controller.local.sink.close();
+      expect(receivedDone, isTrue);
+    });
+
+    test(
+        'synchronously forwards events from the foreign sink to the local '
+        'stream', () {
+      var receivedEvent = false;
+      var receivedError = false;
+      var receivedDone = false;
+      controller.local.stream.listen(expectAsync1((event) {
+        expect(event, equals(1));
+        receivedEvent = true;
+      }), onError: expectAsync1((error) {
+        expect(error, equals('oh no'));
+        receivedError = true;
+      }), onDone: expectAsync0(() {
+        receivedDone = true;
+      }));
+
+      controller.foreign.sink.add(1);
+      expect(receivedEvent, isTrue);
+
+      controller.foreign.sink.addError('oh no');
+      expect(receivedError, isTrue);
+
+      controller.foreign.sink.close();
+      expect(receivedDone, isTrue);
+    });
+  });
+}
diff --git a/pkgs/stream_channel/test/stream_channel_test.dart b/pkgs/stream_channel/test/stream_channel_test.dart
new file mode 100644
index 0000000..166671d
--- /dev/null
+++ b/pkgs/stream_channel/test/stream_channel_test.dart
@@ -0,0 +1,180 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test("pipe() pipes data from each channel's stream into the other's sink",
+      () {
+    var otherStreamController = StreamController<int>();
+    var otherSinkController = StreamController<int>();
+    var otherChannel =
+        StreamChannel(otherStreamController.stream, otherSinkController.sink);
+
+    var streamController = StreamController<int>();
+    var sinkController = StreamController<int>();
+    var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+    channel.pipe(otherChannel);
+
+    streamController.add(1);
+    streamController.add(2);
+    streamController.add(3);
+    streamController.close();
+    expect(otherSinkController.stream.toList(), completion(equals([1, 2, 3])));
+
+    otherStreamController.add(4);
+    otherStreamController.add(5);
+    otherStreamController.add(6);
+    otherStreamController.close();
+    expect(sinkController.stream.toList(), completion(equals([4, 5, 6])));
+  });
+
+  test('transform() transforms the channel', () async {
+    var streamController = StreamController<List<int>>();
+    var sinkController = StreamController<List<int>>();
+    var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+    var transformed = channel
+        .cast<List<int>>()
+        .transform(StreamChannelTransformer.fromCodec(utf8));
+
+    streamController.add([102, 111, 111, 98, 97, 114]);
+    unawaited(streamController.close());
+    expect(await transformed.stream.toList(), equals(['foobar']));
+
+    transformed.sink.add('fblthp');
+    unawaited(transformed.sink.close());
+    expect(
+        sinkController.stream.toList(),
+        completion(equals([
+          [102, 98, 108, 116, 104, 112]
+        ])));
+  });
+
+  test('transformStream() transforms only the stream', () async {
+    var streamController = StreamController<String>();
+    var sinkController = StreamController<String>();
+    var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+    var transformed =
+        channel.cast<String>().transformStream(const LineSplitter());
+
+    streamController.add('hello world');
+    streamController.add(' what\nis');
+    streamController.add('\nup');
+    unawaited(streamController.close());
+    expect(await transformed.stream.toList(),
+        equals(['hello world what', 'is', 'up']));
+
+    transformed.sink.add('fbl\nthp');
+    unawaited(transformed.sink.close());
+    expect(sinkController.stream.toList(), completion(equals(['fbl\nthp'])));
+  });
+
+  test('transformSink() transforms only the sink', () async {
+    var streamController = StreamController<String>();
+    var sinkController = StreamController<String>();
+    var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+    var transformed = channel.cast<String>().transformSink(
+        const StreamSinkTransformer.fromStreamTransformer(LineSplitter()));
+
+    streamController.add('fbl\nthp');
+    unawaited(streamController.close());
+    expect(await transformed.stream.toList(), equals(['fbl\nthp']));
+
+    transformed.sink.add('hello world');
+    transformed.sink.add(' what\nis');
+    transformed.sink.add('\nup');
+    unawaited(transformed.sink.close());
+    expect(sinkController.stream.toList(),
+        completion(equals(['hello world what', 'is', 'up'])));
+  });
+
+  test('changeStream() changes the stream', () {
+    var streamController = StreamController<int>();
+    var sinkController = StreamController<int>();
+    var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+    var newController = StreamController<int>();
+    var changed = channel.changeStream((stream) {
+      expect(stream, equals(channel.stream));
+      return newController.stream;
+    });
+
+    newController.add(10);
+    newController.close();
+
+    streamController.add(20);
+    streamController.close();
+
+    expect(changed.stream.toList(), completion(equals([10])));
+  });
+
+  test('changeSink() changes the sink', () {
+    var streamController = StreamController<int>();
+    var sinkController = StreamController<int>();
+    var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+    var newController = StreamController<int>();
+    var changed = channel.changeSink((sink) {
+      expect(sink, equals(channel.sink));
+      return newController.sink;
+    });
+
+    expect(newController.stream.toList(), completion(equals([10])));
+    streamController.stream.listen(expectAsync1((_) {}, count: 0));
+
+    changed.sink.add(10);
+    changed.sink.close();
+  });
+
+  group('StreamChannelMixin', () {
+    test('can be used as a mixin', () async {
+      var channel = StreamChannelMixinAsMixin<int>();
+      expect(channel.stream, emitsInOrder([1, 2, 3]));
+      channel.sink
+        ..add(1)
+        ..add(2)
+        ..add(3);
+      await channel.controller.close();
+    });
+
+    test('can be extended', () async {
+      var channel = StreamChannelMixinAsSuperclass<int>();
+      expect(channel.stream, emitsInOrder([1, 2, 3]));
+      channel.sink
+        ..add(1)
+        ..add(2)
+        ..add(3);
+      await channel.controller.close();
+    });
+  });
+}
+
+class StreamChannelMixinAsMixin<T> with StreamChannelMixin<T> {
+  final controller = StreamController<T>();
+
+  @override
+  StreamSink<T> get sink => controller.sink;
+
+  @override
+  Stream<T> get stream => controller.stream;
+}
+
+class StreamChannelMixinAsSuperclass<T> extends StreamChannelMixin<T> {
+  final controller = StreamController<T>();
+
+  @override
+  StreamSink<T> get sink => controller.sink;
+
+  @override
+  Stream<T> get stream => controller.stream;
+}
diff --git a/pkgs/stream_channel/test/with_close_guarantee_test.dart b/pkgs/stream_channel/test/with_close_guarantee_test.dart
new file mode 100644
index 0000000..9c0b729
--- /dev/null
+++ b/pkgs/stream_channel/test/with_close_guarantee_test.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+final _delayTransformer = StreamTransformer.fromHandlers(
+    handleData: (data, sink) => Future.microtask(() => sink.add(data)),
+    handleDone: (sink) => Future.microtask(() => sink.close()));
+
+final _delaySinkTransformer =
+    StreamSinkTransformer.fromStreamTransformer(_delayTransformer);
+
+void main() {
+  late StreamChannelController controller;
+  late StreamChannel channel;
+  setUp(() {
+    controller = StreamChannelController();
+
+    // Add a bunch of layers of asynchronous dispatch between the channel and
+    // the underlying controllers.
+    var stream = controller.foreign.stream;
+    var sink = controller.foreign.sink;
+    for (var i = 0; i < 10; i++) {
+      stream = stream.transform(_delayTransformer);
+      sink = _delaySinkTransformer.bind(sink);
+    }
+
+    channel = StreamChannel.withCloseGuarantee(stream, sink);
+  });
+
+  test(
+      'closing the event sink causes the stream to close before it emits any '
+      'more events', () async {
+    controller.local.sink.add(1);
+    controller.local.sink.add(2);
+    controller.local.sink.add(3);
+
+    expect(
+        channel.stream
+            .listen(expectAsync1((event) {
+              if (event == 2) channel.sink.close();
+            }, count: 2))
+            .asFuture<void>(),
+        completes);
+
+    await pumpEventQueue();
+  });
+
+  test(
+      'closing the event sink before events are emitted causes the stream to '
+      'close immediately', () async {
+    unawaited(channel.sink.close());
+    channel.stream.listen(expectAsync1((_) {}, count: 0),
+        onError: expectAsync2((_, __) {}, count: 0),
+        onDone: expectAsync0(() {}));
+
+    controller.local.sink.add(1);
+    controller.local.sink.add(2);
+    controller.local.sink.add(3);
+    unawaited(controller.local.sink.close());
+
+    await pumpEventQueue();
+  });
+}
diff --git a/pkgs/stream_channel/test/with_guarantees_test.dart b/pkgs/stream_channel/test/with_guarantees_test.dart
new file mode 100644
index 0000000..f026079
--- /dev/null
+++ b/pkgs/stream_channel/test/with_guarantees_test.dart
@@ -0,0 +1,200 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late StreamController streamController;
+  late StreamController sinkController;
+  late StreamChannel channel;
+  setUp(() {
+    streamController = StreamController<void>();
+    sinkController = StreamController<void>();
+    channel = StreamChannel.withGuarantees(
+        streamController.stream, sinkController.sink);
+  });
+
+  group('with a broadcast stream', () {
+    setUp(() {
+      streamController = StreamController.broadcast();
+      channel = StreamChannel.withGuarantees(
+          streamController.stream, sinkController.sink);
+    });
+
+    test('buffers events', () async {
+      streamController.add(1);
+      streamController.add(2);
+      streamController.add(3);
+      await pumpEventQueue();
+
+      expect(channel.stream.toList(), completion(equals([1, 2, 3])));
+      unawaited(streamController.close());
+    });
+
+    test('only allows a single subscription', () {
+      channel.stream.listen(null);
+      expect(() => channel.stream.listen(null), throwsStateError);
+    });
+  });
+
+  test(
+      'closing the event sink causes the stream to close before it emits any '
+      'more events', () {
+    streamController.add(1);
+    streamController.add(2);
+    streamController.add(3);
+
+    expect(
+        channel.stream
+            .listen(expectAsync1((event) {
+              if (event == 2) channel.sink.close();
+            }, count: 2))
+            .asFuture<void>(),
+        completes);
+  });
+
+  test('after the stream closes, the sink ignores events', () async {
+    unawaited(streamController.close());
+
+    // Wait for the done event to be delivered.
+    await channel.stream.toList();
+    channel.sink.add(1);
+    channel.sink.add(2);
+    channel.sink.add(3);
+    unawaited(channel.sink.close());
+
+    // None of our channel.sink additions should make it to the other endpoint.
+    sinkController.stream.listen(expectAsync1((_) {}, count: 0),
+        onDone: expectAsync0(() {}, count: 0));
+    await pumpEventQueue();
+  });
+
+  test("canceling the stream's subscription has no effect on the sink",
+      () async {
+    unawaited(channel.stream.listen(null).cancel());
+    await pumpEventQueue();
+
+    channel.sink.add(1);
+    channel.sink.add(2);
+    channel.sink.add(3);
+    unawaited(channel.sink.close());
+    expect(sinkController.stream.toList(), completion(equals([1, 2, 3])));
+  });
+
+  test("canceling the stream's subscription doesn't stop a done event",
+      () async {
+    unawaited(channel.stream.listen(null).cancel());
+    await pumpEventQueue();
+
+    unawaited(streamController.close());
+    await pumpEventQueue();
+
+    channel.sink.add(1);
+    channel.sink.add(2);
+    channel.sink.add(3);
+    unawaited(channel.sink.close());
+
+    // The sink should be ignoring events because the stream closed.
+    sinkController.stream.listen(expectAsync1((_) {}, count: 0),
+        onDone: expectAsync0(() {}, count: 0));
+    await pumpEventQueue();
+  });
+
+  test('forwards errors to the other endpoint', () {
+    channel.sink.addError('error');
+    expect(sinkController.stream.first, throwsA('error'));
+  });
+
+  test('Sink.done completes once the stream is done', () {
+    channel.stream.listen(null);
+    expect(channel.sink.done, completes);
+    streamController.close();
+  });
+
+  test("events can't be added to an explicitly-closed sink", () {
+    sinkController.stream.listen(null); // Work around sdk#19095.
+
+    expect(channel.sink.close(), completes);
+    expect(() => channel.sink.add(1), throwsStateError);
+    expect(() => channel.sink.addError('oh no'), throwsStateError);
+    expect(() => channel.sink.addStream(Stream.fromIterable([])),
+        throwsStateError);
+  });
+
+  test("events can't be added while a stream is being added", () {
+    var controller = StreamController<void>();
+    channel.sink.addStream(controller.stream);
+
+    expect(() => channel.sink.add(1), throwsStateError);
+    expect(() => channel.sink.addError('oh no'), throwsStateError);
+    expect(() => channel.sink.addStream(Stream.fromIterable([])),
+        throwsStateError);
+    expect(() => channel.sink.close(), throwsStateError);
+
+    controller.close();
+  });
+
+  group('with allowSinkErrors: false', () {
+    setUp(() {
+      streamController = StreamController<void>();
+      sinkController = StreamController<void>();
+      channel = StreamChannel.withGuarantees(
+          streamController.stream, sinkController.sink,
+          allowSinkErrors: false);
+    });
+
+    test('forwards errors to Sink.done but not the stream', () {
+      channel.sink.addError('oh no');
+      expect(channel.sink.done, throwsA('oh no'));
+      sinkController.stream
+          .listen(null, onError: expectAsync1((dynamic _) {}, count: 0));
+    });
+
+    test('adding an error causes the stream to emit a done event', () {
+      expect(channel.sink.done, throwsA('oh no'));
+
+      streamController.add(1);
+      streamController.add(2);
+      streamController.add(3);
+
+      expect(
+          channel.stream
+              .listen(expectAsync1((event) {
+                if (event == 2) channel.sink.addError('oh no');
+              }, count: 2))
+              .asFuture<void>(),
+          completes);
+    });
+
+    test('adding an error closes the inner sink', () {
+      channel.sink.addError('oh no');
+      expect(channel.sink.done, throwsA('oh no'));
+      expect(sinkController.stream.toList(), completion(isEmpty));
+    });
+
+    test(
+        'adding an error via via addStream causes the stream to emit a done '
+        'event', () async {
+      var canceled = false;
+      var controller = StreamController<void>(onCancel: () {
+        canceled = true;
+      });
+
+      // This future shouldn't get the error, because it's sent to [Sink.done].
+      expect(channel.sink.addStream(controller.stream), completes);
+
+      controller.addError('oh no');
+      expect(channel.sink.done, throwsA('oh no'));
+      await pumpEventQueue();
+      expect(canceled, isTrue);
+
+      // Even though the sink is closed, this shouldn't throw an error because
+      // the user didn't explicitly close it.
+      channel.sink.add(1);
+    });
+  });
+}
diff --git a/pkgs/stream_transform/.gitignore b/pkgs/stream_transform/.gitignore
new file mode 100644
index 0000000..bfffcc6
--- /dev/null
+++ b/pkgs/stream_transform/.gitignore
@@ -0,0 +1,6 @@
+.pub/
+.dart_tool/
+build/
+packages
+pubspec.lock
+.packages
diff --git a/pkgs/stream_transform/CHANGELOG.md b/pkgs/stream_transform/CHANGELOG.md
new file mode 100644
index 0000000..b09778b
--- /dev/null
+++ b/pkgs/stream_transform/CHANGELOG.md
@@ -0,0 +1,189 @@
+## 2.1.2-wip
+
+- Require Dart 3.4 or greater.
+
+## 2.1.1
+
+- Require Dart 3.1 or greater
+- Forward errors from the `trigger` future through to the result stream in
+  `takeUntil`. Previously an error would have not closed the stream, and instead
+  raised as an unhandled async error.
+- Move to `dart-lang/tools` monorepo.
+
+## 2.1.0
+
+- Add `whereNotNull`.
+
+## 2.0.1
+
+- Require Dart 2.14 or greater.
+- Wait for the future returned from `StreamSubscription.cancel()` before
+  listening to the subsequent stream in `switchLatest` and `switchMap`.
+
+## 2.0.0
+
+- Migrate to null safety.
+- Improve tests of `switchMap` and improve documentation with links and
+  clarification.
+- Add `trailing` argument to `throttle`.
+
+## 1.2.0
+
+-  Add support for emitting the "leading" event in `debounce`.
+
+## 1.1.1
+
+-   Fix a bug in `asyncMapSample`, `buffer`, `combineLatest`,
+    `combineLatestAll`, `merge`, and `mergeAll` which would cause an exception
+    when cancelling a subscription after using the transformer if the original
+    stream(s) returned `null` from cancelling their subscriptions.
+
+## 1.1.0
+
+-   Add `concurrentAsyncExpand` to interleave events emitted by multiple sub
+    streams created by a callback.
+
+## 1.0.0
+
+-   Remove the top level methods and retain the extensions only.
+
+## 0.0.20
+
+-   Add extension methods for most transformers. These should be used in place
+    of the current methods. All current implementations are deprecated and will
+    be removed in the next major version bump.
+    -   Migrating typical use: Instead of
+        `stream.transform(debounce(Duration(seconds: 1)))` use
+        `stream.debounce(Duration(seconds: 1))`.
+    -   To migrate a usage where a `StreamTransformer` instance is stored or
+        passed see "Getting a StreamTransformer instance" on the README.
+-   The `map` and `chainTransformers` utilities are no longer useful with the
+    new patterns so they are deprecated without a replacement. If you still have
+    a need for them they can be replicated with `StreamTransformer.fromBind`:
+
+    ```
+    // Replace `map(convert)`
+    StreamTransformer.fromBind((s) => s.map(convert));
+
+    // Replace `chainTransformers(first, second)`
+    StreamTransformer.fromBind((s) => s.transform(first).transform(second));
+    ```
+
+## 0.0.19
+
+- Add `asyncMapSample` transform.
+
+## 0.0.18
+
+- Internal cleanup. Passed "trigger" streams or futures now allow `<void>`
+  generic type rather than an implicit `dynamic>`
+
+## 0.0.17
+
+- Add concrete types to the `onError` callback in `tap`.
+
+## 0.0.16+1
+
+- Remove usage of Set literal which is not available before Dart 2.2.0
+
+## 0.0.16
+
+- Allow a `combine` callback to return a `FutureOr<T>` in `scan`. There are no
+  behavior changes for synchronous callbacks. **Potential breaking change** In
+  the unlikely situation where `scan` was used to produce a `Stream<Future>`
+  inference may now fail and require explicit generic type arguments.
+- Add `combineLatest`.
+- Add `combineLatestAll`.
+
+## 0.0.15
+
+- Add `whereType`.
+
+## 0.0.14+1
+
+- Allow using non-dev Dart 2 SDK.
+
+## 0.0.14
+
+- `asyncWhere` will now forward exceptions thrown by the callback through the
+  result Stream.
+- Added `concurrentAsyncMap`.
+
+## 0.0.13
+
+- `mergeAll` now accepts an `Iterable<Stream>` instead of only `List<Stream>`.
+
+## 0.0.12
+
+- Add `chainTransformers` and `map` for use cases where `StreamTransformer`
+  instances are stored as variables or passed to methods other than `transform`.
+
+## 0.0.11
+
+- Renamed `concat` as `followedBy` to match the naming of `Iterable.followedBy`.
+  `concat` is now deprecated.
+
+## 0.0.10
+
+- Updates to support Dart 2.0 core library changes (wave
+  2.2). See [issue 31847][sdk#31847] for details.
+
+  [sdk#31847]: https://github.com/dart-lang/sdk/issues/31847
+
+## 0.0.9
+
+- Add `asyncMapBuffer`.
+
+## 0.0.8
+
+- Add `takeUntil`.
+
+## 0.0.7
+
+- Bug Fix: Streams produced with `scan` and `switchMap` now correctly report
+  `isBroadcast`.
+- Add `startWith`, `startWithMany`, and `startWithStream`.
+
+## 0.0.6
+
+- Bug Fix: Some transformers did not correctly add data to all listeners on
+  broadcast streams. Fixed for `throttle`, `debounce`, `asyncWhere` and `audit`.
+- Bug Fix: Only call the `tap` data callback once per event rather than once per
+  listener.
+- Bug Fix: Allow canceling and re-listening to broadcast streams after a
+  `merge` transform.
+- Bug Fix: Broadcast streams which are buffered using a single-subscription
+  trigger can be canceled and re-listened.
+- Bug Fix: Buffer outputs one more value if there is a pending trigger before
+  the trigger closes.
+- Bug Fix: Single-subscription streams concatted after broadcast streams are
+  handled correctly.
+- Use sync `StreamControllers` for forwarding where possible.
+
+## 0.0.5
+
+- Bug Fix: Allow compiling switchLatest with Dart2Js.
+- Add `asyncWhere`: Like `where` but allows an asynchronous predicate.
+
+## 0.0.4
+- Add `scan`: fold which returns intermediate values
+- Add `throttle`: block events for a duration after emitting a value
+- Add `audit`: emits the last event received after a duration
+
+## 0.0.3
+
+- Add `tap`: React to values as they pass without being a subscriber on a stream
+- Add `switchMap` and `switchLatest`: Flatten a Stream of Streams into a Stream
+  which forwards values from the most recent Stream
+
+## 0.0.2
+
+- Add `concat`: Appends streams in series
+- Add `merge` and `mergeAll`: Interleaves streams
+
+## 0.0.1
+
+- Initial release with the following utilities:
+  - `buffer`: Collects events in a `List` until a `trigger` stream fires.
+  - `debounce`, `debounceBuffer`: Collect or drop events which occur closer in
+    time than a given duration.
diff --git a/pkgs/stream_transform/LICENSE b/pkgs/stream_transform/LICENSE
new file mode 100644
index 0000000..03af64a
--- /dev/null
+++ b/pkgs/stream_transform/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2017, 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/stream_transform/README.md b/pkgs/stream_transform/README.md
new file mode 100644
index 0000000..e7049bd
--- /dev/null
+++ b/pkgs/stream_transform/README.md
@@ -0,0 +1,141 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/stream_transform.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/stream_transform.yaml)
+[![pub package](https://img.shields.io/pub/v/stream_transform.svg)](https://pub.dev/packages/stream_transform)
+[![package publisher](https://img.shields.io/pub/publisher/stream_transform.svg)](https://pub.dev/packages/stream_transform/publisher)
+
+Extension methods on `Stream` adding common transform operators.
+
+## Operators
+
+### asyncMapBuffer, asyncMapSample, concurrentAsyncMap
+
+Alternatives to `asyncMap`. `asyncMapBuffer` prevents the callback from
+overlapping execution and collects events while it is executing.
+`asyncMapSample` prevents overlapping execution and discards events while it is
+executing. `concurrentAsyncMap` allows overlap and removes ordering guarantees
+for higher throughput.
+
+Like `asyncMap` but events are buffered in a List until previous events have
+been processed rather than being called for each element individually.
+
+### asyncWhere
+
+Like `where` but allows an asynchronous predicate.
+
+### audit
+
+Waits for a period of time after receiving a value and then only emits the most
+recent value.
+
+### buffer
+
+Collects values from a source stream until a `trigger` stream fires and the
+collected values are emitted.
+
+### combineLatest, combineLatestAll
+
+Combine the most recent event from multiple streams through a callback or into a
+list.
+
+### debounce, debounceBuffer
+
+Prevents a source stream from emitting too frequently by dropping or collecting
+values that occur within a given duration.
+
+### followedBy
+
+Appends the values of a stream after another stream finishes.
+
+### merge, mergeAll, concurrentAsyncExpand
+
+Interleaves events from multiple streams into a single stream.
+
+### scan
+
+Scan is like fold, but instead of producing a single value it yields each
+intermediate accumulation.
+
+### startWith, startWithMany, startWithStream
+
+Prepend a value, an iterable, or a stream to the beginning of another stream.
+
+### switchMap, switchLatest
+
+Flatten a Stream of Streams into a Stream which forwards values from the most
+recent Stream
+
+### takeUntil
+
+Let values through until a Future fires.
+
+### tap
+
+Taps into a single-subscriber stream to react to values as they pass, without
+being a real subscriber.
+
+### throttle
+
+Blocks events for a duration after an event is successfully emitted.
+
+### whereType
+
+Like `Iterable.whereType` for a stream.
+
+## Comparison to Rx Operators
+
+The semantics and naming in this package have some overlap, and some conflict,
+with the [ReactiveX](https://reactivex.io/) suite of libraries. Some of the
+conflict is intentional - Dart `Stream` predates `Observable` and coherence with
+the Dart ecosystem semantics and naming is a strictly higher priority than
+consistency with ReactiveX.
+
+Rx Operator Category      | variation                                              | `stream_transform`
+------------------------- | ------------------------------------------------------ | ------------------
+[`sample`][rx_sample]     | `sample/throttleLast(Duration)`                        | `sample(Stream.periodic(Duration), longPoll: false)`
+&#x200B;                  | `throttleFirst(Duration)`                              | [`throttle`][throttle]
+&#x200B;                  | `sample(Observable)`                                   | `sample(trigger, longPoll: false)`
+[`debounce`][rx_debounce] | `debounce/throttleWithTimeout(Duration)`               | [`debounce`][debounce]
+&#x200B;                  | `debounce(Observable)`                                 | No equivalent
+[`buffer`][rx_buffer]     | `buffer(boundary)`, `bufferWithTime`,`bufferWithCount` | No equivalent
+&#x200B;                  | `buffer(boundaryClosingSelector)`                      | `buffer(trigger, longPoll: false)`
+RxJs extensions           | [`audit(callback)`][rxjs_audit]                        | No equivalent
+&#x200B;                  | [`auditTime(Duration)`][rxjs_auditTime]                | [`audit`][audit]
+&#x200B;                  | [`exhaustMap`][rxjs_exhaustMap]                        | No equivalent
+&#x200B;                  | [`throttleTime(trailing: true)`][rxjs_throttleTime]    | `throttle(trailing: true)`
+&#x200B;                  | `throttleTime(leading: false, trailing: true)`         | No equivalent
+No equivalent?            |                                                        | [`asyncMapBuffer`][asyncMapBuffer]
+&#x200B;                  |                                                        | [`asyncMapSample`][asyncMapSample]
+&#x200B;                  |                                                        | [`buffer`][buffer]
+&#x200B;                  |                                                        | [`sample`][sample]
+&#x200B;                  |                                                        | [`debounceBuffer`][debounceBuffer]
+&#x200B;                  |                                                        | `debounce(leading: true, trailing: false)`
+&#x200B;                  |                                                        | `debounce(leading: true, trailing: true)`
+
+[rx_sample]:https://reactivex.io/documentation/operators/sample.html
+[rx_debounce]:https://reactivex.io/documentation/operators/debounce.html
+[rx_buffer]:https://reactivex.io/documentation/operators/buffer.html
+[rxjs_audit]:https://rxjs.dev/api/operators/audit
+[rxjs_auditTime]:https://rxjs.dev/api/operators/auditTime
+[rxjs_throttleTime]:https://rxjs.dev/api/operators/throttleTime
+[rxjs_exhaustMap]:https://rxjs.dev/api/operators/exhaustMap
+[asyncMapBuffer]:https://pub.dev/documentation/stream_transform/latest/stream_transform/AsyncMap/asyncMapBuffer.html
+[asyncMapSample]:https://pub.dev/documentation/stream_transform/latest/stream_transform/AsyncMap/asyncMapSample.html
+[audit]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/audit.html
+[buffer]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/buffer.html
+[sample]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/sample.html
+[debounceBuffer]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounceBuffer.html
+[debounce]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounce.html
+[throttle]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/throttle.html
+
+## Getting a `StreamTransformer` instance
+
+It may be useful to pass an instance of `StreamTransformer` so that it can be
+used with `stream.transform` calls rather than reference the specific operator
+in place. Any operator on `Stream` that returns a `Stream` can be modeled as a
+`StreamTransformer` using the [`fromBind` constructor][fromBind].
+
+```dart
+final debounce = StreamTransformer.fromBind(
+    (s) => s.debounce(const Duration(milliseconds: 100)));
+```
+
+[fromBind]: https://api.dart.dev/stable/dart-async/StreamTransformer/StreamTransformer.fromBind.html
diff --git a/pkgs/stream_transform/analysis_options.yaml b/pkgs/stream_transform/analysis_options.yaml
new file mode 100644
index 0000000..05f1af1
--- /dev/null
+++ b/pkgs/stream_transform/analysis_options.yaml
@@ -0,0 +1,16 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+    strict-raw-types: true
+
+linter:
+  rules:
+    - avoid_bool_literals_in_conditional_expressions
+    - avoid_classes_with_only_static_members
+    - avoid_returning_this
+    - avoid_unused_constructor_parameters
+    - cascade_invocations
+    - join_return_with_assignment
+    - no_adjacent_strings_in_list
diff --git a/pkgs/stream_transform/example/index.html b/pkgs/stream_transform/example/index.html
new file mode 100644
index 0000000..aecdc09
--- /dev/null
+++ b/pkgs/stream_transform/example/index.html
@@ -0,0 +1,11 @@
+<html>
+  <head>
+    <script defer src="main.dart.js" type="application/javascript"></script>
+  </head>
+  <body>
+    <input id="first_input"><br>
+    <input id="second_input"><br>
+    <p id="output">
+    </p>
+  </body>
+</html>
diff --git a/pkgs/stream_transform/example/main.dart b/pkgs/stream_transform/example/main.dart
new file mode 100644
index 0000000..8224393
--- /dev/null
+++ b/pkgs/stream_transform/example/main.dart
@@ -0,0 +1,25 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:web/web.dart';
+
+void main() {
+  var firstInput = document.querySelector('#first_input') as HTMLInputElement;
+  var secondInput = document.querySelector('#second_input') as HTMLInputElement;
+  var output = document.querySelector('#output')!;
+
+  _inputValues(firstInput)
+      .combineLatest(_inputValues(secondInput),
+          (first, second) => 'First: $first, Second: $second')
+      .tap((v) {
+    print('Saw: $v');
+  }).forEach((v) {
+    output.text = v;
+  });
+}
+
+Stream<String?> _inputValues(HTMLInputElement element) => element.onKeyUp
+    .debounce(const Duration(milliseconds: 100))
+    .map((_) => element.value);
diff --git a/pkgs/stream_transform/lib/src/aggregate_sample.dart b/pkgs/stream_transform/lib/src/aggregate_sample.dart
new file mode 100644
index 0000000..f2ff8ed
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/aggregate_sample.dart
@@ -0,0 +1,146 @@
+// 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:async';
+
+import 'common_callbacks.dart';
+
+extension AggregateSample<T> on Stream<T> {
+  /// Computes a value based on sequences of events, then emits that value when
+  /// [trigger] emits an event.
+  ///
+  /// Every time this stream emits an event, an intermediate value is created
+  /// by combining the new event with the previous intermediate value, or with
+  /// `null` if there is no previous value, using the [aggregate] function.
+  ///
+  /// When [trigger] emits value, the returned stream emits the current
+  /// intermediate value and clears it.
+  ///
+  /// If [longPoll] is `false`, if there is no intermediate value when [trigger]
+  /// emits an event, the [onEmpty] function is called with a [Sink] which can
+  /// add events to the returned stream.
+  ///
+  /// If [longPoll] is `true`, and there is no intermediate value when [trigger]
+  /// emits one or more events, then the *next* event from this stream is
+  /// immediately put through [aggregate] and emitted on the returned stream.
+  /// Subsequent events on [trigger] while there have been no events on this
+  /// stream are ignored.
+  /// In that case, [onEmpty] is never used.
+  ///
+  /// The result stream will close as soon as there is a guarantee it will not
+  /// emit any more events. There will not be any more events emitted if:
+  /// - [trigger] is closed and there is no waiting long poll.
+  /// - Or, the source stream is closed and there are no buffered events.
+  ///
+  /// If the source stream is a broadcast stream, the result will be as well.
+  /// Errors from the source stream or the trigger are immediately forwarded to
+  /// the output.
+  Stream<S> aggregateSample<S>(
+      {required Stream<void> trigger,
+      required S Function(T, S?) aggregate,
+      required bool longPoll,
+      required void Function(Sink<S>) onEmpty}) {
+    var controller = isBroadcast
+        ? StreamController<S>.broadcast(sync: true)
+        : StreamController<S>(sync: true);
+
+    S? currentResults;
+    var hasCurrentResults = false;
+    var activeLongPoll = false;
+    var isTriggerDone = false;
+    var isValueDone = false;
+    StreamSubscription<T>? valueSub;
+    StreamSubscription<void>? triggerSub;
+
+    void emit(S results) {
+      currentResults = null;
+      hasCurrentResults = false;
+      controller.add(results);
+    }
+
+    void onValue(T value) {
+      currentResults = aggregate(value, currentResults);
+      hasCurrentResults = true;
+      if (!longPoll) return;
+
+      if (activeLongPoll) {
+        activeLongPoll = false;
+        emit(currentResults as S);
+      }
+
+      if (isTriggerDone) {
+        valueSub!.cancel();
+        controller.close();
+      }
+    }
+
+    void onValuesDone() {
+      isValueDone = true;
+      if (!hasCurrentResults) {
+        triggerSub?.cancel();
+        controller.close();
+      }
+    }
+
+    void onTrigger(_) {
+      if (hasCurrentResults) {
+        emit(currentResults as S);
+      } else if (longPoll) {
+        activeLongPoll = true;
+      } else {
+        onEmpty(controller);
+      }
+
+      if (isValueDone) {
+        triggerSub!.cancel();
+        controller.close();
+      }
+    }
+
+    void onTriggerDone() {
+      isTriggerDone = true;
+      if (!activeLongPoll) {
+        valueSub?.cancel();
+        controller.close();
+      }
+    }
+
+    controller.onListen = () {
+      assert(valueSub == null);
+      valueSub =
+          listen(onValue, onError: controller.addError, onDone: onValuesDone);
+      final priorTriggerSub = triggerSub;
+      if (priorTriggerSub != null) {
+        if (priorTriggerSub.isPaused) priorTriggerSub.resume();
+      } else {
+        triggerSub = trigger.listen(onTrigger,
+            onError: controller.addError, onDone: onTriggerDone);
+      }
+      if (!isBroadcast) {
+        controller
+          ..onPause = () {
+            valueSub?.pause();
+            triggerSub?.pause();
+          }
+          ..onResume = () {
+            valueSub?.resume();
+            triggerSub?.resume();
+          };
+      }
+      controller.onCancel = () {
+        var cancels = <Future<void>>[if (!isValueDone) valueSub!.cancel()];
+        valueSub = null;
+        if (trigger.isBroadcast || !isBroadcast) {
+          if (!isTriggerDone) cancels.add(triggerSub!.cancel());
+          triggerSub = null;
+        } else {
+          triggerSub!.pause();
+        }
+        if (cancels.isEmpty) return null;
+        return cancels.wait.then(ignoreArgument);
+      };
+    };
+    return controller.stream;
+  }
+}
diff --git a/pkgs/stream_transform/lib/src/async_expand.dart b/pkgs/stream_transform/lib/src/async_expand.dart
new file mode 100644
index 0000000..28d2f40
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/async_expand.dart
@@ -0,0 +1,89 @@
+// Copyright (c) 2022, 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 'common_callbacks.dart';
+import 'switch.dart';
+
+/// Alternatives to [asyncExpand].
+///
+/// The built in [asyncExpand] will not overlap the inner streams and every
+/// event will be sent to the callback individually.
+///
+/// - [concurrentAsyncExpand] allow overlap and merges inner streams without
+///   ordering guarantees.
+extension AsyncExpand<T> on Stream<T> {
+  /// Like [asyncExpand] but the [convert] callback may be called for an element
+  /// before the [Stream] emitted by the previous element has closed.
+  ///
+  /// Events on the result stream will be emitted in the order they are emitted
+  /// by the sub streams, which may not match the order of this stream.
+  ///
+  /// Errors from [convert], the source stream, or any of the sub streams are
+  /// forwarded to the result stream.
+  ///
+  /// The result stream will not close until the source stream closes and all
+  /// sub streams have closed.
+  ///
+  /// If the source stream is a broadcast stream, the result will be as well,
+  /// regardless of the types of streams created by [convert]. In this case,
+  /// some care should be taken:
+  /// -  If [convert] returns a single subscription stream it may be listened to
+  /// and never canceled.
+  /// -  For any period of time where there are no listeners on the result
+  /// stream, any sub streams from previously emitted events will be ignored,
+  /// regardless of whether they emit further events after a listener is added
+  /// back.
+  ///
+  /// See also:
+  /// - [switchMap], which cancels subscriptions to the previous sub stream
+  /// instead of concurrently emitting events from all sub streams.
+  Stream<S> concurrentAsyncExpand<S>(Stream<S> Function(T) convert) {
+    final controller = isBroadcast
+        ? StreamController<S>.broadcast(sync: true)
+        : StreamController<S>(sync: true);
+
+    controller.onListen = () {
+      final subscriptions = <StreamSubscription<dynamic>>[];
+      final outerSubscription = map(convert).listen((inner) {
+        if (isBroadcast && !inner.isBroadcast) {
+          inner = inner.asBroadcastStream();
+        }
+        final subscription =
+            inner.listen(controller.add, onError: controller.addError);
+        subscription.onDone(() {
+          subscriptions.remove(subscription);
+          if (subscriptions.isEmpty) controller.close();
+        });
+        subscriptions.add(subscription);
+      }, onError: controller.addError);
+      outerSubscription.onDone(() {
+        subscriptions.remove(outerSubscription);
+        if (subscriptions.isEmpty) controller.close();
+      });
+      subscriptions.add(outerSubscription);
+      if (!isBroadcast) {
+        controller
+          ..onPause = () {
+            for (final subscription in subscriptions) {
+              subscription.pause();
+            }
+          }
+          ..onResume = () {
+            for (final subscription in subscriptions) {
+              subscription.resume();
+            }
+          };
+      }
+      controller.onCancel = () {
+        if (subscriptions.isEmpty) return null;
+        return [for (var s in subscriptions) s.cancel()]
+            .wait
+            .then(ignoreArgument);
+      };
+    };
+    return controller.stream;
+  }
+}
diff --git a/pkgs/stream_transform/lib/src/async_map.dart b/pkgs/stream_transform/lib/src/async_map.dart
new file mode 100644
index 0000000..094df9c
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/async_map.dart
@@ -0,0 +1,136 @@
+// Copyright (c) 2017, 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 'aggregate_sample.dart';
+import 'common_callbacks.dart';
+import 'from_handlers.dart';
+import 'rate_limit.dart';
+
+/// Alternatives to [asyncMap].
+///
+/// The built in [asyncMap] will not overlap execution of the passed callback,
+/// and every event will be sent to the callback individually.
+///
+/// - [asyncMapBuffer] prevents the callback from overlapping execution and
+///   collects events while it is executing to process in batches.
+/// - [asyncMapSample] prevents overlapping execution and discards events while
+///   it is executing.
+/// - [concurrentAsyncMap] allows overlap and removes ordering guarantees.
+extension AsyncMap<T> on Stream<T> {
+  /// Like [asyncMap] but events are buffered until previous events have been
+  /// processed by [convert].
+  ///
+  /// If this stream is a broadcast stream the result will be as well.
+  /// When used with a broadcast stream behavior also differs from [asyncMap] in
+  /// that the [convert] function is only called once per event, rather than
+  /// once per listener per event.
+  ///
+  /// The first event from this stream is always passed to [convert] as a
+  /// list with a single element.
+  /// After that, events are buffered until the previous Future returned from
+  /// [convert] has completed.
+  ///
+  /// Errors from this stream are forwarded directly to the result stream.
+  /// Errors during the conversion are also forwarded to the result stream and
+  /// are considered completing work so the next values are let through.
+  ///
+  /// The result stream will not close until this stream closes and all pending
+  /// conversions have finished.
+  Stream<S> asyncMapBuffer<S>(Future<S> Function(List<T>) convert) {
+    var workFinished = StreamController<void>()
+      // Let the first event through.
+      ..add(null);
+    return buffer(workFinished.stream)._asyncMapThen(convert, workFinished.add);
+  }
+
+  /// Like [asyncMap] but events are discarded while work is happening in
+  /// [convert].
+  ///
+  /// If this stream is a broadcast stream the result will be as well.
+  /// When used with a broadcast stream behavior also differs from [asyncMap] in
+  /// that the [convert] function is only called once per event, rather than
+  /// once per listener per event.
+  ///
+  /// If no work is happening when an event is emitted it will be immediately
+  /// passed to [convert]. If there is ongoing work when an event is emitted it
+  /// will be held until the work is finished. New events emitted will replace a
+  /// pending event.
+  ///
+  /// Errors from this stream are forwarded directly to the result stream.
+  /// Errors during the conversion are also forwarded to the result stream and
+  /// are considered completing work so the next values are let through.
+  ///
+  /// The result stream will not close until this stream closes and all pending
+  /// conversions have finished.
+  Stream<S> asyncMapSample<S>(Future<S> Function(T) convert) {
+    var workFinished = StreamController<void>()
+      // Let the first event through.
+      ..add(null);
+    return aggregateSample(
+            trigger: workFinished.stream,
+            aggregate: _dropPrevious,
+            longPoll: true,
+            onEmpty: ignoreArgument)
+        ._asyncMapThen(convert, workFinished.add);
+  }
+
+  /// Like [asyncMap] but the [convert] callback may be called for an element
+  /// before processing for the previous element is finished.
+  ///
+  /// Events on the result stream will be emitted in the order that [convert]
+  /// completed which may not match the order of this stream.
+  ///
+  /// If this stream is a broadcast stream the result will be as well.
+  /// When used with a broadcast stream behavior also differs from [asyncMap] in
+  /// that the [convert] function is only called once per event, rather than
+  /// once per listener per event. The [convert] callback won't be called for
+  /// events while a broadcast stream has no listener.
+  ///
+  /// Errors from [convert] or this stream are forwarded directly to the
+  /// result stream.
+  ///
+  /// The result stream will not close until this stream closes and all pending
+  /// conversions have finished.
+  Stream<S> concurrentAsyncMap<S>(FutureOr<S> Function(T) convert) {
+    var valuesWaiting = 0;
+    var sourceDone = false;
+    return transformByHandlers(onData: (element, sink) {
+      valuesWaiting++;
+      () async {
+        try {
+          sink.add(await convert(element));
+        } catch (e, st) {
+          sink.addError(e, st);
+        }
+        valuesWaiting--;
+        if (valuesWaiting <= 0 && sourceDone) sink.close();
+      }();
+    }, onDone: (sink) {
+      sourceDone = true;
+      if (valuesWaiting <= 0) sink.close();
+    });
+  }
+
+  /// Like [Stream.asyncMap] but the [convert] is only called once per event,
+  /// rather than once per listener, and [then] is called after completing the
+  /// work.
+  Stream<S> _asyncMapThen<S>(
+      Future<S> Function(T) convert, void Function(void) then) {
+    Future<void>? pendingEvent;
+    return transformByHandlers(onData: (event, sink) {
+      pendingEvent =
+          convert(event).then(sink.add).catchError(sink.addError).then(then);
+    }, onDone: (sink) {
+      if (pendingEvent != null) {
+        pendingEvent!.then((_) => sink.close());
+      } else {
+        sink.close();
+      }
+    });
+  }
+}
+
+T _dropPrevious<T>(T event, _) => event;
diff --git a/pkgs/stream_transform/lib/src/combine_latest.dart b/pkgs/stream_transform/lib/src/combine_latest.dart
new file mode 100644
index 0000000..f02a19e
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/combine_latest.dart
@@ -0,0 +1,240 @@
+// 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:async';
+
+import 'common_callbacks.dart';
+
+/// Utilities to combine events from multiple streams through a callback or into
+/// a list.
+extension CombineLatest<T> on Stream<T> {
+  /// Combines the latest values from this stream with the latest values from
+  /// [other] using [combine].
+  ///
+  /// No event will be emitted until both the source stream and [other] have
+  /// each emitted at least one event. If either the source stream or [other]
+  /// emit multiple events before the other emits the first event, all but the
+  /// last value will be discarded. Once both streams have emitted at least
+  /// once, the result stream will emit any time either input stream emits.
+  ///
+  /// The result stream will not close until both the source stream and [other]
+  /// have closed.
+  ///
+  /// For example:
+  ///
+  ///     source.combineLatest(other, (a, b) => a + b);
+  ///
+  ///     source: --1--2--------4--|
+  ///     other:  -------3--|
+  ///     result: -------5------7--|
+  ///
+  /// Errors thrown by [combine], along with any errors on the source stream or
+  /// [other], are forwarded to the result stream.
+  ///
+  /// If the source stream is a broadcast stream, the result stream will be as
+  /// well, regardless of [other]'s type. If a single subscription stream is
+  /// combined with a broadcast stream it may never be canceled.
+  Stream<S> combineLatest<T2, S>(
+      Stream<T2> other, FutureOr<S> Function(T, T2) combine) {
+    final controller = isBroadcast
+        ? StreamController<S>.broadcast(sync: true)
+        : StreamController<S>(sync: true);
+
+    other =
+        (isBroadcast && !other.isBroadcast) ? other.asBroadcastStream() : other;
+
+    StreamSubscription<T>? sourceSubscription;
+    StreamSubscription<T2>? otherSubscription;
+
+    var sourceDone = false;
+    var otherDone = false;
+
+    late T latestSource;
+    late T2 latestOther;
+
+    var sourceStarted = false;
+    var otherStarted = false;
+
+    void emitCombined() {
+      if (!sourceStarted || !otherStarted) return;
+      FutureOr<S> result;
+      try {
+        result = combine(latestSource, latestOther);
+      } catch (e, s) {
+        controller.addError(e, s);
+        return;
+      }
+      if (result is Future<S>) {
+        sourceSubscription!.pause();
+        otherSubscription!.pause();
+        result
+            .then(controller.add, onError: controller.addError)
+            .whenComplete(() {
+          sourceSubscription!.resume();
+          otherSubscription!.resume();
+        });
+      } else {
+        controller.add(result);
+      }
+    }
+
+    controller.onListen = () {
+      assert(sourceSubscription == null);
+      sourceSubscription = listen(
+          (s) {
+            sourceStarted = true;
+            latestSource = s;
+            emitCombined();
+          },
+          onError: controller.addError,
+          onDone: () {
+            sourceDone = true;
+            if (otherDone) {
+              controller.close();
+            } else if (!sourceStarted) {
+              // Nothing can ever be emitted
+              otherSubscription!.cancel();
+              controller.close();
+            }
+          });
+      otherSubscription = other.listen(
+          (o) {
+            otherStarted = true;
+            latestOther = o;
+            emitCombined();
+          },
+          onError: controller.addError,
+          onDone: () {
+            otherDone = true;
+            if (sourceDone) {
+              controller.close();
+            } else if (!otherStarted) {
+              // Nothing can ever be emitted
+              sourceSubscription!.cancel();
+              controller.close();
+            }
+          });
+      if (!isBroadcast) {
+        controller
+          ..onPause = () {
+            sourceSubscription!.pause();
+            otherSubscription!.pause();
+          }
+          ..onResume = () {
+            sourceSubscription!.resume();
+            otherSubscription!.resume();
+          };
+      }
+      controller.onCancel = () {
+        var cancels = [
+          sourceSubscription!.cancel(),
+          otherSubscription!.cancel()
+        ];
+        sourceSubscription = null;
+        otherSubscription = null;
+        return cancels.wait.then(ignoreArgument);
+      };
+    };
+    return controller.stream;
+  }
+
+  /// Combine the latest value emitted from the source stream with the latest
+  /// values emitted from [others].
+  ///
+  /// [combineLatestAll] subscribes to the source stream and [others] and when
+  /// any one of the streams emits, the result stream will emit a [List<T>] of
+  /// the latest values emitted from all streams.
+  ///
+  /// No event will be emitted until all source streams emit at least once. If a
+  /// source stream emits multiple values before another starts emitting, all
+  /// but the last value will be discarded. Once all source streams have emitted
+  /// at least once, the result stream will emit any time any source stream
+  /// emits.
+  ///
+  /// The result stream will not close until all source streams have closed.
+  /// When a source stream closes, the result stream will continue to emit the
+  /// last value from the closed stream when the other source streams emit until
+  /// the result stream has closed. If a source stream closes without emitting
+  /// any value, the result stream will close as well.
+  ///
+  /// For example:
+  ///
+  ///     final combined = first
+  ///         .combineLatestAll([second, third])
+  ///         .map((data) => data.join());
+  ///
+  ///     first:    a----b------------------c--------d---|
+  ///     second:   --1---------2-----------------|
+  ///     third:    -------&----------%---|
+  ///     combined: -------b1&--b2&---b2%---c2%------d2%-|
+  ///
+  /// Errors thrown by any source stream will be forwarded to the result stream.
+  ///
+  /// If the source stream is a broadcast stream, the result stream will be as
+  /// well, regardless of the types of [others]. If a single subscription stream
+  /// is combined with a broadcast source stream, it may never be canceled.
+  Stream<List<T>> combineLatestAll(Iterable<Stream<T>> others) {
+    final controller = isBroadcast
+        ? StreamController<List<T>>.broadcast(sync: true)
+        : StreamController<List<T>>(sync: true);
+
+    final allStreams = [
+      this,
+      for (final other in others)
+        !isBroadcast || other.isBroadcast ? other : other.asBroadcastStream(),
+    ];
+
+    controller.onListen = () {
+      final subscriptions = <StreamSubscription<T>>[];
+
+      final latestData = List<T?>.filled(allStreams.length, null);
+      final hasEmitted = <int>{};
+      void handleData(int index, T data) {
+        latestData[index] = data;
+        hasEmitted.add(index);
+        if (hasEmitted.length == allStreams.length) {
+          controller.add(List.from(latestData));
+        }
+      }
+
+      var streamId = 0;
+      for (final stream in allStreams) {
+        final index = streamId;
+
+        final subscription = stream.listen((data) => handleData(index, data),
+            onError: controller.addError);
+        subscription.onDone(() {
+          assert(subscriptions.contains(subscription));
+          subscriptions.remove(subscription);
+          if (subscriptions.isEmpty || !hasEmitted.contains(index)) {
+            controller.close();
+          }
+        });
+        subscriptions.add(subscription);
+
+        streamId++;
+      }
+      if (!isBroadcast) {
+        controller
+          ..onPause = () {
+            for (final subscription in subscriptions) {
+              subscription.pause();
+            }
+          }
+          ..onResume = () {
+            for (final subscription in subscriptions) {
+              subscription.resume();
+            }
+          };
+      }
+      controller.onCancel = () {
+        if (subscriptions.isEmpty) return null;
+        return [for (var s in subscriptions) s.cancel()]
+            .wait
+            .then(ignoreArgument);
+      };
+    };
+    return controller.stream;
+  }
+}
diff --git a/pkgs/stream_transform/lib/src/common_callbacks.dart b/pkgs/stream_transform/lib/src/common_callbacks.dart
new file mode 100644
index 0000000..e211cf9
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/common_callbacks.dart
@@ -0,0 +1,5 @@
+// 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.
+
+void ignoreArgument(Object? _) {}
diff --git a/pkgs/stream_transform/lib/src/concatenate.dart b/pkgs/stream_transform/lib/src/concatenate.dart
new file mode 100644
index 0000000..0330dd7
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/concatenate.dart
@@ -0,0 +1,112 @@
+// Copyright (c) 2017, 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';
+
+/// Utilities to append or prepend to a stream.
+extension Concatenate<T> on Stream<T> {
+  /// Emits all values and errors from [next] following all values and errors
+  /// from this stream.
+  ///
+  /// If this stream never finishes, the [next] stream will never get a
+  /// listener.
+  ///
+  /// If this stream is a broadcast stream, the result will be as well.
+  /// If a single-subscription follows a broadcast stream it may be listened
+  /// to and never canceled since there may be broadcast listeners added later.
+  ///
+  /// If a broadcast stream follows any other stream it will miss any events or
+  /// errors which occur before this stream is done.
+  /// If a broadcast stream follows a single-subscription stream, pausing the
+  /// stream while it is listening to the second stream will cause events to be
+  /// dropped rather than buffered.
+  Stream<T> followedBy(Stream<T> next) {
+    var controller = isBroadcast
+        ? StreamController<T>.broadcast(sync: true)
+        : StreamController<T>(sync: true);
+
+    next = isBroadcast && !next.isBroadcast ? next.asBroadcastStream() : next;
+
+    StreamSubscription<T>? subscription;
+    var currentStream = this;
+    var thisDone = false;
+    var secondDone = false;
+
+    late void Function() currentDoneHandler;
+
+    void listen() {
+      subscription = currentStream.listen(controller.add,
+          onError: controller.addError, onDone: () => currentDoneHandler());
+    }
+
+    void onSecondDone() {
+      secondDone = true;
+      controller.close();
+    }
+
+    void onThisDone() {
+      thisDone = true;
+      currentStream = next;
+      currentDoneHandler = onSecondDone;
+      listen();
+    }
+
+    currentDoneHandler = onThisDone;
+
+    controller.onListen = () {
+      assert(subscription == null);
+      listen();
+      if (!isBroadcast) {
+        controller
+          ..onPause = () {
+            if (!thisDone || !next.isBroadcast) return subscription!.pause();
+            subscription!.cancel();
+            subscription = null;
+          }
+          ..onResume = () {
+            if (!thisDone || !next.isBroadcast) return subscription!.resume();
+            listen();
+          };
+      }
+      controller.onCancel = () {
+        if (secondDone) return null;
+        var toCancel = subscription!;
+        subscription = null;
+        return toCancel.cancel();
+      };
+    };
+    return controller.stream;
+  }
+
+  /// Emits [initial] before any values or errors from the this stream.
+  ///
+  /// If this stream is a broadcast stream the result will be as well.
+  /// If this stream is a broadcast stream, the returned stream will only
+  /// contain events of this stream that are emitted after the [initial] value
+  /// has been emitted on the returned stream.
+  Stream<T> startWith(T initial) =>
+      startWithStream(Future.value(initial).asStream());
+
+  /// Emits all values in [initial] before any values or errors from this
+  /// stream.
+  ///
+  /// If this stream is a broadcast stream the result will be as well.
+  /// If this stream is a broadcast stream it will miss any events which
+  /// occur before the initial values are all emitted.
+  Stream<T> startWithMany(Iterable<T> initial) =>
+      startWithStream(Stream.fromIterable(initial));
+
+  /// Emits all values and errors in [initial] before any values or errors from
+  /// this stream.
+  ///
+  /// If this stream is a broadcast stream the result will be as well.
+  /// If this stream is a broadcast stream it will miss any events which occur
+  /// before [initial] closes.
+  Stream<T> startWithStream(Stream<T> initial) {
+    if (isBroadcast && !initial.isBroadcast) {
+      initial = initial.asBroadcastStream();
+    }
+    return initial.followedBy(this);
+  }
+}
diff --git a/pkgs/stream_transform/lib/src/from_handlers.dart b/pkgs/stream_transform/lib/src/from_handlers.dart
new file mode 100644
index 0000000..1146a13
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/from_handlers.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2017, 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';
+
+extension TransformByHandlers<S> on Stream<S> {
+  /// Transform a stream by callbacks.
+  ///
+  /// This is similar to `transform(StreamTransformer.fromHandler(...))` except
+  /// that the handlers are called once per event rather than called for the
+  /// same event for each listener on a broadcast stream.
+  Stream<T> transformByHandlers<T>(
+      {required void Function(S, EventSink<T>) onData,
+      void Function(Object, StackTrace, EventSink<T>)? onError,
+      void Function(EventSink<T>)? onDone}) {
+    final handleError = onError ?? _defaultHandleError;
+    final handleDone = onDone ?? _defaultHandleDone;
+
+    var controller = isBroadcast
+        ? StreamController<T>.broadcast(sync: true)
+        : StreamController<T>(sync: true);
+
+    StreamSubscription<S>? subscription;
+    controller.onListen = () {
+      assert(subscription == null);
+      var valuesDone = false;
+      subscription = listen((value) => onData(value, controller),
+          onError: (Object error, StackTrace stackTrace) {
+        handleError(error, stackTrace, controller);
+      }, onDone: () {
+        valuesDone = true;
+        handleDone(controller);
+      });
+      if (!isBroadcast) {
+        controller
+          ..onPause = subscription!.pause
+          ..onResume = subscription!.resume;
+      }
+      controller.onCancel = () {
+        var toCancel = subscription;
+        subscription = null;
+        if (!valuesDone) return toCancel!.cancel();
+        return null;
+      };
+    };
+    return controller.stream;
+  }
+
+  static void _defaultHandleError<T>(
+      Object error, StackTrace stackTrace, EventSink<T> sink) {
+    sink.addError(error, stackTrace);
+  }
+
+  static void _defaultHandleDone<T>(EventSink<T> sink) {
+    sink.close();
+  }
+}
diff --git a/pkgs/stream_transform/lib/src/merge.dart b/pkgs/stream_transform/lib/src/merge.dart
new file mode 100644
index 0000000..3bfe06c
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/merge.dart
@@ -0,0 +1,102 @@
+// Copyright (c) 2017, 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 'common_callbacks.dart';
+
+/// Utilities to interleave events from multiple streams.
+extension Merge<T> on Stream<T> {
+  /// Merges values and errors from this stream and [other] in any order as they
+  /// arrive.
+  ///
+  /// The result stream will not close until both this stream and [other] have
+  /// closed.
+  ///
+  /// For example:
+  ///
+  ///     final result = source.merge(other);
+  ///
+  ///     source:  1--2-----3--|
+  ///     other:   ------4-------5--|
+  ///     result:  1--2--4--3----5--|
+  ///
+  /// If this stream is a broadcast stream, the result stream will be as
+  /// well, regardless of [other]'s type. If a single subscription stream is
+  /// merged into a broadcast stream it may never be canceled since there may be
+  /// broadcast listeners added later.
+  ///
+  /// If a broadcast stream is merged into a single-subscription stream any
+  /// events emitted by [other] before the result stream has a subscriber will
+  /// be discarded.
+  Stream<T> merge(Stream<T> other) => mergeAll([other]);
+
+  /// Merges values and errors from this stream and any stream in [others] in
+  /// any order as they arrive.
+  ///
+  /// The result stream will not close until this stream and all streams
+  /// in [others] have closed.
+  ///
+  /// For example:
+  ///
+  ///     final result = first.mergeAll([second, third]);
+  ///
+  ///     first:   1--2--------3--|
+  ///     second:  ---------4-------5--|
+  ///     third:   ------6---------------7--|
+  ///     result:  1--2--6--4--3----5----7--|
+  ///
+  /// If this stream is a broadcast stream, the result stream will be as
+  /// well, regardless the types of streams in [others]. If a single
+  /// subscription stream is merged into a broadcast stream it may never be
+  /// canceled since there may be broadcast listeners added later.
+  ///
+  /// If a broadcast stream is merged into a single-subscription stream any
+  /// events emitted by that stream before the result stream has a subscriber
+  /// will be discarded.
+  Stream<T> mergeAll(Iterable<Stream<T>> others) {
+    final controller = isBroadcast
+        ? StreamController<T>.broadcast(sync: true)
+        : StreamController<T>(sync: true);
+
+    final allStreams = [
+      this,
+      for (final other in others)
+        !isBroadcast || other.isBroadcast ? other : other.asBroadcastStream(),
+    ];
+
+    controller.onListen = () {
+      final subscriptions = <StreamSubscription<T>>[];
+      for (final stream in allStreams) {
+        final subscription =
+            stream.listen(controller.add, onError: controller.addError);
+        subscription.onDone(() {
+          subscriptions.remove(subscription);
+          if (subscriptions.isEmpty) controller.close();
+        });
+        subscriptions.add(subscription);
+      }
+      if (!isBroadcast) {
+        controller
+          ..onPause = () {
+            for (final subscription in subscriptions) {
+              subscription.pause();
+            }
+          }
+          ..onResume = () {
+            for (final subscription in subscriptions) {
+              subscription.resume();
+            }
+          };
+      }
+      controller.onCancel = () {
+        if (subscriptions.isEmpty) return null;
+        return [for (var s in subscriptions) s.cancel()]
+            .wait
+            .then(ignoreArgument);
+      };
+    };
+    return controller.stream;
+  }
+}
diff --git a/pkgs/stream_transform/lib/src/rate_limit.dart b/pkgs/stream_transform/lib/src/rate_limit.dart
new file mode 100644
index 0000000..299c230
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/rate_limit.dart
@@ -0,0 +1,356 @@
+// 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:async';
+
+import 'aggregate_sample.dart';
+import 'common_callbacks.dart';
+import 'from_handlers.dart';
+
+/// Utilities to rate limit events.
+///
+/// - [debounce] - emit the the _first_ or _last_ event of a series of closely
+///   spaced events.
+/// - [debounceBuffer] - emit _all_ events at the _end_ of a series of closely
+///   spaced events.
+/// - [throttle] - emit the _first_ event at the _beginning_ of the period.
+/// - [audit] - emit the _last_ event at the _end_ of the period.
+/// - [buffer] - emit _all_ events on a _trigger_.
+extension RateLimit<T> on Stream<T> {
+  /// Suppresses events with less inter-event spacing than [duration].
+  ///
+  /// Events which are emitted with less than [duration] elapsed between them
+  /// are considered to be part of the same "series". If [leading] is `true`,
+  /// the first event of this series is emitted immediately. If [trailing] is
+  /// `true` the last event of this series is emitted with a delay of at least
+  /// [duration]. By default only trailing events are emitted, both arguments
+  /// must be specified with `leading: true, trailing: false` to emit only
+  /// leading events.
+  ///
+  /// If this stream is a broadcast stream, the result will be as well.
+  /// Errors are forwarded immediately.
+  ///
+  /// If there is a trailing event waiting during the debounce period when the
+  /// source stream closes the returned stream will wait to emit it following
+  /// the debounce period before closing. If there is no pending debounced event
+  /// when this stream closes the returned stream will close immediately.
+  ///
+  /// For example:
+  ///
+  ///     source.debounce(Duration(seconds: 1));
+  ///
+  ///     source: 1-2-3---4---5-6-|
+  ///     result: ------3---4-----6|
+  ///
+  ///     source.debounce(Duration(seconds: 1), leading: true, trailing: false);
+  ///
+  ///     source: 1-2-3---4---5-6-|
+  ///     result: 1-------4---5---|
+  ///
+  ///     source.debounce(Duration(seconds: 1), leading: true);
+  ///
+  ///     source: 1-2-3---4---5-6-|
+  ///     result: 1-----3-4---5---6|
+  ///
+  /// To collect values emitted during the debounce period see [debounceBuffer].
+  Stream<T> debounce(Duration duration,
+          {bool leading = false, bool trailing = true}) =>
+      _debounceAggregate(duration, _dropPrevious,
+          leading: leading, trailing: trailing);
+
+  /// Buffers values until this stream does not emit for [duration] then emits
+  /// the collected values.
+  ///
+  /// Values will always be delayed by at least [duration], and values which
+  /// come within this time will be aggregated into the same list.
+  ///
+  /// If this stream is a broadcast stream, the result will be as well.
+  /// Errors are forwarded immediately.
+  ///
+  /// If there are events waiting during the debounce period when this stream
+  /// closes the returned stream will wait to emit them following the debounce
+  /// period before closing. If there are no pending debounced events when this
+  /// stream closes the returned stream will close immediately.
+  ///
+  /// To keep only the most recent event during the debounce period see
+  /// [debounce].
+  Stream<List<T>> debounceBuffer(Duration duration) =>
+      _debounceAggregate(duration, _collect, leading: false, trailing: true);
+
+  /// Reduces the rate that events are emitted to at most once per [duration].
+  ///
+  /// No events will ever be emitted within [duration] of another event on the
+  /// result stream.
+  /// If this stream is a broadcast stream, the result will be as well.
+  /// Errors are forwarded immediately.
+  ///
+  /// If [trailing] is `false`, source events emitted during the [duration]
+  /// period following a result event are discarded.
+  /// The result stream will not emit an event until this stream emits an event
+  /// following the throttled period.
+  /// If this stream is consistently emitting events with less than
+  /// [duration] between events, the time between events on the result stream
+  /// may still be more than [duration].
+  /// The result stream will close immediately when this stream closes.
+  ///
+  /// If [trailing] is `true`, the latest source event emitted during the
+  /// [duration] period following an result event is held and emitted following
+  /// the period.
+  /// If this stream is consistently emitting events with less than [duration]
+  /// between events, the time between events on the result stream will be
+  /// [duration].
+  /// If this stream closes the result stream will wait to emit a pending event
+  /// before closing.
+  ///
+  /// For example:
+  ///
+  ///     source.throttle(Duration(seconds: 6));
+  ///
+  ///     source: 1-2-3---4-5-6---7-8-|
+  ///     result: 1-------4-------7---|
+  ///
+  ///     source.throttle(Duration(seconds: 6), trailing: true);
+  ///
+  ///     source: 1-2-3---4-5----6--|
+  ///     result: 1-----3-----5-----6|
+  ///
+  ///     source.throttle(Duration(seconds: 6), trailing: true);
+  ///
+  ///     source: 1-2-----------3|
+  ///     result: 1-----2-------3|
+  ///
+  /// See also:
+  /// - [audit], which emits the most recent event at the end of the period.
+  /// Compared to `audit`, `throttle` will not introduce delay to forwarded
+  /// elements, except for the [trailing] events.
+  /// - [debounce], which uses inter-event spacing instead of a fixed period
+  /// from the first event in a window. Compared to `debouce`, `throttle` cannot
+  /// be starved by having events emitted continuously within [duration].
+  Stream<T> throttle(Duration duration, {bool trailing = false}) =>
+      trailing ? _throttleTrailing(duration) : _throttle(duration);
+
+  Stream<T> _throttle(Duration duration) {
+    Timer? timer;
+
+    return transformByHandlers(onData: (data, sink) {
+      if (timer == null) {
+        sink.add(data);
+        timer = Timer(duration, () {
+          timer = null;
+        });
+      }
+    });
+  }
+
+  Stream<T> _throttleTrailing(Duration duration) {
+    Timer? timer;
+    T? pending;
+    var hasPending = false;
+    var isDone = false;
+
+    return transformByHandlers(onData: (data, sink) {
+      void onTimer() {
+        if (hasPending) {
+          sink.add(pending as T);
+          if (isDone) {
+            sink.close();
+          } else {
+            timer = Timer(duration, onTimer);
+            hasPending = false;
+            pending = null;
+          }
+        } else {
+          timer = null;
+        }
+      }
+
+      if (timer == null) {
+        sink.add(data);
+        timer = Timer(duration, onTimer);
+      } else {
+        hasPending = true;
+        pending = data;
+      }
+    }, onDone: (sink) {
+      isDone = true;
+      if (hasPending) return; // Will be closed by timer.
+      sink.close();
+      timer?.cancel();
+      timer = null;
+    });
+  }
+
+  /// Audit a single event from each [duration] length period where there are
+  /// events on this stream.
+  ///
+  /// No events will ever be emitted within [duration] of another event on the
+  /// result stream.
+  /// If this stream is a broadcast stream, the result will be as well.
+  /// Errors are forwarded immediately.
+  ///
+  /// The first event will begin the audit period. At the end of the audit
+  /// period the most recent event is emitted, and the next event restarts the
+  /// audit period.
+  ///
+  /// If the event that started the period is the one that is emitted it will be
+  /// delayed by [duration]. If a later event comes in within the period it's
+  /// delay will be shorter by the difference in arrival times.
+  ///
+  /// If there is no pending event when this stream closes the output
+  /// stream will close immediately. If there is a pending event the output
+  /// stream will wait to emit it before closing.
+  ///
+  /// For example:
+  ///
+  ///     source.audit(Duration(seconds: 5));
+  ///
+  ///     source: a------b--c----d--|
+  ///     output: -----a------c--------d|
+  ///
+  /// See also:
+  /// - [throttle], which emits the _first_ event during the window, instead of
+  /// the last event in the window. Compared to `throttle`, `audit` will
+  /// introduce delay to forwarded events.
+  /// - [debounce], which only emits after the stream has not emitted for some
+  /// period. Compared to `debouce`, `audit` cannot be starved by having events
+  /// emitted continuously within [duration].
+  Stream<T> audit(Duration duration) {
+    Timer? timer;
+    var shouldClose = false;
+    T recentData;
+
+    return transformByHandlers(onData: (data, sink) {
+      recentData = data;
+      timer ??= Timer(duration, () {
+        sink.add(recentData);
+        timer = null;
+        if (shouldClose) {
+          sink.close();
+        }
+      });
+    }, onDone: (sink) {
+      if (timer != null) {
+        shouldClose = true;
+      } else {
+        sink.close();
+      }
+    });
+  }
+
+  /// Buffers the values emitted on this stream and emits them when [trigger]
+  /// emits an event.
+  ///
+  /// If [longPoll] is `false`, if there are no buffered values when [trigger]
+  /// emits an empty list is immediately emitted.
+  ///
+  /// If [longPoll] is `true`, and there are no buffered values when [trigger]
+  /// emits one or more events, then the *next* value from this stream is
+  /// immediately emitted on the returned stream as a single element list.
+  /// Subsequent events on [trigger] while there have been no events on this
+  /// stream are ignored.
+  ///
+  /// The result stream will close as soon as there is a guarantee it will not
+  /// emit any more events. There will not be any more events emitted if:
+  /// - [trigger] is closed and there is no waiting long poll.
+  /// - Or, this stream is closed and previously buffered events have been
+  /// delivered.
+  ///
+  /// If this stream is a broadcast stream, the result will be as well.
+  /// Errors from this stream or the trigger are immediately forwarded to the
+  /// output.
+  ///
+  /// See also:
+  /// - [sample] which use a [trigger] stream in the same way, but keeps only
+  /// the most recent source event.
+  Stream<List<T>> buffer(Stream<void> trigger, {bool longPoll = true}) =>
+      aggregateSample(
+          trigger: trigger,
+          aggregate: _collect,
+          longPoll: longPoll,
+          onEmpty: _empty);
+
+  /// Emits the most recent new value from this stream when [trigger] emits an
+  /// event.
+  ///
+  /// If [longPoll] is `false`, then an event on [trigger] when there is no
+  /// pending source event will be ignored.
+  /// If [longPoll] is `true` (the default), then an event on [trigger] when
+  /// there is no pending source event will cause the next source event
+  /// to immediately flow to the result stream.
+  ///
+  /// If [longPoll] is `false`, if there is no pending source event when
+  /// [trigger] emits, then the trigger event will be ignored.
+  ///
+  /// If [longPoll] is `true`, and there are no buffered values when [trigger]
+  /// emits one or more events, then the *next* value from this stream is
+  /// immediately emitted on the returned stream as a single element list.
+  /// Subsequent events on [trigger] while there have been no events on this
+  /// stream are ignored.
+  ///
+  /// The result stream will close as soon as there is a guarantee it will not
+  /// emit any more events. There will not be any more events emitted if:
+  /// - [trigger] is closed and there is no waiting long poll.
+  /// - Or, this source stream is closed and any pending source event has been
+  /// delivered.
+  ///
+  /// If this source stream is a broadcast stream, the result will be as well.
+  /// Errors from this source stream or the trigger are immediately forwarded to
+  /// the output.
+  ///
+  /// See also:
+  /// - [buffer] which use [trigger] stream in the same way, but keeps a list of
+  /// pending source events.
+  Stream<T> sample(Stream<void> trigger, {bool longPoll = true}) =>
+      aggregateSample(
+          trigger: trigger,
+          aggregate: _dropPrevious,
+          longPoll: longPoll,
+          onEmpty: ignoreArgument);
+
+  /// Aggregates values until this source stream does not emit for [duration],
+  /// then emits the aggregated values.
+  Stream<S> _debounceAggregate<S>(
+      Duration duration, S Function(T element, S? soFar) collect,
+      {required bool leading, required bool trailing}) {
+    Timer? timer;
+    S? soFar;
+    var hasPending = false;
+    var shouldClose = false;
+    var emittedLatestAsLeading = false;
+
+    return transformByHandlers(onData: (value, sink) {
+      void emit() {
+        sink.add(soFar as S);
+        soFar = null;
+        hasPending = false;
+      }
+
+      timer?.cancel();
+      soFar = collect(value, soFar);
+      hasPending = true;
+      if (timer == null && leading) {
+        emittedLatestAsLeading = true;
+        emit();
+      } else {
+        emittedLatestAsLeading = false;
+      }
+      timer = Timer(duration, () {
+        if (trailing && !emittedLatestAsLeading) emit();
+        if (shouldClose) sink.close();
+        timer = null;
+      });
+    }, onDone: (EventSink<S> sink) {
+      if (hasPending && trailing) {
+        shouldClose = true;
+      } else {
+        timer?.cancel();
+        sink.close();
+      }
+    });
+  }
+}
+
+T _dropPrevious<T>(T element, _) => element;
+List<T> _collect<T>(T event, List<T>? soFar) => (soFar ?? <T>[])..add(event);
+void _empty<T>(Sink<List<T>> sink) => sink.add([]);
diff --git a/pkgs/stream_transform/lib/src/scan.dart b/pkgs/stream_transform/lib/src/scan.dart
new file mode 100644
index 0000000..acd3c76
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/scan.dart
@@ -0,0 +1,31 @@
+// Copyright (c) 2017, 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';
+
+/// A utility similar to [fold] which emits intermediate accumulations.
+extension Scan<T> on Stream<T> {
+  /// Emits a sequence of the accumulated values from repeatedly applying
+  /// [combine].
+  ///
+  /// Like [fold], but instead of producing a single value it yields each
+  /// intermediate result.
+  ///
+  /// If [combine] returns a future it will not be called again for subsequent
+  /// events from the source until it completes, therefore [combine] is always
+  /// called for elements in order, and the result stream always maintains the
+  /// same order as this stream.
+  Stream<S> scan<S>(
+      S initialValue, FutureOr<S> Function(S soFar, T element) combine) {
+    var accumulated = initialValue;
+    return asyncMap((value) {
+      var result = combine(accumulated, value);
+      if (result is Future<S>) {
+        return result.then((r) => accumulated = r);
+      } else {
+        return accumulated = result;
+      }
+    });
+  }
+}
diff --git a/pkgs/stream_transform/lib/src/switch.dart b/pkgs/stream_transform/lib/src/switch.dart
new file mode 100644
index 0000000..546036e
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/switch.dart
@@ -0,0 +1,135 @@
+// Copyright (c) 2017, 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 'async_expand.dart';
+import 'common_callbacks.dart';
+
+/// A utility to take events from the most recent sub stream returned by a
+/// callback.
+extension Switch<T> on Stream<T> {
+  /// Maps events to a Stream and emits values from the most recently created
+  /// Stream.
+  ///
+  /// When the source emits a value it will be converted to a [Stream] using
+  /// [convert] and the output will switch to emitting events from that result.
+  /// Like [asyncExpand] but the [Stream] emitted by a previous element
+  /// will be ignored as soon as the source stream emits a new event.
+  ///
+  /// This means that the source stream is not paused until a sub stream
+  /// returned from the [convert] callback is done. Instead, the subscription
+  /// to the sub stream is canceled as soon as the source stream emits a new
+  /// event.
+  ///
+  /// Errors from [convert], the source stream, or any of the sub streams are
+  /// forwarded to the result stream.
+  ///
+  /// The result stream will not close until the source stream closes and
+  /// the current sub stream have closed.
+  ///
+  /// If the source stream is a broadcast stream, the result will be as well,
+  /// regardless of the types of streams created by [convert]. In this case,
+  /// some care should be taken:
+  ///
+  ///  * If [convert] returns a single subscription stream it may be listened to
+  /// and never canceled.
+  ///
+  /// See also:
+  /// - [concurrentAsyncExpand], which emits events from all sub streams
+  ///   concurrently instead of cancelling subscriptions to previous subs
+  ///   streams.
+  Stream<S> switchMap<S>(Stream<S> Function(T) convert) {
+    return map(convert).switchLatest();
+  }
+}
+
+/// A utility to take events from the most recent sub stream.
+extension SwitchLatest<T> on Stream<Stream<T>> {
+  /// Emits values from the most recently emitted Stream.
+  ///
+  /// When the source emits a stream, the output will switch to emitting events
+  /// from that stream.
+  ///
+  /// Whether the source stream is a single-subscription stream or a
+  /// broadcast stream, the result stream will be the same kind of stream,
+  /// regardless of the types of streams emitted.
+  Stream<T> switchLatest() {
+    var controller = isBroadcast
+        ? StreamController<T>.broadcast(sync: true)
+        : StreamController<T>(sync: true);
+
+    controller.onListen = () {
+      StreamSubscription<T>? innerSubscription;
+      var outerStreamDone = false;
+
+      void listenToInnerStream(Stream<T> innerStream) {
+        assert(innerSubscription == null);
+        var subscription = innerStream
+            .listen(controller.add, onError: controller.addError, onDone: () {
+          innerSubscription = null;
+          if (outerStreamDone) controller.close();
+        });
+        // If a pause happens during an innerSubscription.cancel,
+        // we still listen to the next stream when the cancel is done.
+        // Then we immediately pause it again here.
+        if (controller.isPaused) subscription.pause();
+        innerSubscription = subscription;
+      }
+
+      var addError = controller.addError;
+      final outerSubscription = listen(null, onError: addError, onDone: () {
+        outerStreamDone = true;
+        if (innerSubscription == null) controller.close();
+      });
+      outerSubscription.onData((innerStream) async {
+        var currentSubscription = innerSubscription;
+        if (currentSubscription == null) {
+          listenToInnerStream(innerStream);
+          return;
+        }
+        innerSubscription = null;
+        outerSubscription.pause();
+        try {
+          await currentSubscription.cancel();
+        } catch (error, stack) {
+          controller.addError(error, stack);
+        } finally {
+          if (!isBroadcast && !controller.hasListener) {
+            // Result single-subscription stream subscription was cancelled
+            // while waiting for previous innerStream cancel.
+            //
+            // Ensure that the last received stream is also listened to and
+            // cancelled, then do nothing further.
+            innerStream.listen(null).cancel().ignore();
+          } else {
+            outerSubscription.resume();
+            listenToInnerStream(innerStream);
+          }
+        }
+      });
+      if (!isBroadcast) {
+        controller
+          ..onPause = () {
+            innerSubscription?.pause();
+            outerSubscription.pause();
+          }
+          ..onResume = () {
+            innerSubscription?.resume();
+            outerSubscription.resume();
+          };
+      }
+      controller.onCancel = () {
+        var sub = innerSubscription;
+        var cancels = [
+          if (!outerStreamDone) outerSubscription.cancel(),
+          if (sub != null) sub.cancel(),
+        ];
+        if (cancels.isEmpty) return null;
+        return cancels.wait.then(ignoreArgument);
+      };
+    };
+    return controller.stream;
+  }
+}
diff --git a/pkgs/stream_transform/lib/src/take_until.dart b/pkgs/stream_transform/lib/src/take_until.dart
new file mode 100644
index 0000000..e6deaa1
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/take_until.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2017, 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';
+
+/// A utility to end a stream based on an external trigger.
+extension TakeUntil<T> on Stream<T> {
+  /// Takes values from this stream which are emitted before [trigger]
+  /// completes.
+  ///
+  /// Completing [trigger] differs from canceling a subscription in that values
+  /// which are emitted before the trigger, but have further asynchronous delays
+  /// in transformations following the takeUtil, will still go through.
+  /// Cancelling a subscription immediately stops values.
+  ///
+  /// If [trigger] completes as an error, the error will be forwarded through
+  /// the result stream before the result stream closes.
+  ///
+  /// If [trigger] completes as a value or as an error after this stream has
+  /// already ended, the completion will be ignored.
+  Stream<T> takeUntil(Future<void> trigger) {
+    var controller = isBroadcast
+        ? StreamController<T>.broadcast(sync: true)
+        : StreamController<T>(sync: true);
+
+    StreamSubscription<T>? subscription;
+    var isDone = false;
+    trigger.then((_) {
+      if (isDone) return;
+      isDone = true;
+      subscription?.cancel();
+      controller.close();
+    }, onError: (Object error, StackTrace stackTrace) {
+      if (isDone) return;
+      isDone = true;
+      controller
+        ..addError(error, stackTrace)
+        ..close();
+    });
+
+    controller.onListen = () {
+      if (isDone) return;
+      subscription =
+          listen(controller.add, onError: controller.addError, onDone: () {
+        if (isDone) return;
+        isDone = true;
+        controller.close();
+      });
+      if (!isBroadcast) {
+        controller
+          ..onPause = subscription!.pause
+          ..onResume = subscription!.resume;
+      }
+      controller.onCancel = () {
+        if (isDone) return null;
+        var toCancel = subscription!;
+        subscription = null;
+        return toCancel.cancel();
+      };
+    };
+    return controller.stream;
+  }
+}
diff --git a/pkgs/stream_transform/lib/src/tap.dart b/pkgs/stream_transform/lib/src/tap.dart
new file mode 100644
index 0000000..4b16ab5
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/tap.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2017, 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 'from_handlers.dart';
+
+/// A utility to chain extra behavior on a stream.
+extension Tap<T> on Stream<T> {
+  /// Taps into this stream to allow additional handling on a single-subscriber
+  /// stream without first wrapping as a broadcast stream.
+  ///
+  /// The [onValue] callback will be called with every value from this stream
+  /// before it is forwarded to listeners on the resulting stream.
+  /// May be null if only [onError] or [onDone] callbacks are needed.
+  ///
+  /// The [onError] callback will be called with every error from this stream
+  /// before it is forwarded to listeners on the resulting stream.
+  ///
+  /// The [onDone] callback will be called after this stream closes and before
+  /// the resulting stream is closed.
+  ///
+  /// Errors from any of the callbacks are caught and ignored.
+  ///
+  /// The callbacks may not be called until the tapped stream has a listener,
+  /// and may not be called after the listener has canceled the subscription.
+  Stream<T> tap(void Function(T)? onValue,
+          {void Function(Object, StackTrace)? onError,
+          void Function()? onDone}) =>
+      transformByHandlers(onData: (value, sink) {
+        try {
+          onValue?.call(value);
+        } catch (_) {/*Ignore*/}
+        sink.add(value);
+      }, onError: (error, stackTrace, sink) {
+        try {
+          onError?.call(error, stackTrace);
+        } catch (_) {/*Ignore*/}
+        sink.addError(error, stackTrace);
+      }, onDone: (sink) {
+        try {
+          onDone?.call();
+        } catch (_) {/*Ignore*/}
+        sink.close();
+      });
+}
diff --git a/pkgs/stream_transform/lib/src/where.dart b/pkgs/stream_transform/lib/src/where.dart
new file mode 100644
index 0000000..76aa28a
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/where.dart
@@ -0,0 +1,71 @@
+// 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:async';
+
+import 'from_handlers.dart';
+
+/// Utilities to filter events.
+extension Where<T> on Stream<T> {
+  /// Discards events from this stream that are not of type [S].
+  ///
+  /// If the source stream is a broadcast stream the result will be as well.
+  ///
+  /// Errors from the source stream are forwarded directly to the result stream.
+  ///
+  /// [S] should be a subtype of the stream's generic type, otherwise nothing of
+  /// type [S] could possibly be emitted, however there is no static or runtime
+  /// checking that this is the case.
+  Stream<S> whereType<S>() => transformByHandlers(onData: (event, sink) {
+        if (event is S) sink.add(event);
+      });
+
+  /// Discards events from this stream based on an asynchronous [test] callback.
+  ///
+  /// Like [where] but allows the [test] to return a [Future].
+  ///
+  /// Events on the result stream will be emitted in the order that [test]
+  /// completes which may not match the order of this stream.
+  ///
+  /// If the source stream is a broadcast stream the result will be as well.
+  /// When used with a broadcast stream behavior also differs from [where] in
+  /// that the [test] function is only called once per event, rather than once
+  /// per listener per event.
+  ///
+  /// Errors from the source stream are forwarded directly to the result stream.
+  /// Errors from [test] are also forwarded to the result stream.
+  ///
+  /// The result stream will not close until the source stream closes and all
+  /// pending [test] calls have finished.
+  Stream<T> asyncWhere(FutureOr<bool> Function(T) test) {
+    var valuesWaiting = 0;
+    var sourceDone = false;
+    return transformByHandlers(onData: (element, sink) {
+      valuesWaiting++;
+      () async {
+        try {
+          if (await test(element)) sink.add(element);
+        } catch (e, st) {
+          sink.addError(e, st);
+        }
+        valuesWaiting--;
+        if (valuesWaiting <= 0 && sourceDone) sink.close();
+      }();
+    }, onDone: (sink) {
+      sourceDone = true;
+      if (valuesWaiting <= 0) sink.close();
+    });
+  }
+}
+
+extension WhereNotNull<T extends Object> on Stream<T?> {
+  /// Discards `null` events from this stream.
+  ///
+  /// If the source stream is a broadcast stream the result will be as well.
+  ///
+  /// Errors from the source stream are forwarded directly to the result stream.
+  Stream<T> whereNotNull() => transformByHandlers(onData: (event, sink) {
+        if (event != null) sink.add(event);
+      });
+}
diff --git a/pkgs/stream_transform/lib/stream_transform.dart b/pkgs/stream_transform/lib/stream_transform.dart
new file mode 100644
index 0000000..edf4df9
--- /dev/null
+++ b/pkgs/stream_transform/lib/stream_transform.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2017, 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/async_expand.dart';
+export 'src/async_map.dart';
+export 'src/combine_latest.dart';
+export 'src/concatenate.dart';
+export 'src/merge.dart';
+export 'src/rate_limit.dart';
+export 'src/scan.dart';
+export 'src/switch.dart';
+export 'src/take_until.dart';
+export 'src/tap.dart';
+export 'src/where.dart';
diff --git a/pkgs/stream_transform/pubspec.yaml b/pkgs/stream_transform/pubspec.yaml
new file mode 100644
index 0000000..91840b7
--- /dev/null
+++ b/pkgs/stream_transform/pubspec.yaml
@@ -0,0 +1,14 @@
+name: stream_transform
+version: 2.1.2-wip
+description: A collection of utilities to transform and manipulate streams.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/stream_transform
+
+environment:
+  sdk: ^3.4.0
+
+dev_dependencies:
+  async: ^2.5.0
+  dart_flutter_team_lints: ^3.0.0
+  fake_async: ^1.3.0
+  test: ^1.16.0
+  web: ^1.1.0
diff --git a/pkgs/stream_transform/test/async_expand_test.dart b/pkgs/stream_transform/test/async_expand_test.dart
new file mode 100644
index 0000000..8d84300
--- /dev/null
+++ b/pkgs/stream_transform/test/async_expand_test.dart
@@ -0,0 +1,195 @@
+// 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:async';
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  test('forwards errors from the convert callback', () async {
+    var errors = <String>[];
+    var source = Stream.fromIterable([1, 2, 3]);
+    source.concurrentAsyncExpand<void>((i) {
+      // ignore: only_throw_errors
+      throw 'Error: $i';
+    }).listen((_) {}, onError: errors.add);
+    await Future<void>(() {});
+    expect(errors, ['Error: 1', 'Error: 2', 'Error: 3']);
+  });
+
+  for (var outerType in streamTypes) {
+    for (var innerType in streamTypes) {
+      group('concurrentAsyncExpand $outerType to $innerType', () {
+        late StreamController<int> outerController;
+        late bool outerCanceled;
+        late List<StreamController<String>> innerControllers;
+        late List<bool> innerCanceled;
+        late List<String> emittedValues;
+        late bool isDone;
+        late List<String> errors;
+        late Stream<String> transformed;
+        late StreamSubscription<String> subscription;
+
+        setUp(() {
+          outerController = createController(outerType)
+            ..onCancel = () {
+              outerCanceled = true;
+            };
+          outerCanceled = false;
+          innerControllers = [];
+          innerCanceled = [];
+          emittedValues = [];
+          errors = [];
+          isDone = false;
+          transformed = outerController.stream.concurrentAsyncExpand((i) {
+            var index = innerControllers.length;
+            innerCanceled.add(false);
+            innerControllers.add(createController<String>(innerType)
+              ..onCancel = () {
+                innerCanceled[index] = true;
+              });
+            return innerControllers.last.stream;
+          });
+          subscription = transformed
+              .listen(emittedValues.add, onError: errors.add, onDone: () {
+            isDone = true;
+          });
+        });
+
+        test('interleaves events from sub streams', () async {
+          outerController
+            ..add(1)
+            ..add(2);
+          await Future<void>(() {});
+          expect(emittedValues, isEmpty);
+          expect(innerControllers, hasLength(2));
+          innerControllers[0].add('First');
+          innerControllers[1].add('Second');
+          innerControllers[0].add('First again');
+          await Future<void>(() {});
+          expect(emittedValues, ['First', 'Second', 'First again']);
+        });
+
+        test('forwards errors from outer stream', () async {
+          outerController.addError('Error');
+          await Future<void>(() {});
+          expect(errors, ['Error']);
+        });
+
+        test('forwards errors from inner streams', () async {
+          outerController
+            ..add(1)
+            ..add(2);
+          await Future<void>(() {});
+          innerControllers[0].addError('Error 1');
+          innerControllers[1].addError('Error 2');
+          await Future<void>(() {});
+          expect(errors, ['Error 1', 'Error 2']);
+        });
+
+        test('can continue handling events after an error in outer stream',
+            () async {
+          outerController
+            ..addError('Error')
+            ..add(1);
+          await Future<void>(() {});
+          innerControllers[0].add('First');
+          await Future<void>(() {});
+          expect(emittedValues, ['First']);
+          expect(errors, ['Error']);
+        });
+
+        test('cancels outer subscription if output canceled', () async {
+          await subscription.cancel();
+          expect(outerCanceled, true);
+        });
+
+        if (outerType != 'broadcast' || innerType != 'single subscription') {
+          // A single subscription inner stream in a broadcast outer stream is
+          // not canceled.
+          test('cancels inner subscriptions if output canceled', () async {
+            outerController
+              ..add(1)
+              ..add(2);
+            await Future<void>(() {});
+            await subscription.cancel();
+            expect(innerCanceled, [true, true]);
+          });
+        }
+
+        test('stays open if any inner stream is still open', () async {
+          outerController.add(1);
+          await outerController.close();
+          await Future<void>(() {});
+          expect(isDone, false);
+        });
+
+        test('stays open if outer stream is still open', () async {
+          outerController.add(1);
+          await Future<void>(() {});
+          await innerControllers[0].close();
+          await Future<void>(() {});
+          expect(isDone, false);
+        });
+
+        test('closes after all inner streams and outer stream close', () async {
+          outerController.add(1);
+          await Future<void>(() {});
+          await innerControllers[0].close();
+          await outerController.close();
+          await Future<void>(() {});
+          expect(isDone, true);
+        });
+
+        if (outerType == 'broadcast') {
+          test('multiple listerns all get values', () async {
+            var otherValues = <String>[];
+            transformed.listen(otherValues.add);
+            outerController.add(1);
+            await Future<void>(() {});
+            innerControllers[0].add('First');
+            await Future<void>(() {});
+            expect(emittedValues, ['First']);
+            expect(otherValues, ['First']);
+          });
+
+          test('multiple listeners get closed', () async {
+            var otherDone = false;
+            transformed.listen(null, onDone: () => otherDone = true);
+            outerController.add(1);
+            await Future<void>(() {});
+            await innerControllers[0].close();
+            await outerController.close();
+            await Future<void>(() {});
+            expect(isDone, true);
+            expect(otherDone, true);
+          });
+
+          test('can cancel and relisten', () async {
+            outerController
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            innerControllers[0].add('First');
+            innerControllers[1].add('Second');
+            await Future(() {});
+            await subscription.cancel();
+            innerControllers[0].add('Ignored');
+            await Future(() {});
+            subscription = transformed.listen(emittedValues.add);
+            innerControllers[0].add('Also ignored');
+            outerController.add(3);
+            await Future(() {});
+            innerControllers[2].add('More');
+            await Future(() {});
+            expect(emittedValues, ['First', 'Second', 'More']);
+          });
+        }
+      });
+    }
+  }
+}
diff --git a/pkgs/stream_transform/test/async_map_buffer_test.dart b/pkgs/stream_transform/test/async_map_buffer_test.dart
new file mode 100644
index 0000000..2386217
--- /dev/null
+++ b/pkgs/stream_transform/test/async_map_buffer_test.dart
@@ -0,0 +1,204 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  late StreamController<int> values;
+  late List<String> emittedValues;
+  late bool valuesCanceled;
+  late bool isDone;
+  late List<String> errors;
+  late Stream<String> transformed;
+  late StreamSubscription<String> subscription;
+
+  Completer<String>? finishWork;
+  List<int>? workArgument;
+
+  /// Represents the async `convert` function and asserts that is is only called
+  /// after the previous iteration has completed.
+  Future<String> work(List<int> values) {
+    expect(finishWork, isNull,
+        reason: 'See $values befor previous work is complete');
+    workArgument = values;
+    finishWork = Completer()
+      ..future.then((_) {
+        workArgument = null;
+        finishWork = null;
+      }).catchError((_) {
+        workArgument = null;
+        finishWork = null;
+      });
+    return finishWork!.future;
+  }
+
+  for (var streamType in streamTypes) {
+    group('asyncMapBuffer for stream type: [$streamType]', () {
+      setUp(() {
+        valuesCanceled = false;
+        values = createController(streamType)
+          ..onCancel = () {
+            valuesCanceled = true;
+          };
+        emittedValues = [];
+        errors = [];
+        isDone = false;
+        finishWork = null;
+        workArgument = null;
+        transformed = values.stream.asyncMapBuffer(work);
+        subscription = transformed
+            .listen(emittedValues.add, onError: errors.add, onDone: () {
+          isDone = true;
+        });
+      });
+
+      test('does not emit before work finishes', () async {
+        values.add(1);
+        await Future(() {});
+        expect(emittedValues, isEmpty);
+        expect(workArgument, [1]);
+        finishWork!.complete('result');
+        await Future(() {});
+        expect(emittedValues, ['result']);
+      });
+
+      test('buffers values while work is ongoing', () async {
+        values.add(1);
+        await Future(() {});
+        values
+          ..add(2)
+          ..add(3);
+        await Future(() {});
+        finishWork!.complete('');
+        await Future(() {});
+        expect(workArgument, [2, 3]);
+      });
+
+      test('forwards errors without waiting for work', () async {
+        values.add(1);
+        await Future(() {});
+        values.addError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      test('forwards errors which occur during the work', () async {
+        values.add(1);
+        await Future(() {});
+        finishWork!.completeError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      test('can continue handling events after an error', () async {
+        values.add(1);
+        await Future(() {});
+        finishWork!.completeError('error');
+        values.add(2);
+        await Future(() {});
+        expect(workArgument, [2]);
+        finishWork!.completeError('another');
+        await Future(() {});
+        expect(errors, ['error', 'another']);
+      });
+
+      test('does not start next work early due to an error in values',
+          () async {
+        values.add(1);
+        await Future(() {});
+        values
+          ..addError('error')
+          ..add(2);
+        await Future(() {});
+        expect(errors, ['error']);
+        // [work] will assert that the second iteration is not called because
+        // the first has not completed.
+      });
+
+      test('cancels value subscription when output canceled', () async {
+        expect(valuesCanceled, false);
+        await subscription.cancel();
+        expect(valuesCanceled, true);
+      });
+
+      test('closes when values end if no work is pending', () async {
+        expect(isDone, false);
+        await values.close();
+        await Future(() {});
+        expect(isDone, true);
+      });
+
+      test('waits for pending work when values close', () async {
+        values.add(1);
+        await Future(() {});
+        expect(isDone, false);
+        values.add(2);
+        await values.close();
+        expect(isDone, false);
+        finishWork!.complete('');
+        await Future(() {});
+        // Still a pending value
+        expect(isDone, false);
+        finishWork!.complete('');
+        await Future(() {});
+        expect(isDone, true);
+      });
+
+      test('forwards errors from values', () async {
+        values.addError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      if (streamType == 'broadcast') {
+        test('multiple listeners all get values', () async {
+          var otherValues = <String>[];
+          transformed.listen(otherValues.add);
+          values.add(1);
+          await Future(() {});
+          finishWork!.complete('result');
+          await Future(() {});
+          expect(emittedValues, ['result']);
+          expect(otherValues, ['result']);
+        });
+
+        test('multiple listeners get done when values end', () async {
+          var otherDone = false;
+          transformed.listen(null, onDone: () => otherDone = true);
+          values.add(1);
+          await Future(() {});
+          await values.close();
+          expect(isDone, false);
+          expect(otherDone, false);
+          finishWork!.complete('');
+          await Future(() {});
+          expect(isDone, true);
+          expect(otherDone, true);
+        });
+
+        test('can cancel and relisten', () async {
+          values.add(1);
+          await Future(() {});
+          finishWork!.complete('first');
+          await Future(() {});
+          await subscription.cancel();
+          values.add(2);
+          await Future(() {});
+          subscription = transformed.listen(emittedValues.add);
+          values.add(3);
+          await Future(() {});
+          expect(workArgument, [3]);
+          finishWork!.complete('second');
+          await Future(() {});
+          expect(emittedValues, ['first', 'second']);
+        });
+      }
+    });
+  }
+}
diff --git a/pkgs/stream_transform/test/async_map_sample_test.dart b/pkgs/stream_transform/test/async_map_sample_test.dart
new file mode 100644
index 0000000..62b1b92
--- /dev/null
+++ b/pkgs/stream_transform/test/async_map_sample_test.dart
@@ -0,0 +1,209 @@
+// 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:async';
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  late StreamController<int> values;
+  late List<String> emittedValues;
+  late bool valuesCanceled;
+  late bool isDone;
+  late List<String> errors;
+  late Stream<String> transformed;
+  late StreamSubscription<String> subscription;
+
+  Completer<String>? finishWork;
+  int? workArgument;
+
+  /// Represents the async `convert` function and asserts that is is only called
+  /// after the previous iteration has completed.
+  Future<String> work(int value) {
+    expect(finishWork, isNull,
+        reason: 'See $values befor previous work is complete');
+    workArgument = value;
+    finishWork = Completer()
+      ..future.then((_) {
+        workArgument = null;
+        finishWork = null;
+      }).catchError((_) {
+        workArgument = null;
+        finishWork = null;
+      });
+    return finishWork!.future;
+  }
+
+  for (var streamType in streamTypes) {
+    group('asyncMapSample for stream type: [$streamType]', () {
+      setUp(() {
+        valuesCanceled = false;
+        values = createController(streamType)
+          ..onCancel = () {
+            valuesCanceled = true;
+          };
+        emittedValues = [];
+        errors = [];
+        isDone = false;
+        finishWork = null;
+        workArgument = null;
+        transformed = values.stream.asyncMapSample(work);
+        subscription = transformed
+            .listen(emittedValues.add, onError: errors.add, onDone: () {
+          isDone = true;
+        });
+      });
+
+      test('does not emit before work finishes', () async {
+        values.add(1);
+        await Future(() {});
+        expect(emittedValues, isEmpty);
+        expect(workArgument, 1);
+        finishWork!.complete('result');
+        await Future(() {});
+        expect(emittedValues, ['result']);
+      });
+
+      test('buffers values while work is ongoing', () async {
+        values.add(1);
+        await Future(() {});
+        values
+          ..add(2)
+          ..add(3);
+        await Future(() {});
+        finishWork!.complete('');
+        await Future(() {});
+        expect(workArgument, 3);
+      });
+
+      test('forwards errors without waiting for work', () async {
+        values.add(1);
+        await Future(() {});
+        values.addError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      test('forwards errors which occur during the work', () async {
+        values.add(1);
+        await Future(() {});
+        finishWork!.completeError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      test('can continue handling events after an error', () async {
+        values.add(1);
+        await Future(() {});
+        finishWork!.completeError('error');
+        values.add(2);
+        await Future(() {});
+        expect(workArgument, 2);
+        finishWork!.completeError('another');
+        await Future(() {});
+        expect(errors, ['error', 'another']);
+      });
+
+      test('does not start next work early due to an error in values',
+          () async {
+        values.add(1);
+        await Future(() {});
+        values
+          ..addError('error')
+          ..add(2);
+        await Future(() {});
+        expect(errors, ['error']);
+        // [work] will assert that the second iteration is not called because
+        // the first has not completed.
+      });
+
+      test('cancels value subscription when output canceled', () async {
+        expect(valuesCanceled, false);
+        await subscription.cancel();
+        expect(valuesCanceled, true);
+      });
+
+      test('closes when values end if no work is pending', () async {
+        expect(isDone, false);
+        await values.close();
+        await Future(() {});
+        expect(isDone, true);
+      });
+
+      test('waits for pending work when values close', () async {
+        values.add(1);
+        await Future(() {});
+        expect(isDone, false);
+        values.add(2);
+        await values.close();
+        expect(isDone, false);
+        finishWork!.complete('');
+        await Future(() {});
+        // Still a pending value
+        expect(isDone, false);
+        finishWork!.complete('');
+        await Future(() {});
+        expect(isDone, true);
+      });
+
+      test('forwards errors from values', () async {
+        values.addError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      if (streamType == 'broadcast') {
+        test('multiple listeners all get values', () async {
+          var otherValues = <String>[];
+          transformed.listen(otherValues.add);
+          values.add(1);
+          await Future(() {});
+          finishWork!.complete('result');
+          await Future(() {});
+          expect(emittedValues, ['result']);
+          expect(otherValues, ['result']);
+        });
+
+        test('multiple listeners get done when values end', () async {
+          var otherDone = false;
+          transformed.listen(null, onDone: () => otherDone = true);
+          values.add(1);
+          await Future(() {});
+          await values.close();
+          expect(isDone, false);
+          expect(otherDone, false);
+          finishWork!.complete('');
+          await Future(() {});
+          expect(isDone, true);
+          expect(otherDone, true);
+        });
+
+        test('can cancel and relisten', () async {
+          values.add(1);
+          await Future(() {});
+          finishWork!.complete('first');
+          await Future(() {});
+          await subscription.cancel();
+          values.add(2);
+          await Future(() {});
+          subscription = transformed.listen(emittedValues.add);
+          values.add(3);
+          await Future(() {});
+          expect(workArgument, 3);
+          finishWork!.complete('second');
+          await Future(() {});
+          expect(emittedValues, ['first', 'second']);
+        });
+      }
+    });
+  }
+
+  test('allows nulls', () async {
+    var stream = Stream<int?>.value(null);
+    await stream.asyncMapSample(expectAsync1((_) async {})).drain<void>();
+  });
+}
diff --git a/pkgs/stream_transform/test/async_where_test.dart b/pkgs/stream_transform/test/async_where_test.dart
new file mode 100644
index 0000000..6ea4e76
--- /dev/null
+++ b/pkgs/stream_transform/test/async_where_test.dart
@@ -0,0 +1,90 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('forwards only events that pass the predicate', () async {
+    var values = Stream.fromIterable([1, 2, 3, 4]);
+    var filtered = values.asyncWhere((e) async => e > 2);
+    expect(await filtered.toList(), [3, 4]);
+  });
+
+  test('allows predicates that go through event loop', () async {
+    var values = Stream.fromIterable([1, 2, 3, 4]);
+    var filtered = values.asyncWhere((e) async {
+      await Future(() {});
+      return e > 2;
+    });
+    expect(await filtered.toList(), [3, 4]);
+  });
+
+  test('allows synchronous predicate', () async {
+    var values = Stream.fromIterable([1, 2, 3, 4]);
+    var filtered = values.asyncWhere((e) => e > 2);
+    expect(await filtered.toList(), [3, 4]);
+  });
+
+  test('can result in empty stream', () async {
+    var values = Stream.fromIterable([1, 2, 3, 4]);
+    var filtered = values.asyncWhere((e) => e > 4);
+    expect(await filtered.isEmpty, true);
+  });
+
+  test('forwards values to multiple listeners', () async {
+    var values = StreamController<int>.broadcast();
+    var filtered = values.stream.asyncWhere((e) async => e > 2);
+    var firstValues = <int>[];
+    var secondValues = <int>[];
+    filtered
+      ..listen(firstValues.add)
+      ..listen(secondValues.add);
+    values
+      ..add(1)
+      ..add(2)
+      ..add(3)
+      ..add(4);
+    await Future(() {});
+    expect(firstValues, [3, 4]);
+    expect(secondValues, [3, 4]);
+  });
+
+  test('closes streams with multiple listeners', () async {
+    var values = StreamController<int>.broadcast();
+    var predicate = Completer<bool>();
+    var filtered = values.stream.asyncWhere((_) => predicate.future);
+    var firstDone = false;
+    var secondDone = false;
+    filtered
+      ..listen(null, onDone: () => firstDone = true)
+      ..listen(null, onDone: () => secondDone = true);
+    values.add(1);
+    await values.close();
+    expect(firstDone, false);
+    expect(secondDone, false);
+
+    predicate.complete(true);
+    await Future(() {});
+    expect(firstDone, true);
+    expect(secondDone, true);
+  });
+
+  test('forwards errors emitted by the test callback', () async {
+    var errors = <Object>[];
+    var emitted = <Object>[];
+    var values = Stream.fromIterable([1, 2, 3, 4]);
+    var filtered = values.asyncWhere((e) async {
+      await Future(() {});
+      if (e.isEven) throw Exception('$e');
+      return true;
+    });
+    var done = Completer<Object?>();
+    filtered.listen(emitted.add, onError: errors.add, onDone: done.complete);
+    await done.future;
+    expect(emitted, [1, 3]);
+    expect(errors.map((e) => '$e'), ['Exception: 2', 'Exception: 4']);
+  });
+}
diff --git a/pkgs/stream_transform/test/audit_test.dart b/pkgs/stream_transform/test/audit_test.dart
new file mode 100644
index 0000000..28537db
--- /dev/null
+++ b/pkgs/stream_transform/test/audit_test.dart
@@ -0,0 +1,140 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  for (var streamType in streamTypes) {
+    group('Stream type [$streamType]', () {
+      late StreamController<int> values;
+      late List<int> emittedValues;
+      late bool valuesCanceled;
+      late bool isDone;
+      late List<String> errors;
+      late Stream<int> transformed;
+      late StreamSubscription<int> subscription;
+
+      group('audit', () {
+        setUp(() {
+          valuesCanceled = false;
+          values = createController(streamType)
+            ..onCancel = () {
+              valuesCanceled = true;
+            };
+          emittedValues = [];
+          errors = [];
+          isDone = false;
+          transformed = values.stream.audit(const Duration(milliseconds: 6));
+        });
+
+        void listen() {
+          subscription = transformed
+              .listen(emittedValues.add, onError: errors.add, onDone: () {
+            isDone = true;
+          });
+        }
+
+        test('cancels values', () async {
+          listen();
+          await subscription.cancel();
+          expect(valuesCanceled, true);
+        });
+
+        test('swallows values that come faster than duration', () {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..add(2)
+              ..close();
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [2]);
+          });
+        });
+
+        test('outputs multiple values spaced further than duration', () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values.add(2);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1, 2]);
+          });
+        });
+
+        test('waits for pending value to close', () {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..close();
+            expect(isDone, false);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(isDone, true);
+          });
+        });
+
+        test('closes output if there are no pending values', () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values
+              ..add(2)
+              ..close();
+            expect(isDone, false);
+            expect(emittedValues, [1]);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(isDone, true);
+            expect(emittedValues, [1, 2]);
+          });
+        });
+
+        test('does not starve output if many values come closer than duration',
+            () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 3));
+            values.add(2);
+            async.elapse(const Duration(milliseconds: 3));
+            values.add(3);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [2, 3]);
+          });
+        });
+
+        if (streamType == 'broadcast') {
+          test('multiple listeners all get the values', () {
+            fakeAsync((async) {
+              listen();
+              values.add(1);
+              async.elapse(const Duration(milliseconds: 3));
+              values.add(2);
+              var otherValues = <int>[];
+              transformed.listen(otherValues.add);
+              values.add(3);
+              async.elapse(const Duration(milliseconds: 3));
+              values.add(4);
+              async.elapse(const Duration(milliseconds: 3));
+              values
+                ..add(5)
+                ..close();
+              async.elapse(const Duration(milliseconds: 6));
+              expect(emittedValues, [3, 5]);
+              expect(otherValues, [3, 5]);
+            });
+          });
+        }
+      });
+    });
+  }
+}
diff --git a/pkgs/stream_transform/test/buffer_test.dart b/pkgs/stream_transform/test/buffer_test.dart
new file mode 100644
index 0000000..830f555
--- /dev/null
+++ b/pkgs/stream_transform/test/buffer_test.dart
@@ -0,0 +1,305 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  late StreamController<void> trigger;
+  late StreamController<int> values;
+  late List<List<int>> emittedValues;
+  late bool valuesCanceled;
+  late bool triggerCanceled;
+  late bool triggerPaused;
+  late bool isDone;
+  late List<String> errors;
+  late Stream<List<int>> transformed;
+  late StreamSubscription<List<int>> subscription;
+
+  void setUpForStreamTypes(String triggerType, String valuesType,
+      {required bool longPoll}) {
+    valuesCanceled = false;
+    triggerCanceled = false;
+    triggerPaused = false;
+    trigger = createController(triggerType)
+      ..onCancel = () {
+        triggerCanceled = true;
+      };
+    if (triggerType == 'single subscription') {
+      trigger.onPause = () {
+        triggerPaused = true;
+      };
+    }
+    values = createController(valuesType)
+      ..onCancel = () {
+        valuesCanceled = true;
+      };
+    emittedValues = [];
+    errors = [];
+    isDone = false;
+    transformed = values.stream.buffer(trigger.stream, longPoll: longPoll);
+    subscription =
+        transformed.listen(emittedValues.add, onError: errors.add, onDone: () {
+      isDone = true;
+    });
+  }
+
+  for (var triggerType in streamTypes) {
+    for (var valuesType in streamTypes) {
+      group('Trigger type: [$triggerType], Values type: [$valuesType]', () {
+        group('general behavior', () {
+          setUp(() {
+            setUpForStreamTypes(triggerType, valuesType, longPoll: true);
+          });
+
+          test('does not emit before `trigger`', () async {
+            values.add(1);
+            await Future(() {});
+            expect(emittedValues, isEmpty);
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [
+              [1]
+            ]);
+          });
+
+          test('groups values between trigger', () async {
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            trigger.add(null);
+            values
+              ..add(3)
+              ..add(4);
+            await Future(() {});
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [
+              [1, 2],
+              [3, 4]
+            ]);
+          });
+
+          test('cancels value subscription when output canceled', () async {
+            expect(valuesCanceled, false);
+            await subscription.cancel();
+            expect(valuesCanceled, true);
+          });
+
+          test('closes when trigger ends', () async {
+            expect(isDone, false);
+            await trigger.close();
+            await Future(() {});
+            expect(isDone, true);
+          });
+
+          test('closes after outputting final values when source closes',
+              () async {
+            expect(isDone, false);
+            values.add(1);
+            await values.close();
+            expect(isDone, false);
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [
+              [1]
+            ]);
+            expect(isDone, true);
+          });
+
+          test('closes when source closes and there are no buffered', () async {
+            expect(isDone, false);
+            await values.close();
+            await Future(() {});
+            expect(isDone, true);
+          });
+
+          test('forwards errors from trigger', () async {
+            trigger.addError('error');
+            await Future(() {});
+            expect(errors, ['error']);
+          });
+
+          test('forwards errors from values', () async {
+            values.addError('error');
+            await Future(() {});
+            expect(errors, ['error']);
+          });
+        });
+
+        group('long polling', () {
+          setUp(() {
+            setUpForStreamTypes(triggerType, valuesType, longPoll: true);
+          });
+
+          test('emits immediately if trigger emits before a value', () async {
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, isEmpty);
+            values.add(1);
+            await Future(() {});
+            expect(emittedValues, [
+              [1]
+            ]);
+          });
+
+          test('two triggers in a row - emit buffere then emit next value',
+              () async {
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            trigger
+              ..add(null)
+              ..add(null);
+            await Future(() {});
+            values.add(3);
+            await Future(() {});
+            expect(emittedValues, [
+              [1, 2],
+              [3]
+            ]);
+          });
+
+          test('pre-emptive trigger then trigger after values', () async {
+            trigger.add(null);
+            await Future(() {});
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [
+              [1],
+              [2]
+            ]);
+          });
+
+          test('multiple pre-emptive triggers, only emits first value',
+              () async {
+            trigger
+              ..add(null)
+              ..add(null);
+            await Future(() {});
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            expect(emittedValues, [
+              [1]
+            ]);
+          });
+
+          test('closes if there is no waiting long poll when source closes',
+              () async {
+            expect(isDone, false);
+            values.add(1);
+            trigger.add(null);
+            await values.close();
+            await Future(() {});
+            expect(isDone, true);
+          });
+
+          test('waits to emit if there waiting long poll when trigger closes',
+              () async {
+            trigger.add(null);
+            await trigger.close();
+            expect(isDone, false);
+            values.add(1);
+            await Future(() {});
+            expect(emittedValues, [
+              [1]
+            ]);
+            expect(isDone, true);
+          });
+        });
+
+        group('immediate polling', () {
+          setUp(() {
+            setUpForStreamTypes(triggerType, valuesType, longPoll: false);
+          });
+
+          test('emits empty list before values', () async {
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [<int>[]]);
+          });
+
+          test('emits empty list after emitting values', () async {
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            trigger
+              ..add(null)
+              ..add(null);
+            await Future(() {});
+            expect(emittedValues, [
+              [1, 2],
+              <int>[]
+            ]);
+          });
+        });
+      });
+    }
+  }
+
+  test('always cancels trigger if values is singlesubscription', () async {
+    setUpForStreamTypes('broadcast', 'single subscription', longPoll: true);
+    expect(triggerCanceled, false);
+    await subscription.cancel();
+    expect(triggerCanceled, true);
+
+    setUpForStreamTypes('single subscription', 'single subscription',
+        longPoll: true);
+    expect(triggerCanceled, false);
+    await subscription.cancel();
+    expect(triggerCanceled, true);
+  });
+
+  test('cancels trigger if trigger is broadcast', () async {
+    setUpForStreamTypes('broadcast', 'broadcast', longPoll: true);
+    expect(triggerCanceled, false);
+    await subscription.cancel();
+    expect(triggerCanceled, true);
+  });
+
+  test('pauses single subscription trigger for broadcast values', () async {
+    setUpForStreamTypes('single subscription', 'broadcast', longPoll: true);
+    expect(triggerCanceled, false);
+    expect(triggerPaused, false);
+    await subscription.cancel();
+    expect(triggerCanceled, false);
+    expect(triggerPaused, true);
+  });
+
+  for (var triggerType in streamTypes) {
+    test('cancel and relisten with [$triggerType] trigger', () async {
+      setUpForStreamTypes(triggerType, 'broadcast', longPoll: true);
+      values.add(1);
+      trigger.add(null);
+      await Future(() {});
+      expect(emittedValues, [
+        [1]
+      ]);
+      await subscription.cancel();
+      values.add(2);
+      trigger.add(null);
+      await Future(() {});
+      subscription = transformed.listen(emittedValues.add);
+      values.add(3);
+      trigger.add(null);
+      await Future(() {});
+      expect(emittedValues, [
+        [1],
+        [3]
+      ]);
+    });
+  }
+}
diff --git a/pkgs/stream_transform/test/combine_latest_all_test.dart b/pkgs/stream_transform/test/combine_latest_all_test.dart
new file mode 100644
index 0000000..f4b719c
--- /dev/null
+++ b/pkgs/stream_transform/test/combine_latest_all_test.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.
+
+import 'dart:async';
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+Future<void> tick() => Future(() {});
+
+void main() {
+  group('combineLatestAll', () {
+    test('emits latest values', () async {
+      final first = StreamController<String>();
+      final second = StreamController<String>();
+      final third = StreamController<String>();
+      final combined = first.stream.combineLatestAll(
+          [second.stream, third.stream]).map((data) => data.join());
+
+      // first:    a----b------------------c--------d---|
+      // second:   --1---------2-----------------|
+      // third:    -------&----------%---|
+      // combined: -------b1&--b2&---b2%---c2%------d2%-|
+
+      expect(combined,
+          emitsInOrder(['b1&', 'b2&', 'b2%', 'c2%', 'd2%', emitsDone]));
+
+      first.add('a');
+      await tick();
+      second.add('1');
+      await tick();
+      first.add('b');
+      await tick();
+      third.add('&');
+      await tick();
+      second.add('2');
+      await tick();
+      third.add('%');
+      await tick();
+      await third.close();
+      await tick();
+      first.add('c');
+      await tick();
+      await second.close();
+      await tick();
+      first.add('d');
+      await tick();
+      await first.close();
+    });
+
+    test('ends if a Stream closes without ever emitting a value', () async {
+      final first = StreamController<String>();
+      final second = StreamController<String>();
+      final combined = first.stream.combineLatestAll([second.stream]);
+
+      // first:    -a------b-------|
+      // second:   -----|
+      // combined: -----|
+
+      expect(combined, emits(emitsDone));
+
+      first.add('a');
+      await tick();
+      await second.close();
+      await tick();
+      first.add('b');
+    });
+
+    test('forwards errors', () async {
+      final first = StreamController<String>();
+      final second = StreamController<String>();
+      final combined = first.stream
+          .combineLatestAll([second.stream]).map((data) => data.join());
+
+      // first:    -a---------|
+      // second:   ----1---#
+      // combined: ----a1--#
+
+      expect(combined, emitsThrough(emitsError('doh')));
+
+      first.add('a');
+      await tick();
+      second.add('1');
+      await tick();
+      second.addError('doh');
+    });
+
+    test('ends after both streams have ended', () async {
+      final first = StreamController<String>();
+      final second = StreamController<String>();
+
+      var done = false;
+      first.stream.combineLatestAll([second.stream]).listen(null,
+          onDone: () => done = true);
+
+      // first:    -a---|
+      // second:   --------1--|
+      // combined: --------a1-|
+
+      first.add('a');
+      await tick();
+      await first.close();
+      await tick();
+
+      expect(done, isFalse);
+
+      second.add('1');
+      await tick();
+      await second.close();
+      await tick();
+
+      expect(done, isTrue);
+    });
+
+    group('broadcast source', () {
+      test('can cancel and relisten to broadcast stream', () async {
+        final first = StreamController<String>.broadcast();
+        final second = StreamController<String>.broadcast();
+        final combined = first.stream
+            .combineLatestAll([second.stream]).map((data) => data.join());
+
+        // first:    a------b----------------c------d----e---|
+        // second:   --1---------2---3---4------5-|
+        // combined: --a1---b1---b2--b3--b4-----c5--d5---e5--|
+        // sub1:     ^-----------------!
+        // sub2:     ----------------------^-----------------|
+
+        expect(combined.take(4), emitsInOrder(['a1', 'b1', 'b2', 'b3']));
+
+        first.add('a');
+        await tick();
+        second.add('1');
+        await tick();
+        first.add('b');
+        await tick();
+        second.add('2');
+        await tick();
+        second.add('3');
+        await tick();
+
+        // First subscription is canceled here by .take(4)
+        expect(first.hasListener, isFalse);
+        expect(second.hasListener, isFalse);
+
+        // This emit is thrown away because there are no subscribers
+        second.add('4');
+        await tick();
+
+        expect(combined, emitsInOrder(['c5', 'd5', 'e5', emitsDone]));
+
+        first.add('c');
+        await tick();
+        second.add('5');
+        await tick();
+        await second.close();
+        await tick();
+        first.add('d');
+        await tick();
+        first.add('e');
+        await tick();
+        await first.close();
+      });
+    });
+  });
+}
diff --git a/pkgs/stream_transform/test/combine_latest_test.dart b/pkgs/stream_transform/test/combine_latest_test.dart
new file mode 100644
index 0000000..1985c75
--- /dev/null
+++ b/pkgs/stream_transform/test/combine_latest_test.dart
@@ -0,0 +1,179 @@
+// 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:async';
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('combineLatest', () {
+    test('flows through combine callback', () async {
+      var source = StreamController<int>();
+      var other = StreamController<int>();
+      int sum(int a, int b) => a + b;
+
+      var results = <int>[];
+      unawaited(
+          source.stream.combineLatest(other.stream, sum).forEach(results.add));
+
+      source.add(1);
+      await Future(() {});
+      expect(results, isEmpty);
+
+      other.add(2);
+      await Future(() {});
+      expect(results, [3]);
+
+      source.add(3);
+      await Future(() {});
+      expect(results, [3, 5]);
+
+      source.add(4);
+      await Future(() {});
+      expect(results, [3, 5, 6]);
+
+      other.add(5);
+      await Future(() {});
+      expect(results, [3, 5, 6, 9]);
+    });
+
+    test('can combine different typed streams', () async {
+      var source = StreamController<String>();
+      var other = StreamController<int>();
+      String times(String a, int b) => a * b;
+
+      var results = <String>[];
+      unawaited(source.stream
+          .combineLatest(other.stream, times)
+          .forEach(results.add));
+
+      source
+        ..add('a')
+        ..add('b');
+      await Future(() {});
+      expect(results, isEmpty);
+
+      other.add(2);
+      await Future(() {});
+      expect(results, ['bb']);
+
+      other.add(3);
+      await Future(() {});
+      expect(results, ['bb', 'bbb']);
+
+      source.add('c');
+      await Future(() {});
+      expect(results, ['bb', 'bbb', 'ccc']);
+    });
+
+    test('ends after both streams have ended', () async {
+      var source = StreamController<int>();
+      var other = StreamController<int>();
+      int sum(int a, int b) => a + b;
+
+      var done = false;
+      source.stream
+          .combineLatest(other.stream, sum)
+          .listen(null, onDone: () => done = true);
+
+      source.add(1);
+
+      await source.close();
+      await Future(() {});
+      expect(done, false);
+
+      await other.close();
+      await Future(() {});
+      expect(done, true);
+    });
+
+    test('ends if source stream closes without ever emitting a value',
+        () async {
+      var source = const Stream<int>.empty();
+      var other = StreamController<int>();
+
+      int sum(int a, int b) => a + b;
+
+      var done = false;
+      source
+          .combineLatest(other.stream, sum)
+          .listen(null, onDone: () => done = true);
+
+      await Future(() {});
+      // Nothing can ever be emitted on the result, may as well close.
+      expect(done, true);
+    });
+
+    test('ends if other stream closes without ever emitting a value', () async {
+      var source = StreamController<int>();
+      var other = const Stream<int>.empty();
+
+      int sum(int a, int b) => a + b;
+
+      var done = false;
+      source.stream
+          .combineLatest(other, sum)
+          .listen(null, onDone: () => done = true);
+
+      await Future(() {});
+      // Nothing can ever be emitted on the result, may as well close.
+      expect(done, true);
+    });
+
+    test('forwards errors', () async {
+      var source = StreamController<int>();
+      var other = StreamController<int>();
+      int sum(int a, int b) => throw _NumberedException(3);
+
+      var errors = <Object>[];
+      source.stream
+          .combineLatest(other.stream, sum)
+          .listen(null, onError: errors.add);
+
+      source.addError(_NumberedException(1));
+      other.addError(_NumberedException(2));
+
+      source.add(1);
+      other.add(2);
+
+      await Future(() {});
+
+      expect(errors, [_isException(1), _isException(2), _isException(3)]);
+    });
+
+    group('broadcast source', () {
+      test('can cancel and relisten to broadcast stream', () async {
+        var source = StreamController<int>.broadcast();
+        var other = StreamController<int>();
+        int combine(int a, int b) => a + b;
+
+        var emittedValues = <int>[];
+        var transformed = source.stream.combineLatest(other.stream, combine);
+
+        var subscription = transformed.listen(emittedValues.add);
+
+        source.add(1);
+        other.add(2);
+        await Future(() {});
+        expect(emittedValues, [3]);
+
+        await subscription.cancel();
+
+        subscription = transformed.listen(emittedValues.add);
+        source.add(3);
+        await Future(() {});
+        expect(emittedValues, [3, 5]);
+      });
+    });
+  });
+}
+
+class _NumberedException implements Exception {
+  final int id;
+  _NumberedException(this.id);
+}
+
+Matcher _isException(int id) =>
+    const TypeMatcher<_NumberedException>().having((n) => n.id, 'id', id);
diff --git a/pkgs/stream_transform/test/concurrent_async_map_test.dart b/pkgs/stream_transform/test/concurrent_async_map_test.dart
new file mode 100644
index 0000000..1807f9f
--- /dev/null
+++ b/pkgs/stream_transform/test/concurrent_async_map_test.dart
@@ -0,0 +1,157 @@
+// Copyright (c) 2018, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  late StreamController<int> controller;
+  late List<String> emittedValues;
+  late bool valuesCanceled;
+  late bool isDone;
+  late List<String> errors;
+  late Stream<String> transformed;
+  late StreamSubscription<String> subscription;
+
+  late List<Completer<String>> finishWork;
+  late List<dynamic> values;
+
+  Future<String> convert(int value) {
+    values.add(value);
+    var completer = Completer<String>();
+    finishWork.add(completer);
+    return completer.future;
+  }
+
+  for (var streamType in streamTypes) {
+    group('concurrentAsyncMap for stream type: [$streamType]', () {
+      setUp(() {
+        valuesCanceled = false;
+        controller = createController(streamType)
+          ..onCancel = () {
+            valuesCanceled = true;
+          };
+        emittedValues = [];
+        errors = [];
+        isDone = false;
+        finishWork = [];
+        values = [];
+        transformed = controller.stream.concurrentAsyncMap(convert);
+        subscription = transformed
+            .listen(emittedValues.add, onError: errors.add, onDone: () {
+          isDone = true;
+        });
+      });
+
+      test('does not emit before convert finishes', () async {
+        controller.add(1);
+        await Future(() {});
+        expect(emittedValues, isEmpty);
+        expect(values, [1]);
+        finishWork.first.complete('result');
+        await Future(() {});
+        expect(emittedValues, ['result']);
+      });
+
+      test('allows calls to convert before the last one finished', () async {
+        controller
+          ..add(1)
+          ..add(2)
+          ..add(3);
+        await Future(() {});
+        expect(values, [1, 2, 3]);
+      });
+
+      test('forwards errors directly without waiting for previous convert',
+          () async {
+        controller.add(1);
+        await Future(() {});
+        controller.addError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      test('forwards errors which occur during the convert', () async {
+        controller.add(1);
+        await Future(() {});
+        finishWork.first.completeError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      test('can continue handling events after an error', () async {
+        controller.add(1);
+        await Future(() {});
+        finishWork[0].completeError('error');
+        controller.add(2);
+        await Future(() {});
+        expect(values, [1, 2]);
+        finishWork[1].completeError('another');
+        await Future(() {});
+        expect(errors, ['error', 'another']);
+      });
+
+      test('cancels value subscription when output canceled', () async {
+        expect(valuesCanceled, false);
+        await subscription.cancel();
+        expect(valuesCanceled, true);
+      });
+
+      test('closes when values end if no conversion is pending', () async {
+        expect(isDone, false);
+        await controller.close();
+        await Future(() {});
+        expect(isDone, true);
+      });
+
+      if (streamType == 'broadcast') {
+        test('multiple listeners all get values', () async {
+          var otherValues = <String>[];
+          transformed.listen(otherValues.add);
+          controller.add(1);
+          await Future(() {});
+          finishWork.first.complete('result');
+          await Future(() {});
+          expect(emittedValues, ['result']);
+          expect(otherValues, ['result']);
+        });
+
+        test('multiple listeners get done when values end', () async {
+          var otherDone = false;
+          transformed.listen(null, onDone: () => otherDone = true);
+          controller.add(1);
+          await Future(() {});
+          await controller.close();
+          expect(isDone, false);
+          expect(otherDone, false);
+          finishWork.first.complete('');
+          await Future(() {});
+          expect(isDone, true);
+          expect(otherDone, true);
+        });
+
+        test('can cancel and relisten', () async {
+          controller.add(1);
+          await Future(() {});
+          finishWork.first.complete('first');
+          await Future(() {});
+          await subscription.cancel();
+          controller.add(2);
+          await Future(() {});
+          subscription = transformed.listen(emittedValues.add);
+          controller.add(3);
+          await Future(() {});
+          expect(values, [1, 3]);
+          finishWork[1].complete('second');
+          await Future(() {});
+          expect(emittedValues, ['first', 'second']);
+        });
+      }
+    });
+  }
+}
diff --git a/pkgs/stream_transform/test/debounce_test.dart b/pkgs/stream_transform/test/debounce_test.dart
new file mode 100644
index 0000000..19de055
--- /dev/null
+++ b/pkgs/stream_transform/test/debounce_test.dart
@@ -0,0 +1,310 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  for (var streamType in streamTypes) {
+    group('Stream type [$streamType]', () {
+      group('debounce - trailing', () {
+        late StreamController<int> values;
+        late List<int> emittedValues;
+        late bool valuesCanceled;
+        late bool isDone;
+        late List<String> errors;
+        late StreamSubscription<int> subscription;
+        late Stream<int> transformed;
+
+        setUp(() async {
+          valuesCanceled = false;
+          values = createController(streamType)
+            ..onCancel = () {
+              valuesCanceled = true;
+            };
+          emittedValues = [];
+          errors = [];
+          isDone = false;
+          transformed = values.stream.debounce(const Duration(milliseconds: 5));
+        });
+
+        void listen() {
+          subscription = transformed
+              .listen(emittedValues.add, onError: errors.add, onDone: () {
+            isDone = true;
+          });
+        }
+
+        test('cancels values', () async {
+          listen();
+          await subscription.cancel();
+          expect(valuesCanceled, true);
+        });
+
+        test('swallows values that come faster than duration', () {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..add(2)
+              ..close();
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [2]);
+          });
+        });
+
+        test('outputs multiple values spaced further than duration', () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values.add(2);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1, 2]);
+          });
+        });
+
+        test('waits for pending value to close', () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values.close();
+            async.flushMicrotasks();
+            expect(isDone, true);
+          });
+        });
+
+        test('closes output if there are no pending values', () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values
+              ..add(2)
+              ..close();
+            async.flushMicrotasks();
+            expect(isDone, false);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(isDone, true);
+          });
+        });
+
+        if (streamType == 'broadcast') {
+          test('multiple listeners all get values', () {
+            fakeAsync((async) {
+              listen();
+              var otherValues = <int>[];
+              transformed.listen(otherValues.add);
+              values
+                ..add(1)
+                ..add(2);
+              async.elapse(const Duration(milliseconds: 6));
+              expect(emittedValues, [2]);
+              expect(otherValues, [2]);
+            });
+          });
+        }
+      });
+
+      group('debounce - leading', () {
+        late StreamController<int> values;
+        late List<int> emittedValues;
+        late Stream<int> transformed;
+        late bool isDone;
+
+        setUp(() async {
+          values = createController(streamType);
+          emittedValues = [];
+          isDone = false;
+          transformed = values.stream.debounce(const Duration(milliseconds: 5),
+              leading: true, trailing: false);
+        });
+
+        void listen() {
+          transformed.listen(emittedValues.add, onDone: () {
+            isDone = true;
+          });
+        }
+
+        test('swallows values that come faster than duration', () async {
+          listen();
+          values
+            ..add(1)
+            ..add(2);
+          await values.close();
+          expect(emittedValues, [1]);
+        });
+
+        test('outputs multiple values spaced further than duration', () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values.add(2);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1, 2]);
+          });
+        });
+
+        if (streamType == 'broadcast') {
+          test('multiple listeners all get values', () {
+            fakeAsync((async) {
+              listen();
+              var otherValues = <int>[];
+              transformed.listen(otherValues.add);
+              values
+                ..add(1)
+                ..add(2);
+              async.elapse(const Duration(milliseconds: 6));
+              expect(emittedValues, [1]);
+              expect(otherValues, [1]);
+            });
+          });
+        }
+
+        test('closes output immediately if not waiting for trailing value',
+            () async {
+          listen();
+          values.add(1);
+          await values.close();
+          expect(isDone, true);
+        });
+      });
+
+      group('debounce - leading and trailing', () {
+        late StreamController<int> values;
+        late List<int> emittedValues;
+        late Stream<int> transformed;
+
+        setUp(() async {
+          values = createController(streamType);
+          emittedValues = [];
+          transformed = values.stream.debounce(const Duration(milliseconds: 5),
+              leading: true, trailing: true);
+        });
+        void listen() {
+          transformed.listen(emittedValues.add);
+        }
+
+        test('swallows values that come faster than duration', () {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..add(2)
+              ..add(3)
+              ..close();
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1, 3]);
+          });
+        });
+
+        test('outputs multiple values spaced further than duration', () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values.add(2);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1, 2]);
+          });
+        });
+
+        if (streamType == 'broadcast') {
+          test('multiple listeners all get values', () {
+            fakeAsync((async) {
+              listen();
+              var otherValues = <int>[];
+              transformed.listen(otherValues.add);
+              values
+                ..add(1)
+                ..add(2);
+              async.elapse(const Duration(milliseconds: 6));
+              expect(emittedValues, [1, 2]);
+              expect(otherValues, [1, 2]);
+            });
+          });
+        }
+      });
+
+      group('debounceBuffer', () {
+        late StreamController<int> values;
+        late List<List<int>> emittedValues;
+        late List<String> errors;
+        late Stream<List<int>> transformed;
+
+        setUp(() async {
+          values = createController(streamType);
+          emittedValues = [];
+          errors = [];
+          transformed =
+              values.stream.debounceBuffer(const Duration(milliseconds: 5));
+        });
+        void listen() {
+          transformed.listen(emittedValues.add, onError: errors.add);
+        }
+
+        test('Emits all values as a list', () {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..add(2)
+              ..close();
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [
+              [1, 2]
+            ]);
+          });
+        });
+
+        test('separate lists for multiple values spaced further than duration',
+            () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values.add(2);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [
+              [1],
+              [2]
+            ]);
+          });
+        });
+
+        if (streamType == 'broadcast') {
+          test('multiple listeners all get values', () {
+            fakeAsync((async) {
+              listen();
+              var otherValues = <List<int>>[];
+              transformed.listen(otherValues.add);
+              values
+                ..add(1)
+                ..add(2);
+              async.elapse(const Duration(milliseconds: 6));
+              expect(emittedValues, [
+                [1, 2]
+              ]);
+              expect(otherValues, [
+                [1, 2]
+              ]);
+            });
+          });
+        }
+      });
+    });
+  }
+  test('allows nulls', () async {
+    final values = Stream<int?>.fromIterable([null]);
+    final transformed = values.debounce(const Duration(milliseconds: 1));
+    expect(await transformed.toList(), [null]);
+  });
+}
diff --git a/pkgs/stream_transform/test/followd_by_test.dart b/pkgs/stream_transform/test/followd_by_test.dart
new file mode 100644
index 0000000..d600d13
--- /dev/null
+++ b/pkgs/stream_transform/test/followd_by_test.dart
@@ -0,0 +1,159 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  for (var firstType in streamTypes) {
+    for (var secondType in streamTypes) {
+      group('followedBy [$firstType] with [$secondType]', () {
+        late StreamController<int> first;
+        late StreamController<int> second;
+
+        late List<int> emittedValues;
+        late bool firstCanceled;
+        late bool secondCanceled;
+        late bool secondListened;
+        late bool isDone;
+        late List<String> errors;
+        late Stream<int> transformed;
+        late StreamSubscription<int> subscription;
+
+        setUp(() async {
+          firstCanceled = false;
+          secondCanceled = false;
+          secondListened = false;
+          first = createController(firstType)
+            ..onCancel = () {
+              firstCanceled = true;
+            };
+          second = createController(secondType)
+            ..onCancel = () {
+              secondCanceled = true;
+            }
+            ..onListen = () {
+              secondListened = true;
+            };
+          emittedValues = [];
+          errors = [];
+          isDone = false;
+          transformed = first.stream.followedBy(second.stream);
+          subscription = transformed
+              .listen(emittedValues.add, onError: errors.add, onDone: () {
+            isDone = true;
+          });
+        });
+
+        test('adds all values from both streams', () async {
+          first
+            ..add(1)
+            ..add(2);
+          await first.close();
+          await Future(() {});
+          second
+            ..add(3)
+            ..add(4);
+          await Future(() {});
+          expect(emittedValues, [1, 2, 3, 4]);
+        });
+
+        test('Does not listen to second stream before first stream finishes',
+            () async {
+          expect(secondListened, false);
+          await first.close();
+          expect(secondListened, true);
+        });
+
+        test('closes stream after both inputs close', () async {
+          await first.close();
+          await second.close();
+          expect(isDone, true);
+        });
+
+        test('cancels any type of first stream on cancel', () async {
+          await subscription.cancel();
+          expect(firstCanceled, true);
+        });
+
+        if (firstType == 'single subscription') {
+          test(
+              'cancels any type of second stream on cancel if first is '
+              'broadcast', () async {
+            await first.close();
+            await subscription.cancel();
+            expect(secondCanceled, true);
+          });
+
+          if (secondType == 'broadcast') {
+            test('can pause and resume during second stream - dropping values',
+                () async {
+              await first.close();
+              subscription.pause();
+              second.add(1);
+              await Future(() {});
+              subscription.resume();
+              second.add(2);
+              await Future(() {});
+              expect(emittedValues, [2]);
+            });
+          } else {
+            test('can pause and resume during second stream - buffering values',
+                () async {
+              await first.close();
+              subscription.pause();
+              second.add(1);
+              await Future(() {});
+              subscription.resume();
+              second.add(2);
+              await Future(() {});
+              expect(emittedValues, [1, 2]);
+            });
+          }
+        }
+
+        if (firstType == 'broadcast') {
+          test('can cancel and relisten during first stream', () async {
+            await subscription.cancel();
+            first.add(1);
+            subscription = transformed.listen(emittedValues.add);
+            first.add(2);
+            await Future(() {});
+            expect(emittedValues, [2]);
+          });
+
+          test('can cancel and relisten during second stream', () async {
+            await first.close();
+            await subscription.cancel();
+            second.add(2);
+            await Future(() {});
+            subscription = transformed.listen(emittedValues.add);
+            second.add(3);
+            await Future(() {});
+            expect(emittedValues, [3]);
+          });
+
+          test('forwards values to multiple listeners', () async {
+            var otherValues = <int>[];
+            transformed.listen(otherValues.add);
+            first.add(1);
+            await first.close();
+            second.add(2);
+            await Future(() {});
+            var thirdValues = <int>[];
+            transformed.listen(thirdValues.add);
+            second.add(3);
+            await Future(() {});
+            expect(emittedValues, [1, 2, 3]);
+            expect(otherValues, [1, 2, 3]);
+            expect(thirdValues, [3]);
+          });
+        }
+      });
+    }
+  }
+}
diff --git a/pkgs/stream_transform/test/from_handlers_test.dart b/pkgs/stream_transform/test/from_handlers_test.dart
new file mode 100644
index 0000000..694199c
--- /dev/null
+++ b/pkgs/stream_transform/test/from_handlers_test.dart
@@ -0,0 +1,183 @@
+// Copyright (c) 2017, 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:stream_transform/src/from_handlers.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late StreamController<int> values;
+  late List<int> emittedValues;
+  late bool valuesCanceled;
+  late bool isDone;
+  late List<String> errors;
+  late Stream<int> transformed;
+  late StreamSubscription<int> subscription;
+
+  void setUpForController(StreamController<int> controller,
+      Stream<int> Function(Stream<int>) transform) {
+    valuesCanceled = false;
+    values = controller
+      ..onCancel = () {
+        valuesCanceled = true;
+      };
+    emittedValues = [];
+    errors = [];
+    isDone = false;
+    transformed = transform(values.stream);
+    subscription =
+        transformed.listen(emittedValues.add, onError: errors.add, onDone: () {
+      isDone = true;
+    });
+  }
+
+  group('default from_handlers', () {
+    group('Single subscription stream', () {
+      setUp(() {
+        setUpForController(StreamController(),
+            (s) => s.transformByHandlers(onData: (e, sink) => sink.add(e)));
+      });
+
+      test('has correct stream type', () {
+        expect(transformed.isBroadcast, false);
+      });
+
+      test('forwards values', () async {
+        values
+          ..add(1)
+          ..add(2);
+        await Future(() {});
+        expect(emittedValues, [1, 2]);
+      });
+
+      test('forwards errors', () async {
+        values.addError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      test('forwards done', () async {
+        await values.close();
+        expect(isDone, true);
+      });
+
+      test('forwards cancel', () async {
+        await subscription.cancel();
+        expect(valuesCanceled, true);
+      });
+    });
+
+    group('broadcast stream with muliple listeners', () {
+      late List<int> emittedValues2;
+      late List<String> errors2;
+      late bool isDone2;
+      late StreamSubscription<int> subscription2;
+
+      setUp(() {
+        setUpForController(StreamController.broadcast(),
+            (s) => s.transformByHandlers(onData: (e, sink) => sink.add(e)));
+        emittedValues2 = [];
+        errors2 = [];
+        isDone2 = false;
+        subscription2 = transformed
+            .listen(emittedValues2.add, onError: errors2.add, onDone: () {
+          isDone2 = true;
+        });
+      });
+
+      test('has correct stream type', () {
+        expect(transformed.isBroadcast, true);
+      });
+
+      test('forwards values', () async {
+        values
+          ..add(1)
+          ..add(2);
+        await Future(() {});
+        expect(emittedValues, [1, 2]);
+        expect(emittedValues2, [1, 2]);
+      });
+
+      test('forwards errors', () async {
+        values.addError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+        expect(errors2, ['error']);
+      });
+
+      test('forwards done', () async {
+        await values.close();
+        expect(isDone, true);
+        expect(isDone2, true);
+      });
+
+      test('forwards cancel', () async {
+        await subscription.cancel();
+        expect(valuesCanceled, false);
+        await subscription2.cancel();
+        expect(valuesCanceled, true);
+      });
+    });
+  });
+
+  group('custom handlers', () {
+    group('single subscription', () {
+      setUp(() async {
+        setUpForController(
+            StreamController(),
+            (s) => s.transformByHandlers(onData: (value, sink) {
+                  sink.add(value + 1);
+                }));
+      });
+      test('uses transform from handleData', () async {
+        values
+          ..add(1)
+          ..add(2);
+        await Future(() {});
+        expect(emittedValues, [2, 3]);
+      });
+    });
+
+    group('broadcast stream with multiple listeners', () {
+      late int dataCallCount;
+      late int doneCallCount;
+      late int errorCallCount;
+
+      setUp(() async {
+        dataCallCount = 0;
+        doneCallCount = 0;
+        errorCallCount = 0;
+        setUpForController(
+            StreamController.broadcast(),
+            (s) => s.transformByHandlers(onData: (value, sink) {
+                  dataCallCount++;
+                }, onError: (error, stackTrace, sink) {
+                  errorCallCount++;
+                  sink.addError(error, stackTrace);
+                }, onDone: (sink) {
+                  doneCallCount++;
+                }));
+        transformed.listen((_) {}, onError: (_, __) {});
+      });
+
+      test('handles data once', () async {
+        values.add(1);
+        await Future(() {});
+        expect(dataCallCount, 1);
+      });
+
+      test('handles done once', () async {
+        await values.close();
+        expect(doneCallCount, 1);
+      });
+
+      test('handles errors once', () async {
+        values.addError('error');
+        await Future(() {});
+        expect(errorCallCount, 1);
+      });
+    });
+  });
+}
diff --git a/pkgs/stream_transform/test/merge_test.dart b/pkgs/stream_transform/test/merge_test.dart
new file mode 100644
index 0000000..ecbf97f
--- /dev/null
+++ b/pkgs/stream_transform/test/merge_test.dart
@@ -0,0 +1,140 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('merge', () {
+    test('includes all values', () async {
+      var first = Stream.fromIterable([1, 2, 3]);
+      var second = Stream.fromIterable([4, 5, 6]);
+      var allValues = await first.merge(second).toList();
+      expect(allValues, containsAllInOrder([1, 2, 3]));
+      expect(allValues, containsAllInOrder([4, 5, 6]));
+      expect(allValues, hasLength(6));
+    });
+
+    test('cancels both sources', () async {
+      var firstCanceled = false;
+      var first = StreamController<int>()
+        ..onCancel = () {
+          firstCanceled = true;
+        };
+      var secondCanceled = false;
+      var second = StreamController<int>()
+        ..onCancel = () {
+          secondCanceled = true;
+        };
+      var subscription = first.stream.merge(second.stream).listen((_) {});
+      await subscription.cancel();
+      expect(firstCanceled, true);
+      expect(secondCanceled, true);
+    });
+
+    test('completes when both sources complete', () async {
+      var first = StreamController<int>();
+      var second = StreamController<int>();
+      var isDone = false;
+      first.stream.merge(second.stream).listen((_) {}, onDone: () {
+        isDone = true;
+      });
+      await first.close();
+      expect(isDone, false);
+      await second.close();
+      expect(isDone, true);
+    });
+
+    test('can cancel and relisten to broadcast stream', () async {
+      var first = StreamController<int>.broadcast();
+      var second = StreamController<int>();
+      var emittedValues = <int>[];
+      var transformed = first.stream.merge(second.stream);
+      var subscription = transformed.listen(emittedValues.add);
+      first.add(1);
+      second.add(2);
+      await Future(() {});
+      expect(emittedValues, contains(1));
+      expect(emittedValues, contains(2));
+      await subscription.cancel();
+      emittedValues = [];
+      subscription = transformed.listen(emittedValues.add);
+      first.add(3);
+      second.add(4);
+      await Future(() {});
+      expect(emittedValues, contains(3));
+      expect(emittedValues, contains(4));
+    });
+  });
+
+  group('mergeAll', () {
+    test('includes all values', () async {
+      var first = Stream.fromIterable([1, 2, 3]);
+      var second = Stream.fromIterable([4, 5, 6]);
+      var third = Stream.fromIterable([7, 8, 9]);
+      var allValues = await first.mergeAll([second, third]).toList();
+      expect(allValues, containsAllInOrder([1, 2, 3]));
+      expect(allValues, containsAllInOrder([4, 5, 6]));
+      expect(allValues, containsAllInOrder([7, 8, 9]));
+      expect(allValues, hasLength(9));
+    });
+
+    test('handles mix of broadcast and single-subscription', () async {
+      var firstCanceled = false;
+      var first = StreamController<int>.broadcast()
+        ..onCancel = () {
+          firstCanceled = true;
+        };
+      var secondBroadcastCanceled = false;
+      var secondBroadcast = StreamController<int>.broadcast()
+        ..onCancel = () {
+          secondBroadcastCanceled = true;
+        };
+      var secondSingleCanceled = false;
+      var secondSingle = StreamController<int>()
+        ..onCancel = () {
+          secondSingleCanceled = true;
+        };
+
+      var merged =
+          first.stream.mergeAll([secondBroadcast.stream, secondSingle.stream]);
+
+      var firstListenerValues = <int>[];
+      var secondListenerValues = <int>[];
+
+      var firstSubscription = merged.listen(firstListenerValues.add);
+      var secondSubscription = merged.listen(secondListenerValues.add);
+
+      first.add(1);
+      secondBroadcast.add(2);
+      secondSingle.add(3);
+
+      await Future(() {});
+      await firstSubscription.cancel();
+
+      expect(firstCanceled, false);
+      expect(secondBroadcastCanceled, false);
+      expect(secondSingleCanceled, false);
+
+      first.add(4);
+      secondBroadcast.add(5);
+      secondSingle.add(6);
+
+      await Future(() {});
+      await secondSubscription.cancel();
+
+      await Future(() {});
+      expect(firstCanceled, true);
+      expect(secondBroadcastCanceled, true);
+      expect(secondSingleCanceled, false,
+          reason: 'Single subscription streams merged into broadcast streams '
+              'are not canceled');
+
+      expect(firstListenerValues, [1, 2, 3]);
+      expect(secondListenerValues, [1, 2, 3, 4, 5, 6]);
+    });
+  });
+}
diff --git a/pkgs/stream_transform/test/sample_test.dart b/pkgs/stream_transform/test/sample_test.dart
new file mode 100644
index 0000000..66ca09d
--- /dev/null
+++ b/pkgs/stream_transform/test/sample_test.dart
@@ -0,0 +1,291 @@
+// Copyright (c) 2022, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  late StreamController<void> trigger;
+  late StreamController<int> values;
+  late List<int> emittedValues;
+  late bool valuesCanceled;
+  late bool triggerCanceled;
+  late bool triggerPaused;
+  late bool isDone;
+  late List<String> errors;
+  late Stream<int> transformed;
+  late StreamSubscription<int> subscription;
+
+  void setUpForStreamTypes(String triggerType, String valuesType,
+      {required bool longPoll}) {
+    valuesCanceled = false;
+    triggerCanceled = false;
+    triggerPaused = false;
+    trigger = createController(triggerType)
+      ..onCancel = () {
+        triggerCanceled = true;
+      };
+    if (triggerType == 'single subscription') {
+      trigger.onPause = () {
+        triggerPaused = true;
+      };
+    }
+    values = createController(valuesType)
+      ..onCancel = () {
+        valuesCanceled = true;
+      };
+    emittedValues = [];
+    errors = [];
+    isDone = false;
+    transformed = values.stream.sample(trigger.stream, longPoll: longPoll);
+    subscription =
+        transformed.listen(emittedValues.add, onError: errors.add, onDone: () {
+      isDone = true;
+    });
+  }
+
+  for (var triggerType in streamTypes) {
+    for (var valuesType in streamTypes) {
+      group('Trigger type: [$triggerType], Values type: [$valuesType]', () {
+        group('general behavior', () {
+          setUp(() {
+            setUpForStreamTypes(triggerType, valuesType, longPoll: true);
+          });
+
+          test('does not emit before `trigger`', () async {
+            values.add(1);
+            await Future(() {});
+            expect(emittedValues, isEmpty);
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [1]);
+          });
+
+          test('keeps most recent event between triggers', () async {
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            trigger.add(null);
+            values
+              ..add(3)
+              ..add(4);
+            await Future(() {});
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [2, 4]);
+          });
+
+          test('cancels value subscription when output canceled', () async {
+            expect(valuesCanceled, false);
+            await subscription.cancel();
+            expect(valuesCanceled, true);
+          });
+
+          test('closes when trigger ends', () async {
+            expect(isDone, false);
+            await trigger.close();
+            await Future(() {});
+            expect(isDone, true);
+          });
+
+          test('closes after outputting final values when source closes',
+              () async {
+            expect(isDone, false);
+            values.add(1);
+            await values.close();
+            expect(isDone, false);
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [1]);
+            expect(isDone, true);
+          });
+
+          test('closes when source closes and there is no pending', () async {
+            expect(isDone, false);
+            await values.close();
+            await Future(() {});
+            expect(isDone, true);
+          });
+
+          test('forwards errors from trigger', () async {
+            trigger.addError('error');
+            await Future(() {});
+            expect(errors, ['error']);
+          });
+
+          test('forwards errors from values', () async {
+            values.addError('error');
+            await Future(() {});
+            expect(errors, ['error']);
+          });
+        });
+
+        group('long polling', () {
+          setUp(() {
+            setUpForStreamTypes(triggerType, valuesType, longPoll: true);
+          });
+
+          test('emits immediately if trigger emits before a value', () async {
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, isEmpty);
+            values.add(1);
+            await Future(() {});
+            expect(emittedValues, [1]);
+          });
+
+          test('two triggers in a row - emit buffere then emit next value',
+              () async {
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            trigger
+              ..add(null)
+              ..add(null);
+            await Future(() {});
+            values.add(3);
+            await Future(() {});
+            expect(emittedValues, [2, 3]);
+          });
+
+          test('pre-emptive trigger then trigger after values', () async {
+            trigger.add(null);
+            await Future(() {});
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [1, 2]);
+          });
+
+          test('multiple pre-emptive triggers, only emits first value',
+              () async {
+            trigger
+              ..add(null)
+              ..add(null);
+            await Future(() {});
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            expect(emittedValues, [1]);
+          });
+
+          test('closes if there is no waiting long poll when source closes',
+              () async {
+            expect(isDone, false);
+            values.add(1);
+            trigger.add(null);
+            await values.close();
+            await Future(() {});
+            expect(isDone, true);
+          });
+
+          test('waits to emit if there waiting long poll when trigger closes',
+              () async {
+            trigger.add(null);
+            await trigger.close();
+            expect(isDone, false);
+            values.add(1);
+            await Future(() {});
+            expect(emittedValues, [1]);
+            expect(isDone, true);
+          });
+        });
+
+        group('immediate polling', () {
+          setUp(() {
+            setUpForStreamTypes(triggerType, valuesType, longPoll: false);
+          });
+
+          test('ignores trigger before values', () async {
+            trigger.add(null);
+            await Future(() {});
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [2]);
+          });
+
+          test('ignores trigger if no pending values', () async {
+            values
+              ..add(1)
+              ..add(2);
+            await Future(() {});
+            trigger
+              ..add(null)
+              ..add(null);
+            await Future(() {});
+            values
+              ..add(3)
+              ..add(4);
+            await Future(() {});
+            trigger.add(null);
+            await Future(() {});
+            expect(emittedValues, [2, 4]);
+          });
+        });
+      });
+    }
+  }
+
+  test('always cancels trigger if values is singlesubscription', () async {
+    setUpForStreamTypes('broadcast', 'single subscription', longPoll: true);
+    expect(triggerCanceled, false);
+    await subscription.cancel();
+    expect(triggerCanceled, true);
+
+    setUpForStreamTypes('single subscription', 'single subscription',
+        longPoll: true);
+    expect(triggerCanceled, false);
+    await subscription.cancel();
+    expect(triggerCanceled, true);
+  });
+
+  test('cancels trigger if trigger is broadcast', () async {
+    setUpForStreamTypes('broadcast', 'broadcast', longPoll: true);
+    expect(triggerCanceled, false);
+    await subscription.cancel();
+    expect(triggerCanceled, true);
+  });
+
+  test('pauses single subscription trigger for broadcast values', () async {
+    setUpForStreamTypes('single subscription', 'broadcast', longPoll: true);
+    expect(triggerCanceled, false);
+    expect(triggerPaused, false);
+    await subscription.cancel();
+    expect(triggerCanceled, false);
+    expect(triggerPaused, true);
+  });
+
+  for (var triggerType in streamTypes) {
+    test('cancel and relisten with [$triggerType] trigger', () async {
+      setUpForStreamTypes(triggerType, 'broadcast', longPoll: true);
+      values.add(1);
+      trigger.add(null);
+      await Future(() {});
+      expect(emittedValues, [1]);
+      await subscription.cancel();
+      values.add(2);
+      trigger.add(null);
+      await Future(() {});
+      subscription = transformed.listen(emittedValues.add);
+      values.add(3);
+      trigger.add(null);
+      await Future(() {});
+      expect(emittedValues, [1, 3]);
+    });
+  }
+}
diff --git a/pkgs/stream_transform/test/scan_test.dart b/pkgs/stream_transform/test/scan_test.dart
new file mode 100644
index 0000000..3c749e7
--- /dev/null
+++ b/pkgs/stream_transform/test/scan_test.dart
@@ -0,0 +1,109 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('Scan', () {
+    test('produces intermediate values', () async {
+      var source = Stream.fromIterable([1, 2, 3, 4]);
+      int sum(int x, int y) => x + y;
+      var result = await source.scan(0, sum).toList();
+
+      expect(result, [1, 3, 6, 10]);
+    });
+
+    test('can create a broadcast stream', () {
+      var source = StreamController<int>.broadcast();
+
+      var transformed = source.stream.scan(null, (_, __) {});
+
+      expect(transformed.isBroadcast, true);
+    });
+
+    test('forwards errors from source', () async {
+      var source = StreamController<int>();
+
+      int sum(int x, int y) => x + y;
+
+      var errors = <Object>[];
+
+      source.stream.scan(0, sum).listen(null, onError: errors.add);
+
+      source.addError(StateError('fail'));
+      await Future(() {});
+
+      expect(errors, [isStateError]);
+    });
+
+    group('with async combine', () {
+      test('returns a Stream of non-futures', () async {
+        var source = Stream.fromIterable([1, 2, 3, 4]);
+        Future<int> sum(int x, int y) async => x + y;
+        var result = await source.scan(0, sum).toList();
+
+        expect(result, [1, 3, 6, 10]);
+      });
+
+      test('can return a Stream of futures when specified', () async {
+        var source = Stream.fromIterable([1, 2]);
+        Future<int> sum(Future<int> x, int y) async => (await x) + y;
+        var result =
+            await source.scan<Future<int>>(Future.value(0), sum).toList();
+
+        expect(result, [
+          const TypeMatcher<Future<void>>(),
+          const TypeMatcher<Future<void>>()
+        ]);
+        expect(await result.wait, [1, 3]);
+      });
+
+      test('does not call for subsequent values while waiting', () async {
+        var source = StreamController<int>();
+
+        var calledWith = <int>[];
+        var block = Completer<void>();
+        Future<int> combine(int x, int y) async {
+          calledWith.add(y);
+          await block.future;
+          return x + y;
+        }
+
+        var results = <int>[];
+
+        unawaited(source.stream.scan(0, combine).forEach(results.add));
+
+        source
+          ..add(1)
+          ..add(2);
+        await Future(() {});
+        expect(calledWith, [1]);
+        expect(results, isEmpty);
+
+        block.complete();
+        await Future(() {});
+        expect(calledWith, [1, 2]);
+        expect(results, [1, 3]);
+      });
+
+      test('forwards async errors', () async {
+        var source = StreamController<int>();
+
+        Future<int> combine(int x, int y) async => throw StateError('fail');
+
+        var errors = <Object>[];
+
+        source.stream.scan(0, combine).listen(null, onError: errors.add);
+
+        source.add(1);
+        await Future(() {});
+
+        expect(errors, [isStateError]);
+      });
+    });
+  });
+}
diff --git a/pkgs/stream_transform/test/start_with_test.dart b/pkgs/stream_transform/test/start_with_test.dart
new file mode 100644
index 0000000..35f0330
--- /dev/null
+++ b/pkgs/stream_transform/test/start_with_test.dart
@@ -0,0 +1,167 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  late StreamController<int> values;
+  late Stream<int> transformed;
+  late StreamSubscription<int> subscription;
+
+  late List<int> emittedValues;
+  late bool isDone;
+
+  void setupForStreamType(
+      String streamType, Stream<int> Function(Stream<int>) transform) {
+    emittedValues = [];
+    isDone = false;
+    values = createController(streamType);
+    transformed = transform(values.stream);
+    subscription =
+        transformed.listen(emittedValues.add, onDone: () => isDone = true);
+  }
+
+  for (var streamType in streamTypes) {
+    group('startWith then [$streamType]', () {
+      setUp(() => setupForStreamType(streamType, (s) => s.startWith(1)));
+
+      test('outputs all values', () async {
+        values
+          ..add(2)
+          ..add(3);
+        await Future(() {});
+        expect(emittedValues, [1, 2, 3]);
+      });
+
+      test('outputs initial when followed by empty stream', () async {
+        await values.close();
+        expect(emittedValues, [1]);
+      });
+
+      test('closes with values', () async {
+        expect(isDone, false);
+        await values.close();
+        expect(isDone, true);
+      });
+
+      if (streamType == 'broadcast') {
+        test('can cancel and relisten', () async {
+          values.add(2);
+          await Future(() {});
+          await subscription.cancel();
+          subscription = transformed.listen(emittedValues.add);
+          values.add(3);
+          await Future(() {});
+          await Future(() {});
+          expect(emittedValues, [1, 2, 3]);
+        });
+      }
+    });
+
+    group('startWithMany then [$streamType]', () {
+      setUp(() async {
+        setupForStreamType(streamType, (s) => s.startWithMany([1, 2]));
+        // Ensure all initial values go through
+        await Future(() {});
+      });
+
+      test('outputs all values', () async {
+        values
+          ..add(3)
+          ..add(4);
+        await Future(() {});
+        expect(emittedValues, [1, 2, 3, 4]);
+      });
+
+      test('outputs initial when followed by empty stream', () async {
+        await values.close();
+        expect(emittedValues, [1, 2]);
+      });
+
+      test('closes with values', () async {
+        expect(isDone, false);
+        await values.close();
+        expect(isDone, true);
+      });
+
+      if (streamType == 'broadcast') {
+        test('can cancel and relisten', () async {
+          values.add(3);
+          await Future(() {});
+          await subscription.cancel();
+          subscription = transformed.listen(emittedValues.add);
+          values.add(4);
+          await Future(() {});
+          expect(emittedValues, [1, 2, 3, 4]);
+        });
+      }
+    });
+
+    for (var startingStreamType in streamTypes) {
+      group('startWithStream [$startingStreamType] then [$streamType]', () {
+        late StreamController<int> starting;
+        setUp(() async {
+          starting = createController(startingStreamType);
+          setupForStreamType(
+              streamType, (s) => s.startWithStream(starting.stream));
+        });
+
+        test('outputs all values', () async {
+          starting
+            ..add(1)
+            ..add(2);
+          await starting.close();
+          values
+            ..add(3)
+            ..add(4);
+          await Future(() {});
+          expect(emittedValues, [1, 2, 3, 4]);
+        });
+
+        test('closes with values', () async {
+          expect(isDone, false);
+          await starting.close();
+          expect(isDone, false);
+          await values.close();
+          expect(isDone, true);
+        });
+
+        if (streamType == 'broadcast') {
+          test('can cancel and relisten during starting', () async {
+            starting.add(1);
+            await Future(() {});
+            await subscription.cancel();
+            subscription = transformed.listen(emittedValues.add);
+            starting.add(2);
+            await starting.close();
+            values
+              ..add(3)
+              ..add(4);
+            await Future(() {});
+            expect(emittedValues, [1, 2, 3, 4]);
+          });
+
+          test('can cancel and relisten during values', () async {
+            starting
+              ..add(1)
+              ..add(2);
+            await starting.close();
+            values.add(3);
+            await Future(() {});
+            await subscription.cancel();
+            subscription = transformed.listen(emittedValues.add);
+            values.add(4);
+            await Future(() {});
+            expect(emittedValues, [1, 2, 3, 4]);
+          });
+        }
+      });
+    }
+  }
+}
diff --git a/pkgs/stream_transform/test/switch_test.dart b/pkgs/stream_transform/test/switch_test.dart
new file mode 100644
index 0000000..9e70c08
--- /dev/null
+++ b/pkgs/stream_transform/test/switch_test.dart
@@ -0,0 +1,229 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  for (var outerType in streamTypes) {
+    for (var innerType in streamTypes) {
+      group('Outer type: [$outerType], Inner type: [$innerType]', () {
+        late StreamController<int> first;
+        late StreamController<int> second;
+        late StreamController<int> third;
+        late StreamController<Stream<int>> outer;
+
+        late List<int> emittedValues;
+        late bool firstCanceled;
+        late bool outerCanceled;
+        late bool isDone;
+        late List<String> errors;
+        late StreamSubscription<int> subscription;
+
+        setUp(() async {
+          firstCanceled = false;
+          outerCanceled = false;
+          outer = createController(outerType)
+            ..onCancel = () {
+              outerCanceled = true;
+            };
+          first = createController(innerType)
+            ..onCancel = () {
+              firstCanceled = true;
+            };
+          second = createController(innerType);
+          third = createController(innerType);
+          emittedValues = [];
+          errors = [];
+          isDone = false;
+          subscription = outer.stream
+              .switchLatest()
+              .listen(emittedValues.add, onError: errors.add, onDone: () {
+            isDone = true;
+          });
+        });
+
+        test('forwards events', () async {
+          outer.add(first.stream);
+          await Future(() {});
+          first
+            ..add(1)
+            ..add(2);
+          await Future(() {});
+
+          outer.add(second.stream);
+          await Future(() {});
+          second
+            ..add(3)
+            ..add(4);
+          await Future(() {});
+
+          expect(emittedValues, [1, 2, 3, 4]);
+        });
+
+        test('forwards errors from outer Stream', () async {
+          outer.addError('error');
+          await Future(() {});
+          expect(errors, ['error']);
+        });
+
+        test('forwards errors from inner Stream', () async {
+          outer.add(first.stream);
+          await Future(() {});
+          first.addError('error');
+          await Future(() {});
+          expect(errors, ['error']);
+        });
+
+        test('closes when final stream is done', () async {
+          outer.add(first.stream);
+          await Future(() {});
+
+          outer.add(second.stream);
+          await Future(() {});
+
+          await outer.close();
+          expect(isDone, false);
+
+          await second.close();
+          expect(isDone, true);
+        });
+
+        test(
+            'closes when outer stream closes if latest inner stream already '
+            'closed', () async {
+          outer.add(first.stream);
+          await Future(() {});
+          await first.close();
+          expect(isDone, false);
+
+          await outer.close();
+          expect(isDone, true);
+        });
+
+        test('cancels listeners on previous streams', () async {
+          outer.add(first.stream);
+          await Future(() {});
+
+          outer.add(second.stream);
+          await Future(() {});
+          expect(firstCanceled, true);
+        });
+
+        if (innerType != 'broadcast') {
+          test('waits for cancel before listening to subsequent stream',
+              () async {
+            var cancelWork = Completer<void>();
+            first.onCancel = () => cancelWork.future;
+            outer.add(first.stream);
+            await Future(() {});
+
+            var cancelDone = false;
+            second.onListen = expectAsync0(() {
+              expect(cancelDone, true);
+            });
+            outer.add(second.stream);
+            await Future(() {});
+            cancelWork.complete();
+            cancelDone = true;
+          });
+
+          test('all streams are listened to, even while cancelling', () async {
+            var cancelWork = Completer<void>();
+            first.onCancel = () => cancelWork.future;
+            outer.add(first.stream);
+            await Future(() {});
+
+            var cancelDone = false;
+            second.onListen = expectAsync0(() {
+              expect(cancelDone, true);
+            });
+            third.onListen = expectAsync0(() {
+              expect(cancelDone, true);
+            });
+            outer
+              ..add(second.stream)
+              ..add(third.stream);
+            await Future(() {});
+            cancelWork.complete();
+            cancelDone = true;
+          });
+        }
+
+        if (outerType != 'broadcast' && innerType != 'broadcast') {
+          test('pausing while cancelling an inner stream is respected',
+              () async {
+            var cancelWork = Completer<void>();
+            first.onCancel = () => cancelWork.future;
+            outer.add(first.stream);
+            await Future(() {});
+
+            var cancelDone = false;
+            second.onListen = expectAsync0(() {
+              expect(cancelDone, true);
+            });
+            outer.add(second.stream);
+            await Future(() {});
+            subscription.pause();
+            cancelWork.complete();
+            cancelDone = true;
+            await Future(() {});
+            expect(second.isPaused, true);
+            subscription.resume();
+          });
+        }
+
+        test('cancels listener on current and outer stream on cancel',
+            () async {
+          outer.add(first.stream);
+          await Future(() {});
+          await subscription.cancel();
+
+          await Future(() {});
+          expect(outerCanceled, true);
+          expect(firstCanceled, true);
+        });
+      });
+    }
+  }
+
+  group('switchMap', () {
+    test('uses map function', () async {
+      var outer = StreamController<List<int>>();
+
+      var values = <int>[];
+      outer.stream.switchMap(Stream.fromIterable).listen(values.add);
+
+      outer.add([1, 2, 3]);
+      await Future(() {});
+      outer.add([4, 5, 6]);
+      await Future(() {});
+      expect(values, [1, 2, 3, 4, 5, 6]);
+    });
+
+    test('can create a broadcast stream', () async {
+      var outer = StreamController<int>.broadcast();
+
+      var transformed =
+          outer.stream.switchMap((_) => const Stream<int>.empty());
+
+      expect(transformed.isBroadcast, true);
+    });
+
+    test('forwards errors from the convert callback', () async {
+      var errors = <String>[];
+      var source = Stream.fromIterable([1, 2, 3]);
+      source.switchMap<int>((i) {
+        // ignore: only_throw_errors
+        throw 'Error: $i';
+      }).listen((_) {}, onError: errors.add);
+      await Future<void>(() {});
+      expect(errors, ['Error: 1', 'Error: 2', 'Error: 3']);
+    });
+  });
+}
diff --git a/pkgs/stream_transform/test/take_until_test.dart b/pkgs/stream_transform/test/take_until_test.dart
new file mode 100644
index 0000000..982b3da
--- /dev/null
+++ b/pkgs/stream_transform/test/take_until_test.dart
@@ -0,0 +1,135 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  for (var streamType in streamTypes) {
+    group('takeUntil on Stream type [$streamType]', () {
+      late StreamController<int> values;
+      late List<int> emittedValues;
+      late bool valuesCanceled;
+      late bool isDone;
+      late List<String> errors;
+      late Stream<int> transformed;
+      late StreamSubscription<int> subscription;
+      late Completer<void> closeTrigger;
+
+      setUp(() {
+        valuesCanceled = false;
+        values = createController(streamType)
+          ..onCancel = () {
+            valuesCanceled = true;
+          };
+        emittedValues = [];
+        errors = [];
+        isDone = false;
+        closeTrigger = Completer();
+        transformed = values.stream.takeUntil(closeTrigger.future);
+        subscription = transformed
+            .listen(emittedValues.add, onError: errors.add, onDone: () {
+          isDone = true;
+        });
+      });
+
+      test('forwards cancellation', () async {
+        await subscription.cancel();
+        expect(valuesCanceled, true);
+      });
+
+      test('lets values through before trigger', () async {
+        values
+          ..add(1)
+          ..add(2);
+        await Future(() {});
+        expect(emittedValues, [1, 2]);
+      });
+
+      test('forwards errors', () async {
+        values.addError('error');
+        await Future(() {});
+        expect(errors, ['error']);
+      });
+
+      test('sends done if original strem ends', () async {
+        await values.close();
+        expect(isDone, true);
+      });
+
+      test('sends done when trigger fires', () async {
+        closeTrigger.complete();
+        await Future(() {});
+        expect(isDone, true);
+      });
+
+      test('forwards errors from the close trigger', () async {
+        closeTrigger.completeError('sad');
+        await Future(() {});
+        expect(errors, ['sad']);
+        expect(isDone, true);
+      });
+
+      test('ignores errors from the close trigger after stream closed',
+          () async {
+        await values.close();
+        closeTrigger.completeError('sad');
+        await Future(() {});
+        expect(errors, <Object>[]);
+      });
+
+      test('cancels value subscription when trigger fires', () async {
+        closeTrigger.complete();
+        await Future(() {});
+        expect(valuesCanceled, true);
+      });
+
+      if (streamType == 'broadcast') {
+        test('multiple listeners all get values', () async {
+          var otherValues = <Object>[];
+          transformed.listen(otherValues.add);
+          values
+            ..add(1)
+            ..add(2);
+          await Future(() {});
+          expect(emittedValues, [1, 2]);
+          expect(otherValues, [1, 2]);
+        });
+
+        test('multiple listeners get done when trigger fires', () async {
+          var otherDone = false;
+          transformed.listen(null, onDone: () => otherDone = true);
+          closeTrigger.complete();
+          await Future(() {});
+          expect(otherDone, true);
+          expect(isDone, true);
+        });
+
+        test('multiple listeners get done when values end', () async {
+          var otherDone = false;
+          transformed.listen(null, onDone: () => otherDone = true);
+          await values.close();
+          expect(otherDone, true);
+          expect(isDone, true);
+        });
+
+        test('can cancel and relisten before trigger fires', () async {
+          values.add(1);
+          await Future(() {});
+          await subscription.cancel();
+          values.add(2);
+          await Future(() {});
+          subscription = transformed.listen(emittedValues.add);
+          values.add(3);
+          await Future(() {});
+          expect(emittedValues, [1, 3]);
+        });
+      }
+    });
+  }
+}
diff --git a/pkgs/stream_transform/test/tap_test.dart b/pkgs/stream_transform/test/tap_test.dart
new file mode 100644
index 0000000..f2b4346
--- /dev/null
+++ b/pkgs/stream_transform/test/tap_test.dart
@@ -0,0 +1,116 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('calls function for values', () async {
+    var valuesSeen = <int>[];
+    var stream = Stream.fromIterable([1, 2, 3]);
+    await stream.tap(valuesSeen.add).last;
+    expect(valuesSeen, [1, 2, 3]);
+  });
+
+  test('forwards values', () async {
+    var stream = Stream.fromIterable([1, 2, 3]);
+    var values = await stream.tap((_) {}).toList();
+    expect(values, [1, 2, 3]);
+  });
+
+  test('calls function for errors', () async {
+    dynamic error;
+    var source = StreamController<int>();
+    source.stream.tap((_) {}, onError: (e, st) {
+      error = e;
+    }).listen((_) {}, onError: (_) {});
+    source.addError('error');
+    await Future(() {});
+    expect(error, 'error');
+  });
+
+  test('forwards errors', () async {
+    dynamic error;
+    var source = StreamController<int>();
+    source.stream.tap((_) {}, onError: (e, st) {}).listen((_) {},
+        onError: (Object e) {
+      error = e;
+    });
+    source.addError('error');
+    await Future(() {});
+    expect(error, 'error');
+  });
+
+  test('calls function on done', () async {
+    var doneCalled = false;
+    var source = StreamController<int>();
+    source.stream.tap((_) {}, onDone: () {
+      doneCalled = true;
+    }).listen((_) {});
+    await source.close();
+    expect(doneCalled, true);
+  });
+
+  test('forwards only once with multiple listeners on a broadcast stream',
+      () async {
+    var dataCallCount = 0;
+    var source = StreamController<int>.broadcast();
+    source.stream.tap((_) {
+      dataCallCount++;
+    })
+      ..listen((_) {})
+      ..listen((_) {});
+    source.add(1);
+    await Future(() {});
+    expect(dataCallCount, 1);
+  });
+
+  test(
+      'forwards errors only once with multiple listeners on a broadcast stream',
+      () async {
+    var errorCallCount = 0;
+    var source = StreamController<int>.broadcast();
+    source.stream.tap((_) {}, onError: (_, __) {
+      errorCallCount++;
+    })
+      ..listen((_) {}, onError: (_, __) {})
+      ..listen((_) {}, onError: (_, __) {});
+    source.addError('error');
+    await Future(() {});
+    expect(errorCallCount, 1);
+  });
+
+  test('calls onDone only once with multiple listeners on a broadcast stream',
+      () async {
+    var doneCallCount = 0;
+    var source = StreamController<int>.broadcast();
+    source.stream.tap((_) {}, onDone: () {
+      doneCallCount++;
+    })
+      ..listen((_) {})
+      ..listen((_) {});
+    await source.close();
+    expect(doneCallCount, 1);
+  });
+
+  test('forwards values to multiple listeners', () async {
+    var source = StreamController<int>.broadcast();
+    var emittedValues1 = <int>[];
+    var emittedValues2 = <int>[];
+    source.stream.tap((_) {})
+      ..listen(emittedValues1.add)
+      ..listen(emittedValues2.add);
+    source.add(1);
+    await Future(() {});
+    expect(emittedValues1, [1]);
+    expect(emittedValues2, [1]);
+  });
+
+  test('allows null callback', () async {
+    var stream = Stream.fromIterable([1, 2, 3]);
+    await stream.tap(null).last;
+  });
+}
diff --git a/pkgs/stream_transform/test/throttle_test.dart b/pkgs/stream_transform/test/throttle_test.dart
new file mode 100644
index 0000000..07f607a
--- /dev/null
+++ b/pkgs/stream_transform/test/throttle_test.dart
@@ -0,0 +1,193 @@
+// Copyright (c) 2017, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  for (var streamType in streamTypes) {
+    group('Stream type [$streamType]', () {
+      late StreamController<int> values;
+      late List<int> emittedValues;
+      late bool valuesCanceled;
+      late bool isDone;
+      late Stream<int> transformed;
+      late StreamSubscription<int> subscription;
+
+      group('throttle - trailing: false', () {
+        setUp(() async {
+          valuesCanceled = false;
+          values = createController(streamType)
+            ..onCancel = () {
+              valuesCanceled = true;
+            };
+          emittedValues = [];
+          isDone = false;
+          transformed = values.stream.throttle(const Duration(milliseconds: 5));
+        });
+
+        void listen() {
+          subscription = transformed.listen(emittedValues.add, onDone: () {
+            isDone = true;
+          });
+        }
+
+        test('cancels values', () async {
+          listen();
+          await subscription.cancel();
+          expect(valuesCanceled, true);
+        });
+
+        test('swallows values that come faster than duration', () {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..add(2)
+              ..close();
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1]);
+          });
+        });
+
+        test('outputs multiple values spaced further than duration', () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values.add(2);
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1, 2]);
+            async.elapse(const Duration(milliseconds: 6));
+          });
+        });
+
+        test('closes output immediately', () {
+          fakeAsync((async) {
+            listen();
+            values.add(1);
+            async.elapse(const Duration(milliseconds: 6));
+            values
+              ..add(2)
+              ..close();
+            async.flushMicrotasks();
+            expect(isDone, true);
+          });
+        });
+
+        if (streamType == 'broadcast') {
+          test('multiple listeners all get values', () {
+            fakeAsync((async) {
+              listen();
+              var otherValues = <int>[];
+              transformed.listen(otherValues.add);
+              values.add(1);
+              async.flushMicrotasks();
+              expect(emittedValues, [1]);
+              expect(otherValues, [1]);
+            });
+          });
+        }
+      });
+
+      group('throttle - trailing: true', () {
+        setUp(() async {
+          valuesCanceled = false;
+          values = createController(streamType)
+            ..onCancel = () {
+              valuesCanceled = true;
+            };
+          emittedValues = [];
+          isDone = false;
+          transformed = values.stream
+              .throttle(const Duration(milliseconds: 5), trailing: true);
+        });
+        void listen() {
+          subscription = transformed.listen(emittedValues.add, onDone: () {
+            isDone = true;
+          });
+        }
+
+        test('emits both first and last in a period', () {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..add(2)
+              ..close();
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1, 2]);
+          });
+        });
+
+        test('swallows values that are not the latest in a period', () {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..add(2)
+              ..add(3)
+              ..close();
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1, 3]);
+          });
+        });
+
+        test('waits to output the last value even if the stream closes',
+            () async {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..add(2)
+              ..close();
+            async.flushMicrotasks();
+            expect(isDone, false);
+            expect(emittedValues, [1],
+                reason: 'Should not be emitted until after duration');
+            async.elapse(const Duration(milliseconds: 6));
+            expect(emittedValues, [1, 2]);
+            expect(isDone, true);
+            async.elapse(const Duration(milliseconds: 6));
+          });
+        });
+
+        test('closes immediately if there is no pending value', () {
+          fakeAsync((async) {
+            listen();
+            values
+              ..add(1)
+              ..close();
+            async.flushMicrotasks();
+            expect(isDone, true);
+          });
+        });
+
+        if (streamType == 'broadcast') {
+          test('multiple listeners all get values', () {
+            fakeAsync((async) {
+              listen();
+              var otherValues = <int>[];
+              transformed.listen(otherValues.add);
+              values
+                ..add(1)
+                ..add(2);
+              async.flushMicrotasks();
+              expect(emittedValues, [1]);
+              expect(otherValues, [1]);
+              async.elapse(const Duration(milliseconds: 6));
+              expect(emittedValues, [1, 2]);
+              expect(otherValues, [1, 2]);
+            });
+          });
+        }
+      });
+    });
+  }
+}
diff --git a/pkgs/stream_transform/test/utils.dart b/pkgs/stream_transform/test/utils.dart
new file mode 100644
index 0000000..42d9613
--- /dev/null
+++ b/pkgs/stream_transform/test/utils.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2017, 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';
+
+StreamController<T> createController<T>(String streamType) {
+  switch (streamType) {
+    case 'single subscription':
+      return StreamController<T>();
+    case 'broadcast':
+      return StreamController<T>.broadcast();
+    default:
+      throw ArgumentError.value(
+          streamType, 'streamType', 'Must be one of $streamTypes');
+  }
+}
+
+const streamTypes = ['single subscription', 'broadcast'];
diff --git a/pkgs/stream_transform/test/where_not_null_test.dart b/pkgs/stream_transform/test/where_not_null_test.dart
new file mode 100644
index 0000000..c9af794
--- /dev/null
+++ b/pkgs/stream_transform/test/where_not_null_test.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2022, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('forwards only events that match the type', () async {
+    var values = Stream.fromIterable([null, 'a', null, 'b']);
+    var filtered = values.whereNotNull();
+    expect(await filtered.toList(), ['a', 'b']);
+  });
+
+  test('can result in empty stream', () async {
+    var values = Stream<Object?>.fromIterable([null, null]);
+    var filtered = values.whereNotNull();
+    expect(await filtered.isEmpty, true);
+  });
+
+  test('forwards values to multiple listeners', () async {
+    var values = StreamController<Object?>.broadcast();
+    var filtered = values.stream.whereNotNull();
+    var firstValues = <Object>[];
+    var secondValues = <Object>[];
+    filtered
+      ..listen(firstValues.add)
+      ..listen(secondValues.add);
+    values
+      ..add(null)
+      ..add('a')
+      ..add(null)
+      ..add('b');
+    await Future(() {});
+    expect(firstValues, ['a', 'b']);
+    expect(secondValues, ['a', 'b']);
+  });
+
+  test('closes streams with multiple listeners', () async {
+    var values = StreamController<Object?>.broadcast();
+    var filtered = values.stream.whereNotNull();
+    var firstDone = false;
+    var secondDone = false;
+    filtered
+      ..listen(null, onDone: () => firstDone = true)
+      ..listen(null, onDone: () => secondDone = true);
+    values
+      ..add(null)
+      ..add('a');
+    await values.close();
+    expect(firstDone, true);
+    expect(secondDone, true);
+  });
+}
diff --git a/pkgs/stream_transform/test/where_type_test.dart b/pkgs/stream_transform/test/where_type_test.dart
new file mode 100644
index 0000000..4cbea37
--- /dev/null
+++ b/pkgs/stream_transform/test/where_type_test.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2018, 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('forwards only events that match the type', () async {
+    var values = Stream.fromIterable([1, 'a', 2, 'b']);
+    var filtered = values.whereType<String>();
+    expect(await filtered.toList(), ['a', 'b']);
+  });
+
+  test('can result in empty stream', () async {
+    var values = Stream.fromIterable([1, 2, 3, 4]);
+    var filtered = values.whereType<String>();
+    expect(await filtered.isEmpty, true);
+  });
+
+  test('forwards values to multiple listeners', () async {
+    var values = StreamController<Object>.broadcast();
+    var filtered = values.stream.whereType<String>();
+    var firstValues = <Object>[];
+    var secondValues = <Object>[];
+    filtered
+      ..listen(firstValues.add)
+      ..listen(secondValues.add);
+    values
+      ..add(1)
+      ..add('a')
+      ..add(2)
+      ..add('b');
+    await Future(() {});
+    expect(firstValues, ['a', 'b']);
+    expect(secondValues, ['a', 'b']);
+  });
+
+  test('closes streams with multiple listeners', () async {
+    var values = StreamController<Object>.broadcast();
+    var filtered = values.stream.whereType<String>();
+    var firstDone = false;
+    var secondDone = false;
+    filtered
+      ..listen(null, onDone: () => firstDone = true)
+      ..listen(null, onDone: () => secondDone = true);
+    values
+      ..add(1)
+      ..add('a');
+    await values.close();
+    expect(firstDone, true);
+    expect(secondDone, true);
+  });
+}
diff --git a/pkgs/string_scanner/.github/dependabot.yml b/pkgs/string_scanner/.github/dependabot.yml
new file mode 100644
index 0000000..a19a66a
--- /dev/null
+++ b/pkgs/string_scanner/.github/dependabot.yml
@@ -0,0 +1,16 @@
+# Set update schedule for GitHub Actions
+# See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/keeping-your-actions-up-to-date-with-dependabot
+
+version: 2
+updates:
+
+- package-ecosystem: github-actions
+  directory: /
+  schedule:
+    interval: monthly
+  labels:
+    - autosubmit
+  groups:
+    github-actions:
+      patterns:
+        - "*"
diff --git a/pkgs/string_scanner/.github/workflows/publish.yaml b/pkgs/string_scanner/.github/workflows/publish.yaml
new file mode 100644
index 0000000..27157a0
--- /dev/null
+++ b/pkgs/string_scanner/.github/workflows/publish.yaml
@@ -0,0 +1,17 @@
+# A CI configuration to auto-publish pub packages.
+
+name: Publish
+
+on:
+  pull_request:
+    branches: [ master ]
+  push:
+    tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ]
+
+jobs:
+  publish:
+    if: ${{ github.repository_owner == 'dart-lang' }}
+    uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main
+    permissions:
+      id-token: write # Required for authentication using OIDC
+      pull-requests: write # Required for writing the pull request note
diff --git a/pkgs/string_scanner/.github/workflows/test-package.yml b/pkgs/string_scanner/.github/workflows/test-package.yml
new file mode 100644
index 0000000..c60f710
--- /dev/null
+++ b/pkgs/string_scanner/.github/workflows/test-package.yml
@@ -0,0 +1,64 @@
+name: Dart CI
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+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.1, 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'
diff --git a/pkgs/string_scanner/.gitignore b/pkgs/string_scanner/.gitignore
new file mode 100644
index 0000000..fb97bde
--- /dev/null
+++ b/pkgs/string_scanner/.gitignore
@@ -0,0 +1,5 @@
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.pub/
+.packages
+pubspec.lock
diff --git a/pkgs/string_scanner/CHANGELOG.md b/pkgs/string_scanner/CHANGELOG.md
new file mode 100644
index 0000000..082e9f2
--- /dev/null
+++ b/pkgs/string_scanner/CHANGELOG.md
@@ -0,0 +1,175 @@
+## 1.4.1
+
+* Move to `dart-lang/tools` monorepo.
+
+## 1.4.0
+
+* Fix `LineScanner`'s handling of `\r\n`'s to preventing errors scanning
+  zero-length matches when between CR and LF. CR is treated as a new line only
+  if not immediately followed by a LF.
+* Fix `LineScanner`'s updating of `column` when setting `position` if the
+  current position is not `0`.
+
+## 1.3.0
+
+* Require Dart 3.1.0
+
+* Add a `SpanScanner.spanFromPosition()` method which takes raw code units
+  rather than `SpanScanner.spanFrom()`'s `LineScannerState`s.
+
+## 1.2.0
+
+* Require Dart 2.18.0
+
+* Add better support for reading code points in the Unicode supplementary plane:
+
+  * Added `StringScanner.readCodePoint()`, which consumes an entire Unicode code
+    point even if it's represented by two UTF-16 code units.
+
+  * Added `StringScanner.peekCodePoint()`, which returns an entire Unicode code
+    point even if it's represented by two UTF-16 code units.
+
+  * `StringScanner.scanChar()` and `StringScanner.expectChar()` will now
+    properly consume two UTF-16 code units if they're passed Unicode code points
+    in the supplementary plane.
+
+## 1.1.1
+
+* Populate the pubspec `repository` field.
+* Switch to `package:lints`.
+* Remove a dependency on `package:charcode`.
+
+## 1.1.0
+
+* Stable release for null safety.
+
+## 1.1.0-nullsafety.3
+
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+## 1.1.0-nullsafety.2
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.1.0-nullsafety.1
+
+- Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 1.1.0-nullsafety
+
+- Migrate to null safety.
+
+## 1.0.5
+
+- Added an example.
+
+- Update Dart SDK constraint to `>=2.0.0 <3.0.0`.
+
+## 1.0.4
+
+* Add @alwaysThrows annotation to error method.
+
+## 1.0.3
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.0.2
+
+* `SpanScanner` no longer crashes when creating a span that contains a UTF-16
+  surrogate pair.
+
+## 1.0.1
+
+* Fix the error text emitted by `StringScanner.expectChar()`.
+
+## 1.0.0
+
+* **Breaking change**: `StringScanner.error()`'s `length` argument now defaults
+  to `0` rather than `1` when no match data is available.
+
+* **Breaking change**: `StringScanner.lastMatch` and related methods are now
+  reset when the scanner's position changes without producing a new match.
+
+**Note**: While the changes in `1.0.0` are user-visible, they're unlikely to
+actually break any code in practice. Unless you know that your package is
+incompatible with 0.1.x, consider using 0.1.5 as your lower bound rather
+than 1.0.0. For example, `string_scanner: ">=0.1.5 <2.0.0"`.
+
+## 0.1.5
+
+* Add `new SpanScanner.within()`, which scans within a existing `FileSpan`.
+
+* Add `StringScanner.scanChar()` and `StringScanner.expectChar()`.
+
+## 0.1.4+1
+
+* Remove the dependency on `path`, since we don't actually import it.
+
+## 0.1.4
+
+* Add `new SpanScanner.eager()` for creating a `SpanScanner` that eagerly
+  computes its current line and column numbers.
+
+## 0.1.3+2
+
+* Fix `LineScanner`'s handling of carriage returns to match that of
+  `SpanScanner`.
+
+## 0.1.3+1
+
+* Fixed the homepage URL.
+
+## 0.1.3
+
+* Add an optional `endState` argument to `SpanScanner.spanFrom`.
+
+## 0.1.2
+
+* Add `StringScanner.substring`, which returns a substring of the source string.
+
+## 0.1.1
+
+* Declare `SpanScanner`'s exposed `SourceSpan`s and `SourceLocation`s to be
+  `FileSpan`s and `FileLocation`s. They always were underneath, but callers may
+  now rely on it.
+
+* Add `SpanScanner.location`, which returns the scanner's current
+  `SourceLocation`.
+
+## 0.1.0
+
+* Switch from `source_maps`' `Span` class to `source_span`'s `SourceSpan` class.
+
+* `new StringScanner()`'s `sourceUrl` parameter is now named to make it clear
+  that it can be safely `null`.
+
+* `new StringScannerException()` takes different arguments in a different order
+  to match `SpanFormatException`.
+
+* `StringScannerException.string` has been renamed to
+  `StringScannerException.source` to match the `FormatException` interface.
+
+## 0.0.3
+
+* Make `StringScannerException` inherit from source_map's `SpanFormatException`.
+
+## 0.0.2
+
+* `new StringScanner()` now takes an optional `sourceUrl` argument that provides
+  the URL of the source file. This is used for error reporting.
+
+* Add `StringScanner.readChar()` and `StringScanner.peekChar()` methods for
+  doing character-by-character scanning.
+
+* Scanners now throw `StringScannerException`s which provide more detailed
+  access to information about the errors that were thrown and can provide
+  terminal-colored messages.
+
+* Add a `LineScanner` subclass of `StringScanner` that automatically tracks line
+  and column information of the text being scanned.
+
+* Add a `SpanScanner` subclass of `LineScanner` that exposes matched ranges as
+  [source map][] `Span` objects.
+
+[source_map]: https://pub.dev/packages/source_maps
diff --git a/pkgs/string_scanner/LICENSE b/pkgs/string_scanner/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/string_scanner/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/string_scanner/README.md b/pkgs/string_scanner/README.md
new file mode 100644
index 0000000..e06e325
--- /dev/null
+++ b/pkgs/string_scanner/README.md
@@ -0,0 +1,41 @@
+[![Dart CI](https://github.com/dart-lang/string_scanner/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/string_scanner/actions/workflows/test-package.yml)
+[![pub package](https://img.shields.io/pub/v/string_scanner.svg)](https://pub.dev/packages/string_scanner)
+[![package publisher](https://img.shields.io/pub/publisher/string_scanner.svg)](https://pub.dev/packages/string_scanner/publisher)
+
+This package exposes a `StringScanner` type that makes it easy to parse a string
+using a series of `Pattern`s. For example:
+
+```dart
+import 'dart:math' as math;
+
+import 'package:string_scanner/string_scanner.dart';
+
+num parseNumber(String source) {
+  // Scan a number ("1", "1.5", "-3").
+  final scanner = StringScanner(source);
+
+  // [Scanner.scan] tries to consume a [Pattern] and returns whether or not it
+  // succeeded. It will move the scan pointer past the end of the pattern.
+  final negative = scanner.scan('-');
+
+  // [Scanner.expect] consumes a [Pattern] and throws a [FormatError] if it
+  // fails. Like [Scanner.scan], it will move the scan pointer forward.
+  scanner.expect(RegExp(r'\d+'));
+
+  // [Scanner.lastMatch] holds the [MatchData] for the most recent call to
+  // [Scanner.scan], [Scanner.expect], or [Scanner.matches].
+  var number = num.parse(scanner.lastMatch![0]!);
+
+  if (scanner.scan('.')) {
+    scanner.expect(RegExp(r'\d+'));
+    final decimal = scanner.lastMatch![0]!;
+    number += int.parse(decimal) / math.pow(10, decimal.length);
+  }
+
+  // [Scanner.expectDone] will throw a [FormatError] if there's any input that
+  // hasn't yet been consumed.
+  scanner.expectDone();
+
+  return (negative ? -1 : 1) * number;
+}
+```
diff --git a/pkgs/string_scanner/analysis_options.yaml b/pkgs/string_scanner/analysis_options.yaml
new file mode 100644
index 0000000..59f763a
--- /dev/null
+++ b/pkgs/string_scanner/analysis_options.yaml
@@ -0,0 +1,32 @@
+# 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
+    - 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
+    - prefer_final_locals
+    - unnecessary_await_in_return
+    - unnecessary_raw_strings
+    - use_if_null_to_convert_nulls_to_bools
+    - use_raw_strings
+    - use_string_buffers
diff --git a/pkgs/string_scanner/example/example.dart b/pkgs/string_scanner/example/example.dart
new file mode 100644
index 0000000..ec9dd76
--- /dev/null
+++ b/pkgs/string_scanner/example/example.dart
@@ -0,0 +1,40 @@
+// 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:math' as math;
+
+import 'package:string_scanner/string_scanner.dart';
+
+void main(List<String> args) {
+  print(parseNumber(args.single));
+}
+
+num parseNumber(String source) {
+  // Scan a number ("1", "1.5", "-3").
+  final scanner = StringScanner(source);
+
+  // [Scanner.scan] tries to consume a [Pattern] and returns whether or not it
+  // succeeded. It will move the scan pointer past the end of the pattern.
+  final negative = scanner.scan('-');
+
+  // [Scanner.expect] consumes a [Pattern] and throws a [FormatError] if it
+  // fails. Like [Scanner.scan], it will move the scan pointer forward.
+  scanner.expect(RegExp(r'\d+'));
+
+  // [Scanner.lastMatch] holds the [MatchData] for the most recent call to
+  // [Scanner.scan], [Scanner.expect], or [Scanner.matches].
+  var number = num.parse(scanner.lastMatch![0]!);
+
+  if (scanner.scan('.')) {
+    scanner.expect(RegExp(r'\d+'));
+    final decimal = scanner.lastMatch![0]!;
+    number += int.parse(decimal) / math.pow(10, decimal.length);
+  }
+
+  // [Scanner.expectDone] will throw a [FormatError] if there's any input that
+  // hasn't yet been consumed.
+  scanner.expectDone();
+
+  return (negative ? -1 : 1) * number;
+}
diff --git a/pkgs/string_scanner/lib/src/charcode.dart b/pkgs/string_scanner/lib/src/charcode.dart
new file mode 100644
index 0000000..d157749
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/charcode.dart
@@ -0,0 +1,24 @@
+// 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.
+
+/// Character '\'.
+const int $backslash = 0x5C;
+
+/// "Carriage return" control character.
+const int $cr = 0x0D;
+
+/// Character '"'.
+const int $doubleQuote = 0x22;
+
+/// Character 'f'.
+const int $f = 0x66;
+
+/// "Line feed" control character.
+const int $lf = 0x0A;
+
+/// Space character.
+const int $space = 0x20;
+
+/// Character 'x'.
+const int $x = 0x78;
diff --git a/pkgs/string_scanner/lib/src/eager_span_scanner.dart b/pkgs/string_scanner/lib/src/eager_span_scanner.dart
new file mode 100644
index 0000000..1ccc746
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/eager_span_scanner.dart
@@ -0,0 +1,133 @@
+// 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 'charcode.dart';
+import 'line_scanner.dart';
+import 'span_scanner.dart';
+import 'utils.dart';
+
+// TODO(nweiz): Currently this duplicates code in line_scanner.dart. Once
+// sdk#23770 is fully complete, we should move the shared code into a mixin.
+
+/// A regular expression matching newlines across platforms.
+final _newlineRegExp = RegExp(r'\r\n?|\n');
+
+/// A [SpanScanner] that tracks the line and column eagerly, like [LineScanner].
+class EagerSpanScanner extends SpanScanner {
+  @override
+  int get line => _line;
+  int _line = 0;
+
+  @override
+  int get column => _column;
+  int _column = 0;
+
+  @override
+  LineScannerState get state =>
+      _EagerSpanScannerState(this, position, line, column);
+
+  bool get _betweenCRLF => peekChar(-1) == $cr && peekChar() == $lf;
+
+  @override
+  set state(LineScannerState state) {
+    if (state is! _EagerSpanScannerState || !identical(state._scanner, this)) {
+      throw ArgumentError('The given LineScannerState was not returned by '
+          'this LineScanner.');
+    }
+
+    super.position = state.position;
+    _line = state.line;
+    _column = state.column;
+  }
+
+  @override
+  set position(int newPosition) {
+    final oldPosition = position;
+    super.position = newPosition;
+
+    if (newPosition > oldPosition) {
+      final newlines = _newlinesIn(string.substring(oldPosition, newPosition));
+      _line += newlines.length;
+      if (newlines.isEmpty) {
+        _column += newPosition - oldPosition;
+      } else {
+        _column = newPosition - newlines.last.end;
+      }
+    } else {
+      final newlines = _newlinesIn(string.substring(newPosition, oldPosition));
+      if (_betweenCRLF) newlines.removeLast();
+
+      _line -= newlines.length;
+      if (newlines.isEmpty) {
+        _column -= oldPosition - newPosition;
+      } else {
+        _column =
+            newPosition - string.lastIndexOf(_newlineRegExp, newPosition) - 1;
+      }
+    }
+  }
+
+  EagerSpanScanner(super.string, {super.sourceUrl, super.position});
+
+  @override
+  bool scanChar(int character) {
+    if (!super.scanChar(character)) return false;
+    _adjustLineAndColumn(character);
+    return true;
+  }
+
+  @override
+  int readChar() {
+    final character = super.readChar();
+    _adjustLineAndColumn(character);
+    return character;
+  }
+
+  /// Adjusts [_line] and [_column] after having consumed [character].
+  void _adjustLineAndColumn(int character) {
+    if (character == $lf || (character == $cr && peekChar() != $lf)) {
+      _line += 1;
+      _column = 0;
+    } else {
+      _column += inSupplementaryPlane(character) ? 2 : 1;
+    }
+  }
+
+  @override
+  bool scan(Pattern pattern) {
+    if (!super.scan(pattern)) return false;
+    final firstMatch = lastMatch![0]!;
+
+    final newlines = _newlinesIn(firstMatch);
+    _line += newlines.length;
+    if (newlines.isEmpty) {
+      _column += firstMatch.length;
+    } else {
+      _column = firstMatch.length - newlines.last.end;
+    }
+
+    return true;
+  }
+
+  /// Returns a list of [Match]es describing all the newlines in [text], which
+  /// is assumed to end at [position].
+  List<Match> _newlinesIn(String text) {
+    final newlines = _newlineRegExp.allMatches(text).toList();
+    if (_betweenCRLF) newlines.removeLast();
+    return newlines;
+  }
+}
+
+/// A class representing the state of an [EagerSpanScanner].
+class _EagerSpanScannerState implements LineScannerState {
+  final EagerSpanScanner _scanner;
+  @override
+  final int position;
+  @override
+  final int line;
+  @override
+  final int column;
+
+  _EagerSpanScannerState(this._scanner, this.position, this.line, this.column);
+}
diff --git a/pkgs/string_scanner/lib/src/exception.dart b/pkgs/string_scanner/lib/src/exception.dart
new file mode 100644
index 0000000..57af541
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/exception.dart
@@ -0,0 +1,21 @@
+// 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:source_span/source_span.dart';
+
+import 'string_scanner.dart';
+
+/// An exception thrown by a [StringScanner] that failed to parse a string.
+class StringScannerException extends SourceSpanFormatException {
+  @override
+  String get source => super.source as String;
+
+  /// The URL of the source file being parsed.
+  ///
+  /// This may be `null`, indicating that the source URL is unknown.
+  Uri? get sourceUrl => span?.sourceUrl;
+
+  StringScannerException(
+      super.message, SourceSpan super.span, String super.source);
+}
diff --git a/pkgs/string_scanner/lib/src/line_scanner.dart b/pkgs/string_scanner/lib/src/line_scanner.dart
new file mode 100644
index 0000000..b18d610
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/line_scanner.dart
@@ -0,0 +1,183 @@
+// 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 'charcode.dart';
+import 'string_scanner.dart';
+import 'utils.dart';
+
+// Note that much of this code is duplicated in eager_span_scanner.dart.
+
+/// A regular expression matching newlines. A newline is either a `\n`, a `\r\n`
+/// or a `\r` that is not immediately followed by a `\n`.
+final _newlineRegExp = RegExp(r'\n|\r\n|\r(?!\n)');
+
+/// A subclass of [StringScanner] that tracks line and column information.
+class LineScanner extends StringScanner {
+  /// The scanner's current (zero-based) line number.
+  int get line => _line;
+  int _line = 0;
+
+  /// The scanner's current (zero-based) column number.
+  int get column => _column;
+  int _column = 0;
+
+  /// The scanner's state, including line and column information.
+  ///
+  /// This can be used to efficiently save and restore the state of the scanner
+  /// when backtracking. A given [LineScannerState] is only valid for the
+  /// [LineScanner] that created it.
+  ///
+  /// This does not include the scanner's match information.
+  LineScannerState get state =>
+      LineScannerState._(this, position, line, column);
+
+  /// Whether the current position is between a CR character and an LF
+  /// charactet.
+  bool get _betweenCRLF => peekChar(-1) == $cr && peekChar() == $lf;
+
+  set state(LineScannerState state) {
+    if (!identical(state._scanner, this)) {
+      throw ArgumentError('The given LineScannerState was not returned by '
+          'this LineScanner.');
+    }
+
+    super.position = state.position;
+    _line = state.line;
+    _column = state.column;
+  }
+
+  @override
+  set position(int newPosition) {
+    if (newPosition == position) {
+      return;
+    }
+
+    final oldPosition = position;
+    super.position = newPosition;
+
+    if (newPosition == 0) {
+      _line = 0;
+      _column = 0;
+    } else if (newPosition > oldPosition) {
+      final newlines = _newlinesIn(string.substring(oldPosition, newPosition),
+          endPosition: newPosition);
+      _line += newlines.length;
+      if (newlines.isEmpty) {
+        _column += newPosition - oldPosition;
+      } else {
+        // The regex got a substring, so we need to account for where it started
+        // in the string.
+        final offsetOfLastNewline = oldPosition + newlines.last.end;
+        _column = newPosition - offsetOfLastNewline;
+      }
+    } else if (newPosition < oldPosition) {
+      final newlines = _newlinesIn(string.substring(newPosition, oldPosition),
+          endPosition: oldPosition);
+
+      _line -= newlines.length;
+      if (newlines.isEmpty) {
+        _column -= oldPosition - newPosition;
+      } else {
+        // To compute the new column, we need to locate the last newline before
+        // the new position. When searching, we must exclude the CR if we're
+        // between a CRLF because it's not considered a newline.
+        final crOffset = _betweenCRLF ? -1 : 0;
+        // Additionally, if we use newPosition as the end of the search and the
+        // character at that position itself (the next character) is a newline
+        // we should not use it, so also offset to account for that.
+        const currentCharOffset = -1;
+        final lastNewline = string.lastIndexOf(
+            _newlineRegExp, newPosition + currentCharOffset + crOffset);
+
+        // Now we need to know the offset after the newline. This is the index
+        // above plus the length of the newline (eg. if we found `\r\n`) we need
+        // to add two. However if no newline was found, that index is 0.
+        final offsetAfterLastNewline = lastNewline == -1
+            ? 0
+            : string[lastNewline] == '\r' && string[lastNewline + 1] == '\n'
+                ? lastNewline + 2
+                : lastNewline + 1;
+
+        _column = newPosition - offsetAfterLastNewline;
+      }
+    }
+  }
+
+  LineScanner(super.string, {super.sourceUrl, super.position});
+
+  @override
+  bool scanChar(int character) {
+    if (!super.scanChar(character)) return false;
+    _adjustLineAndColumn(character);
+    return true;
+  }
+
+  @override
+  int readChar() {
+    final character = super.readChar();
+    _adjustLineAndColumn(character);
+    return character;
+  }
+
+  /// Adjusts [_line] and [_column] after having consumed [character].
+  void _adjustLineAndColumn(int character) {
+    if (character == $lf || (character == $cr && peekChar() != $lf)) {
+      _line += 1;
+      _column = 0;
+    } else {
+      _column += inSupplementaryPlane(character) ? 2 : 1;
+    }
+  }
+
+  @override
+  bool scan(Pattern pattern) {
+    if (!super.scan(pattern)) return false;
+
+    final newlines = _newlinesIn(lastMatch![0]!, endPosition: position);
+    _line += newlines.length;
+    if (newlines.isEmpty) {
+      _column += lastMatch![0]!.length;
+    } else {
+      _column = lastMatch![0]!.length - newlines.last.end;
+    }
+
+    return true;
+  }
+
+  /// Returns a list of [Match]es describing all the newlines in [text], which
+  /// ends at [endPosition].
+  ///
+  /// If [text] ends with `\r`, it will only be treated as a newline if the next
+  /// character at [position] is not a `\n`.
+  List<Match> _newlinesIn(String text, {required int endPosition}) {
+    final newlines = _newlineRegExp.allMatches(text).toList();
+    // If the last character is a `\r` it will have been treated as a newline,
+    // but this is only valid if the next character is not a `\n`.
+    if (endPosition < string.length &&
+        text.endsWith('\r') &&
+        string[endPosition] == '\n') {
+      // newlines should never be empty here, because if `text` ends with `\r`
+      // it would have matched `\r(?!\n)` in the newline regex.
+      newlines.removeLast();
+    }
+    return newlines;
+  }
+}
+
+/// A class representing the state of a [LineScanner].
+class LineScannerState {
+  /// The [LineScanner] that created this.
+  final LineScanner _scanner;
+
+  /// The position of the scanner in this state.
+  final int position;
+
+  /// The zero-based line number of the scanner in this state.
+  final int line;
+
+  /// The zero-based column number of the scanner in this state.
+  final int column;
+
+  LineScannerState._(this._scanner, this.position, this.line, this.column);
+}
diff --git a/pkgs/string_scanner/lib/src/relative_span_scanner.dart b/pkgs/string_scanner/lib/src/relative_span_scanner.dart
new file mode 100644
index 0000000..cd9af0e
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/relative_span_scanner.dart
@@ -0,0 +1,132 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+
+import 'exception.dart';
+import 'line_scanner.dart';
+import 'span_scanner.dart';
+import 'string_scanner.dart';
+import 'utils.dart';
+
+/// A [SpanScanner] that scans within an existing [FileSpan].
+///
+/// This re-implements chunks of [SpanScanner] rather than using a dummy span or
+/// inheritance because scanning is often a performance-critical operation, so
+/// it's important to avoid adding extra overhead when relative scanning isn't
+/// needed.
+class RelativeSpanScanner extends StringScanner implements SpanScanner {
+  /// The source of the scanner.
+  ///
+  /// This caches line break information and is used to generate [SourceSpan]s.
+  final SourceFile _sourceFile;
+
+  /// The start location of the span within which this scanner is scanning.
+  ///
+  /// This is used to convert between span-relative and file-relative fields.
+  final FileLocation _startLocation;
+
+  @override
+  int get line =>
+      _sourceFile.getLine(_startLocation.offset + position) -
+      _startLocation.line;
+
+  @override
+  int get column {
+    final line = _sourceFile.getLine(_startLocation.offset + position);
+    final column =
+        _sourceFile.getColumn(_startLocation.offset + position, line: line);
+    return line == _startLocation.line
+        ? column - _startLocation.column
+        : column;
+  }
+
+  @override
+  LineScannerState get state => _SpanScannerState(this, position);
+
+  @override
+  set state(LineScannerState state) {
+    if (state is! _SpanScannerState || !identical(state._scanner, this)) {
+      throw ArgumentError('The given LineScannerState was not returned by '
+          'this LineScanner.');
+    }
+
+    position = state.position;
+  }
+
+  @override
+  FileSpan? get lastSpan => _lastSpan;
+  FileSpan? _lastSpan;
+
+  @override
+  FileLocation get location =>
+      _sourceFile.location(_startLocation.offset + position);
+
+  @override
+  FileSpan get emptySpan => location.pointSpan();
+
+  RelativeSpanScanner(FileSpan span)
+      : _sourceFile = span.file,
+        _startLocation = span.start,
+        super(span.text, sourceUrl: span.sourceUrl);
+
+  @override
+  FileSpan spanFrom(LineScannerState startState, [LineScannerState? endState]) {
+    final endPosition = endState == null ? position : endState.position;
+    return _sourceFile.span(_startLocation.offset + startState.position,
+        _startLocation.offset + endPosition);
+  }
+
+  @override
+  FileSpan spanFromPosition(int startPosition, [int? endPosition]) {
+    RangeError.checkValidRange(
+        startPosition,
+        endPosition,
+        _sourceFile.length - _startLocation.offset,
+        'startPosition',
+        'endPosition');
+    return _sourceFile.span(_startLocation.offset + startPosition,
+        _startLocation.offset + (endPosition ?? position));
+  }
+
+  @override
+  bool matches(Pattern pattern) {
+    if (!super.matches(pattern)) {
+      _lastSpan = null;
+      return false;
+    }
+
+    _lastSpan = _sourceFile.span(_startLocation.offset + position,
+        _startLocation.offset + lastMatch!.end);
+    return true;
+  }
+
+  @override
+  Never error(String message, {Match? match, int? position, int? length}) {
+    validateErrorArgs(string, match, position, length);
+
+    if (match == null && position == null && length == null) match = lastMatch;
+    position ??= match == null ? this.position : match.start;
+    length ??= match == null ? 1 : match.end - match.start;
+
+    final span = _sourceFile.span(_startLocation.offset + position,
+        _startLocation.offset + position + length);
+    throw StringScannerException(message, span, string);
+  }
+}
+
+/// A class representing the state of a [SpanScanner].
+class _SpanScannerState implements LineScannerState {
+  /// The [SpanScanner] that created this.
+  final RelativeSpanScanner _scanner;
+
+  @override
+  final int position;
+  @override
+  int get line => _scanner._sourceFile.getLine(position);
+  @override
+  int get column => _scanner._sourceFile.getColumn(position);
+
+  _SpanScannerState(this._scanner, this.position);
+}
diff --git a/pkgs/string_scanner/lib/src/span_scanner.dart b/pkgs/string_scanner/lib/src/span_scanner.dart
new file mode 100644
index 0000000..509cf60
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/span_scanner.dart
@@ -0,0 +1,142 @@
+// 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:source_span/source_span.dart';
+
+import 'eager_span_scanner.dart';
+import 'exception.dart';
+import 'line_scanner.dart';
+import 'relative_span_scanner.dart';
+import 'string_scanner.dart';
+import 'utils.dart';
+
+/// A subclass of [LineScanner] that exposes matched ranges as source map
+/// [FileSpan]s.
+class SpanScanner extends StringScanner implements LineScanner {
+  /// The source of the scanner.
+  ///
+  /// This caches line break information and is used to generate [FileSpan]s.
+  final SourceFile _sourceFile;
+
+  @override
+  int get line => _sourceFile.getLine(position);
+  @override
+  int get column => _sourceFile.getColumn(position);
+
+  @override
+  LineScannerState get state => _SpanScannerState(this, position);
+
+  @override
+  set state(LineScannerState state) {
+    if (state is! _SpanScannerState || !identical(state._scanner, this)) {
+      throw ArgumentError('The given LineScannerState was not returned by '
+          'this LineScanner.');
+    }
+
+    position = state.position;
+  }
+
+  /// The [FileSpan] for [lastMatch].
+  ///
+  /// This is the span for the entire match. There's no way to get spans for
+  /// subgroups since [Match] exposes no information about their positions.
+  FileSpan? get lastSpan {
+    if (lastMatch == null) _lastSpan = null;
+    return _lastSpan;
+  }
+
+  FileSpan? _lastSpan;
+
+  /// The current location of the scanner.
+  FileLocation get location => _sourceFile.location(position);
+
+  /// Returns an empty span at the current location.
+  FileSpan get emptySpan => location.pointSpan();
+
+  /// Creates a new [SpanScanner] that starts scanning from [position].
+  ///
+  /// [sourceUrl] is used as [SourceLocation.sourceUrl] for the returned
+  /// [FileSpan]s as well as for error reporting. It can be a [String], a
+  /// [Uri], or `null`.
+  SpanScanner(super.string, {super.sourceUrl, super.position})
+      : _sourceFile = SourceFile.fromString(string, url: sourceUrl);
+
+  /// Creates a new [SpanScanner] that eagerly computes line and column numbers.
+  ///
+  /// In general [SpanScanner.new] will be more efficient, since it avoids extra
+  /// computation on every scan. However, eager scanning can be useful for
+  /// situations where the normal course of parsing frequently involves
+  /// accessing the current line and column numbers.
+  ///
+  /// Note that *only* the `line` and `column` fields on the `SpanScanner`
+  /// itself and its `LineScannerState` are eagerly computed. To limit their
+  /// memory footprint, returned spans and locations will still lazily compute
+  /// their line and column numbers.
+  factory SpanScanner.eager(String string, {sourceUrl, int? position}) =
+      EagerSpanScanner;
+
+  /// Creates a new [SpanScanner] that scans within [span].
+  ///
+  /// This scans through [span]`.text, but emits new spans from [span]`.file` in
+  /// their appropriate relative positions. The [string] field contains only
+  /// [span]`.text`, and [position], [line], and [column] are all relative to
+  /// the span.
+  factory SpanScanner.within(FileSpan span) = RelativeSpanScanner;
+
+  /// Creates a [FileSpan] representing the source range between [startState]
+  /// and the current position.
+  FileSpan spanFrom(LineScannerState startState, [LineScannerState? endState]) {
+    final endPosition = endState == null ? position : endState.position;
+    return _sourceFile.span(startState.position, endPosition);
+  }
+
+  /// Creates a [FileSpan] representing the source range between [startPosition]
+  /// and [endPosition], or the current position if [endPosition] is null.
+  ///
+  /// Each position should be a code unit offset into the string being scanned,
+  /// with the same conventions as [StringScanner.position].
+  ///
+  /// Throws a [RangeError] if [startPosition] or [endPosition] aren't within
+  /// this source file.
+  FileSpan spanFromPosition(int startPosition, [int? endPosition]) =>
+      _sourceFile.span(startPosition, endPosition ?? position);
+
+  @override
+  bool matches(Pattern pattern) {
+    if (!super.matches(pattern)) {
+      _lastSpan = null;
+      return false;
+    }
+
+    _lastSpan = _sourceFile.span(position, lastMatch!.end);
+    return true;
+  }
+
+  @override
+  Never error(String message, {Match? match, int? position, int? length}) {
+    validateErrorArgs(string, match, position, length);
+
+    if (match == null && position == null && length == null) match = lastMatch;
+    position ??= match == null ? this.position : match.start;
+    length ??= match == null ? 0 : match.end - match.start;
+
+    final span = _sourceFile.span(position, position + length);
+    throw StringScannerException(message, span, string);
+  }
+}
+
+/// A class representing the state of a [SpanScanner].
+class _SpanScannerState implements LineScannerState {
+  /// The [SpanScanner] that created this.
+  final SpanScanner _scanner;
+
+  @override
+  final int position;
+  @override
+  int get line => _scanner._sourceFile.getLine(position);
+  @override
+  int get column => _scanner._sourceFile.getColumn(position);
+
+  _SpanScannerState(this._scanner, this.position);
+}
diff --git a/pkgs/string_scanner/lib/src/string_scanner.dart b/pkgs/string_scanner/lib/src/string_scanner.dart
new file mode 100644
index 0000000..1466944
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/string_scanner.dart
@@ -0,0 +1,272 @@
+// 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:source_span/source_span.dart';
+
+import 'charcode.dart';
+import 'exception.dart';
+import 'utils.dart';
+
+/// A class that scans through a string using [Pattern]s.
+class StringScanner {
+  /// The URL of the source of the string being scanned.
+  ///
+  /// This is used for error reporting. It may be `null`, indicating that the
+  /// source URL is unknown or unavailable.
+  final Uri? sourceUrl;
+
+  /// The string being scanned through.
+  final String string;
+
+  /// The current position of the scanner in the string, in characters.
+  int get position => _position;
+  set position(int position) {
+    if (position.isNegative || position > string.length) {
+      throw ArgumentError('Invalid position $position');
+    }
+
+    _position = position;
+    _lastMatch = null;
+  }
+
+  int _position = 0;
+
+  /// The data about the previous match made by the scanner.
+  ///
+  /// If the last match failed, this will be `null`.
+  Match? get lastMatch {
+    // Lazily unset [_lastMatch] so that we avoid extra assignments in
+    // character-by-character methods that are used in core loops.
+    if (_position != _lastMatchPosition) _lastMatch = null;
+    return _lastMatch;
+  }
+
+  Match? _lastMatch;
+  int? _lastMatchPosition;
+
+  /// The portion of the string that hasn't yet been scanned.
+  String get rest => string.substring(position);
+
+  /// Whether the scanner has completely consumed [string].
+  bool get isDone => position == string.length;
+
+  /// Creates a new [StringScanner] that starts scanning from [position].
+  ///
+  /// [position] defaults to 0, the beginning of the string. [sourceUrl] is the
+  /// URL of the source of the string being scanned, if available. It can be
+  /// a [String], a [Uri], or `null`.
+  StringScanner(this.string, {Object? sourceUrl, int? position})
+      : sourceUrl = sourceUrl == null
+            ? null
+            : sourceUrl is String
+                ? Uri.parse(sourceUrl)
+                : sourceUrl as Uri {
+    if (position != null) this.position = position;
+  }
+
+  /// Consumes a single character and returns its character code.
+  ///
+  /// This throws a [FormatException] if the string has been fully consumed. It
+  /// doesn't affect [lastMatch].
+  int readChar() {
+    if (isDone) _fail('more input');
+    return string.codeUnitAt(_position++);
+  }
+
+  /// Returns the character code of the character [offset] away from [position].
+  ///
+  /// [offset] defaults to zero, and may be negative to inspect already-consumed
+  /// characters.
+  ///
+  /// This returns `null` if [offset] points outside the string. It doesn't
+  /// affect [lastMatch].
+  int? peekChar([int? offset]) {
+    offset ??= 0;
+    final index = position + offset;
+    if (index < 0 || index >= string.length) return null;
+    return string.codeUnitAt(index);
+  }
+
+  /// If the next character in the string is [character], consumes it.
+  ///
+  /// If [character] is a Unicode code point in a supplementary plane, this will
+  /// consume two code units. Dart's string representation is UTF-16, which
+  /// represents supplementary-plane code units as two code units.
+  ///
+  /// Returns whether or not [character] was consumed.
+  bool scanChar(int character) {
+    if (inSupplementaryPlane(character)) {
+      if (_position + 1 >= string.length ||
+          string.codeUnitAt(_position) != highSurrogate(character) ||
+          string.codeUnitAt(_position + 1) != lowSurrogate(character)) {
+        return false;
+      } else {
+        _position += 2;
+        return true;
+      }
+    } else {
+      if (isDone) return false;
+      if (string.codeUnitAt(_position) != character) return false;
+      _position++;
+      return true;
+    }
+  }
+
+  /// If the next character in the string is [character], consumes it.
+  ///
+  /// If [character] is a Unicode code point in a supplementary plane, this will
+  /// consume two code units. Dart's string representation is UTF-16, which
+  /// represents supplementary-plane code units as two code units.
+  ///
+  /// If [character] could not be consumed, throws a [FormatException]
+  /// describing the position of the failure. [name] is used in this error as
+  /// the expected name of the character being matched; if it's `null`, the
+  /// character itself is used instead.
+  void expectChar(int character, {String? name}) {
+    if (scanChar(character)) return;
+
+    if (name == null) {
+      if (character == $backslash) {
+        name = r'"\"';
+      } else if (character == $doubleQuote) {
+        name = r'"\""';
+      } else {
+        name = '"${String.fromCharCode(character)}"';
+      }
+    }
+
+    _fail(name);
+  }
+
+  /// Consumes a single Unicode code unit and returns it.
+  ///
+  /// This works like [readChar], except that it automatically handles UTF-16
+  /// surrogate pairs. Specifically, if the next two code units form a surrogate
+  /// pair, consumes them both and returns the corresponding Unicode code point.
+  ///
+  /// If next two characters are not a surrogate pair, the next code unit is
+  /// returned as-is, even if it's an unpaired surrogate.
+  int readCodePoint() {
+    final first = readChar();
+    if (!isHighSurrogate(first)) return first;
+
+    final next = peekChar();
+    if (next == null || !isLowSurrogate(next)) return first;
+
+    readChar();
+    return decodeSurrogatePair(first, next);
+  }
+
+  /// Returns the Unicode code point immediately after [position].
+  ///
+  /// This works like [peekChar], except that it automatically handles UTF-16
+  /// surrogate pairs. Specifically, if the next two code units form a surrogate
+  /// pair, returns the corresponding Unicode code point.
+  ///
+  /// If next two characters are not a surrogate pair, the next code unit is
+  /// returned as-is, even if it's an unpaired surrogate.
+  int? peekCodePoint() {
+    final first = peekChar();
+    if (first == null || !isHighSurrogate(first)) return first;
+
+    final next = peekChar(1);
+    if (next == null || !isLowSurrogate(next)) return first;
+
+    return decodeSurrogatePair(first, next);
+  }
+
+  /// If [pattern] matches at the current position of the string, scans forward
+  /// until the end of the match.
+  ///
+  /// Returns whether or not [pattern] matched.
+  bool scan(Pattern pattern) {
+    final success = matches(pattern);
+    if (success) {
+      _position = _lastMatch!.end;
+      _lastMatchPosition = _position;
+    }
+    return success;
+  }
+
+  /// If [pattern] matches at the current position of the string, scans forward
+  /// until the end of the match.
+  ///
+  /// If [pattern] did not match, throws a [FormatException] describing the
+  /// position of the failure. [name] is used in this error as the expected name
+  /// of the pattern being matched; if it's `null`, the pattern itself is used
+  /// instead.
+  void expect(Pattern pattern, {String? name}) {
+    if (scan(pattern)) return;
+
+    if (name == null) {
+      if (pattern is RegExp) {
+        final source = pattern.pattern;
+        name = '/$source/';
+      } else {
+        name =
+            pattern.toString().replaceAll(r'\', r'\\').replaceAll('"', r'\"');
+        name = '"$name"';
+      }
+    }
+    _fail(name);
+  }
+
+  /// If the string has not been fully consumed, this throws a
+  /// [FormatException].
+  void expectDone() {
+    if (isDone) return;
+    _fail('no more input');
+  }
+
+  /// Returns whether or not [pattern] matches at the current position of the
+  /// string.
+  ///
+  /// This doesn't move the scan pointer forward.
+  bool matches(Pattern pattern) {
+    _lastMatch = pattern.matchAsPrefix(string, position);
+    _lastMatchPosition = _position;
+    return _lastMatch != null;
+  }
+
+  /// Returns the substring of [string] between [start] and [end].
+  ///
+  /// Unlike [String.substring], [end] defaults to [position] rather than the
+  /// end of the string.
+  String substring(int start, [int? end]) {
+    end ??= position;
+    return string.substring(start, end);
+  }
+
+  /// Throws a [FormatException] with [message] as well as a detailed
+  /// description of the location of the error in the string.
+  ///
+  /// [match] is the match information for the span of the string with which the
+  /// error is associated. This should be a match returned by this scanner's
+  /// [lastMatch] property. By default, the error is associated with the last
+  /// match.
+  ///
+  /// If [position] and/or [length] are passed, they are used as the error span
+  /// instead. If only [length] is passed, [position] defaults to the current
+  /// position; if only [position] is passed, [length] defaults to 0.
+  ///
+  /// It's an error to pass [match] at the same time as [position] or [length].
+  Never error(String message, {Match? match, int? position, int? length}) {
+    validateErrorArgs(string, match, position, length);
+
+    if (match == null && position == null && length == null) match = lastMatch;
+    position ??= match == null ? this.position : match.start;
+    length ??= match == null ? 0 : match.end - match.start;
+
+    final sourceFile = SourceFile.fromString(string, url: sourceUrl);
+    final span = sourceFile.span(position, position + length);
+    throw StringScannerException(message, span, string);
+  }
+
+  // TODO(nweiz): Make this handle long lines more gracefully.
+  /// Throws a [FormatException] describing that [name] is expected at the
+  /// current position in the string.
+  Never _fail(String name) {
+    error('expected $name.', position: position, length: 0);
+  }
+}
diff --git a/pkgs/string_scanner/lib/src/utils.dart b/pkgs/string_scanner/lib/src/utils.dart
new file mode 100644
index 0000000..39891a1
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/utils.dart
@@ -0,0 +1,95 @@
+// 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 'string_scanner.dart';
+
+/// Validates the arguments passed to [StringScanner.error].
+void validateErrorArgs(
+    String string, Match? match, int? position, int? length) {
+  if (match != null && (position != null || length != null)) {
+    throw ArgumentError("Can't pass both match and position/length.");
+  }
+
+  if (position != null) {
+    if (position < 0) {
+      throw RangeError('position must be greater than or equal to 0.');
+    } else if (position > string.length) {
+      throw RangeError('position must be less than or equal to the '
+          'string length.');
+    }
+  }
+
+  if (length != null && length < 0) {
+    throw RangeError('length must be greater than or equal to 0.');
+  }
+
+  if (position != null && length != null && position + length > string.length) {
+    throw RangeError('position plus length must not go beyond the end of '
+        'the string.');
+  }
+}
+
+// See https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
+// for documentation on how UTF-16 encoding works and definitions of various
+// related terms.
+
+/// The inclusive lower bound of Unicode's supplementary plane.
+const _supplementaryPlaneLowerBound = 0x10000;
+
+/// The inclusive upper bound of Unicode's supplementary plane.
+const _supplementaryPlaneUpperBound = 0x10FFFF;
+
+/// The inclusive lower bound of the UTF-16 high surrogate block.
+const _highSurrogateLowerBound = 0xD800;
+
+/// The inclusive lower bound of the UTF-16 low surrogate block.
+const _lowSurrogateLowerBound = 0xDC00;
+
+/// The number of low bits in each code unit of a surrogate pair that goes into
+/// determining which code point it encodes.
+const _surrogateBits = 10;
+
+/// A bit mask that covers the lower [_surrogateBits] of a code point, which can
+/// be used to extract the value of a surrogate or the low surrogate value of a
+/// code unit.
+const _surrogateValueMask = (1 << _surrogateBits) - 1;
+
+/// Returns whether [codePoint] is in the Unicode supplementary plane, and thus
+/// must be represented as a surrogate pair in UTF-16.
+bool inSupplementaryPlane(int codePoint) =>
+    codePoint >= _supplementaryPlaneLowerBound &&
+    codePoint <= _supplementaryPlaneUpperBound;
+
+/// Returns whether [codeUnit] is a UTF-16 high surrogate.
+bool isHighSurrogate(int codeUnit) =>
+    (codeUnit & ~_surrogateValueMask) == _highSurrogateLowerBound;
+
+/// Returns whether [codeUnit] is a UTF-16 low surrogate.
+bool isLowSurrogate(int codeUnit) =>
+    (codeUnit >> _surrogateBits) == (_lowSurrogateLowerBound >> _surrogateBits);
+
+/// Returns the high surrogate needed to encode the supplementary-plane
+/// [codePoint].
+int highSurrogate(int codePoint) {
+  assert(inSupplementaryPlane(codePoint));
+  return ((codePoint - _supplementaryPlaneLowerBound) >> _surrogateBits) +
+      _highSurrogateLowerBound;
+}
+
+/// Returns the low surrogate needed to encode the supplementary-plane
+/// [codePoint].
+int lowSurrogate(int codePoint) {
+  assert(inSupplementaryPlane(codePoint));
+  return ((codePoint - _supplementaryPlaneLowerBound) & _surrogateValueMask) +
+      _lowSurrogateLowerBound;
+}
+
+/// Converts a UTF-16 surrogate pair into the Unicode code unit it represents.
+int decodeSurrogatePair(int highSurrogate, int lowSurrogate) {
+  assert(isHighSurrogate(highSurrogate));
+  assert(isLowSurrogate(lowSurrogate));
+  return _supplementaryPlaneLowerBound +
+      (((highSurrogate & _surrogateValueMask) << _surrogateBits) |
+          (lowSurrogate & _surrogateValueMask));
+}
diff --git a/pkgs/string_scanner/lib/string_scanner.dart b/pkgs/string_scanner/lib/string_scanner.dart
new file mode 100644
index 0000000..e641ae7
--- /dev/null
+++ b/pkgs/string_scanner/lib/string_scanner.dart
@@ -0,0 +1,11 @@
+// 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.
+
+/// A library for parsing strings using a sequence of patterns.
+library;
+
+export 'src/exception.dart';
+export 'src/line_scanner.dart';
+export 'src/span_scanner.dart';
+export 'src/string_scanner.dart';
diff --git a/pkgs/string_scanner/pubspec.yaml b/pkgs/string_scanner/pubspec.yaml
new file mode 100644
index 0000000..9b259cf
--- /dev/null
+++ b/pkgs/string_scanner/pubspec.yaml
@@ -0,0 +1,14 @@
+name: string_scanner
+version: 1.4.1
+description: A class for parsing strings using a sequence of patterns.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/string_scanner
+
+environment:
+  sdk: ^3.1.0
+
+dependencies:
+  source_span: ^1.8.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.6
diff --git a/pkgs/string_scanner/test/error_test.dart b/pkgs/string_scanner/test/error_test.dart
new file mode 100644
index 0000000..1f98c32
--- /dev/null
+++ b/pkgs/string_scanner/test/error_test.dart
@@ -0,0 +1,143 @@
+// 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:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  test('defaults to the last match', () {
+    final scanner = StringScanner('foo bar baz');
+    scanner.expect('foo ');
+    scanner.expect('bar');
+    expect(() => scanner.error('oh no!'), throwsStringScannerException('bar'));
+  });
+
+  group('with match', () {
+    test('supports an earlier match', () {
+      final scanner = StringScanner('foo bar baz');
+      scanner.expect('foo ');
+      final match = scanner.lastMatch;
+      scanner.expect('bar');
+      expect(() => scanner.error('oh no!', match: match),
+          throwsStringScannerException('foo '));
+    });
+
+    test('supports a match on a previous line', () {
+      final scanner = StringScanner('foo bar baz\ndo re mi\nearth fire water');
+      scanner.expect('foo bar baz\ndo ');
+      scanner.expect('re');
+      final match = scanner.lastMatch;
+      scanner.expect(' mi\nearth ');
+      expect(() => scanner.error('oh no!', match: match),
+          throwsStringScannerException('re'));
+    });
+
+    test('supports a multiline match', () {
+      final scanner = StringScanner('foo bar baz\ndo re mi\nearth fire water');
+      scanner.expect('foo bar ');
+      scanner.expect('baz\ndo');
+      final match = scanner.lastMatch;
+      scanner.expect(' re mi');
+      expect(() => scanner.error('oh no!', match: match),
+          throwsStringScannerException('baz\ndo'));
+    });
+
+    test('supports a match after position', () {
+      final scanner = StringScanner('foo bar baz');
+      scanner.expect('foo ');
+      scanner.expect('bar');
+      final match = scanner.lastMatch;
+      scanner.position = 0;
+      expect(() => scanner.error('oh no!', match: match),
+          throwsStringScannerException('bar'));
+    });
+  });
+
+  group('with position and/or length', () {
+    test('defaults to length 0', () {
+      final scanner = StringScanner('foo bar baz');
+      scanner.expect('foo ');
+      expect(() => scanner.error('oh no!', position: 1),
+          throwsStringScannerException(''));
+    });
+
+    test('defaults to the current position', () {
+      final scanner = StringScanner('foo bar baz');
+      scanner.expect('foo ');
+      expect(() => scanner.error('oh no!', length: 3),
+          throwsStringScannerException('bar'));
+    });
+
+    test('supports an earlier position', () {
+      final scanner = StringScanner('foo bar baz');
+      scanner.expect('foo ');
+      expect(() => scanner.error('oh no!', position: 1, length: 2),
+          throwsStringScannerException('oo'));
+    });
+
+    test('supports a position on a previous line', () {
+      final scanner = StringScanner('foo bar baz\ndo re mi\nearth fire water');
+      scanner.expect('foo bar baz\ndo re mi\nearth');
+      expect(() => scanner.error('oh no!', position: 15, length: 2),
+          throwsStringScannerException('re'));
+    });
+
+    test('supports a multiline length', () {
+      final scanner = StringScanner('foo bar baz\ndo re mi\nearth fire water');
+      scanner.expect('foo bar baz\ndo re mi\nearth');
+      expect(() => scanner.error('oh no!', position: 8, length: 8),
+          throwsStringScannerException('baz\ndo r'));
+    });
+
+    test('supports a position after the current one', () {
+      final scanner = StringScanner('foo bar baz');
+      expect(() => scanner.error('oh no!', position: 4, length: 3),
+          throwsStringScannerException('bar'));
+    });
+
+    test('supports a length of zero', () {
+      final scanner = StringScanner('foo bar baz');
+      expect(() => scanner.error('oh no!', position: 4, length: 0),
+          throwsStringScannerException(''));
+    });
+  });
+
+  group('argument errors', () {
+    late StringScanner scanner;
+    setUp(() {
+      scanner = StringScanner('foo bar baz');
+      scanner.scan('foo');
+    });
+
+    test('if match is passed with position', () {
+      expect(
+          () => scanner.error('oh no!', match: scanner.lastMatch, position: 1),
+          throwsArgumentError);
+    });
+
+    test('if match is passed with length', () {
+      expect(() => scanner.error('oh no!', match: scanner.lastMatch, length: 1),
+          throwsArgumentError);
+    });
+
+    test('if position is negative', () {
+      expect(() => scanner.error('oh no!', position: -1), throwsArgumentError);
+    });
+
+    test('if position is outside the string', () {
+      expect(() => scanner.error('oh no!', position: 100), throwsArgumentError);
+    });
+
+    test('if position + length is outside the string', () {
+      expect(() => scanner.error('oh no!', position: 7, length: 7),
+          throwsArgumentError);
+    });
+
+    test('if length is negative', () {
+      expect(() => scanner.error('oh no!', length: -1), throwsArgumentError);
+    });
+  });
+}
diff --git a/pkgs/string_scanner/test/line_scanner_test.dart b/pkgs/string_scanner/test/line_scanner_test.dart
new file mode 100644
index 0000000..1af5c36
--- /dev/null
+++ b/pkgs/string_scanner/test/line_scanner_test.dart
@@ -0,0 +1,465 @@
+// 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:string_scanner/src/charcode.dart';
+import 'package:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late LineScanner scanner;
+  setUp(() {
+    scanner = LineScanner('foo\nbar\r\nbaz');
+  });
+
+  test('begins with line and column 0', () {
+    expect(scanner.line, equals(0));
+    expect(scanner.column, equals(0));
+  });
+
+  group('scan()', () {
+    test('consuming no newlines increases the column but not the line', () {
+      scanner.expect('foo');
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(3));
+    });
+
+    test('consuming a LF resets the column and increases the line', () {
+      scanner.expect('foo\nba');
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(2));
+    });
+
+    test('consuming multiple LFs resets the column and increases the line', () {
+      scanner.expect('foo\nbar\r\nb');
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(1));
+    });
+
+    test('consuming a CR LF increases the line only after the LF', () {
+      scanner.expect('foo\nbar\r');
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(4));
+
+      scanner.expect('\nb');
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(1));
+    });
+
+    test('consuming a CR not followed by LF increases the line', () {
+      scanner = LineScanner('foo\nbar\rbaz');
+      scanner.expect('foo\nbar\r');
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(0));
+
+      scanner.expect('b');
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(1));
+    });
+
+    test('consuming a CR at the end increases the line', () {
+      scanner = LineScanner('foo\nbar\r');
+      scanner.expect('foo\nbar\r');
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(0));
+      expect(scanner.isDone, isTrue);
+    });
+
+    test('consuming a mix of CR, LF, CR+LF increases the line', () {
+      scanner = LineScanner('0\n1\r2\r\n3');
+      scanner.expect('0\n1\r2\r\n3');
+      expect(scanner.line, equals(3));
+      expect(scanner.column, equals(1));
+    });
+
+    test('scanning a zero length match between CR LF does not fail', () {
+      scanner.expect('foo\nbar\r');
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(4));
+      scanner.expect(RegExp('(?!x)'));
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(4));
+    });
+  });
+
+  group('readChar()', () {
+    test('on a non-newline character increases the column but not the line',
+        () {
+      scanner.readChar();
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(1));
+    });
+
+    test('consuming a LF resets the column and increases the line', () {
+      scanner.expect('foo');
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(3));
+
+      scanner.readChar();
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(0));
+    });
+
+    test('consuming a CR LF increases the line only after the LF', () {
+      scanner = LineScanner('foo\r\nbar');
+      scanner.expect('foo');
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(3));
+
+      scanner.readChar();
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(4));
+
+      scanner.readChar();
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(0));
+    });
+
+    test('consuming a CR not followed by a LF increases the line', () {
+      scanner = LineScanner('foo\nbar\rbaz');
+      scanner.expect('foo\nbar');
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(3));
+
+      scanner.readChar();
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(0));
+    });
+
+    test('consuming a CR at the end increases the line', () {
+      scanner = LineScanner('foo\nbar\r');
+      scanner.expect('foo\nbar');
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(3));
+
+      scanner.readChar();
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(0));
+    });
+
+    test('consuming a mix of CR, LF, CR+LF increases the line', () {
+      scanner = LineScanner('0\n1\r2\r\n3');
+      for (var i = 0; i < scanner.string.length; i++) {
+        scanner.readChar();
+      }
+
+      expect(scanner.line, equals(3));
+      expect(scanner.column, equals(1));
+    });
+  });
+
+  group('readCodePoint()', () {
+    test('on a non-newline character increases the column but not the line',
+        () {
+      scanner.readCodePoint();
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(1));
+    });
+
+    test('consuming a newline resets the column and increases the line', () {
+      scanner.expect('foo');
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(3));
+
+      scanner.readCodePoint();
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(0));
+    });
+
+    test("consuming halfway through a CR LF doesn't count as a line", () {
+      scanner.expect('foo\nbar');
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(3));
+
+      scanner.readCodePoint();
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(4));
+
+      scanner.readCodePoint();
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(0));
+    });
+  });
+
+  group('scanChar()', () {
+    test('on a non-newline character increases the column but not the line',
+        () {
+      scanner.scanChar($f);
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(1));
+    });
+
+    test('consuming a LF resets the column and increases the line', () {
+      scanner.expect('foo');
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(3));
+
+      scanner.scanChar($lf);
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(0));
+    });
+
+    test('consuming a CR LF increases the line only after the LF', () {
+      scanner.expect('foo\nbar');
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(3));
+
+      scanner.scanChar($cr);
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(4));
+
+      scanner.scanChar($lf);
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(0));
+    });
+
+    test('consuming a CR not followed by LF increases the line', () {
+      scanner = LineScanner('foo\rbar');
+      scanner.expect('foo');
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(3));
+
+      scanner.scanChar($cr);
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(0));
+    });
+
+    test('consuming a CR at the end increases the line', () {
+      scanner = LineScanner('foo\r');
+      scanner.expect('foo');
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(3));
+
+      scanner.scanChar($cr);
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(0));
+    });
+
+    test('consuming a mix of CR, LF, CR+LF increases the line', () {
+      scanner = LineScanner('0\n1\r2\r\n3');
+      for (var i = 0; i < scanner.string.length; i++) {
+        scanner.scanChar(scanner.string[i].codeUnits.single);
+      }
+
+      expect(scanner.line, equals(3));
+      expect(scanner.column, equals(1));
+    });
+  });
+
+  group('before a surrogate pair', () {
+    final codePoint = '\uD83D\uDC6D'.runes.first;
+    const highSurrogate = 0xD83D;
+
+    late LineScanner scanner;
+    setUp(() {
+      scanner = LineScanner('foo: \uD83D\uDC6D');
+      expect(scanner.scan('foo: '), isTrue);
+    });
+
+    test('readChar returns the high surrogate and moves into the pair', () {
+      expect(scanner.readChar(), equals(highSurrogate));
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(6));
+      expect(scanner.position, equals(6));
+    });
+
+    test('readCodePoint returns the code unit and moves past the pair', () {
+      expect(scanner.readCodePoint(), equals(codePoint));
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(7));
+      expect(scanner.position, equals(7));
+    });
+
+    test('scanChar with the high surrogate moves into the pair', () {
+      expect(scanner.scanChar(highSurrogate), isTrue);
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(6));
+      expect(scanner.position, equals(6));
+    });
+
+    test('scanChar with the code point moves past the pair', () {
+      expect(scanner.scanChar(codePoint), isTrue);
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(7));
+      expect(scanner.position, equals(7));
+    });
+
+    test('expectChar with the high surrogate moves into the pair', () {
+      scanner.expectChar(highSurrogate);
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(6));
+      expect(scanner.position, equals(6));
+    });
+
+    test('expectChar with the code point moves past the pair', () {
+      scanner.expectChar(codePoint);
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(7));
+      expect(scanner.position, equals(7));
+    });
+  });
+
+  group('position=', () {
+    test('forward through LFs sets the line and column', () {
+      scanner = LineScanner('foo\nbar\nbaz');
+      scanner.position = 9; // "foo\nbar\nb"
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(1));
+    });
+
+    test('forward from non-zero character through LFs sets the line and column',
+        () {
+      scanner = LineScanner('foo\nbar\nbaz');
+      scanner.expect('fo');
+      scanner.position = 9; // "foo\nbar\nb"
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(1));
+    });
+
+    test('forward through CR LFs sets the line and column', () {
+      scanner = LineScanner('foo\r\nbar\r\nbaz');
+      scanner.position = 11; // "foo\r\nbar\r\nb"
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(1));
+    });
+
+    test('forward through CR not followed by LFs sets the line and column', () {
+      scanner = LineScanner('foo\rbar\rbaz');
+      scanner.position = 9; // "foo\rbar\rb"
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(1));
+    });
+
+    test('forward through CR at end sets the line and column', () {
+      scanner = LineScanner('foo\rbar\r');
+      scanner.position = 8; // "foo\rbar\r"
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(0));
+    });
+
+    test('forward through a mix of CR, LF, CR+LF sets the line and column', () {
+      scanner = LineScanner('0\n1\r2\r\n3');
+      scanner.position = scanner.string.length;
+
+      expect(scanner.line, equals(3));
+      expect(scanner.column, equals(1));
+    });
+
+    test('forward through no newlines sets the column', () {
+      scanner.position = 2; // "fo"
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(2));
+    });
+
+    test('backward through LFs sets the line and column', () {
+      scanner = LineScanner('foo\nbar\nbaz');
+      scanner.expect('foo\nbar\nbaz');
+      scanner.position = 2; // "fo"
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(2));
+    });
+
+    test('backward through CR LFs sets the line and column', () {
+      scanner = LineScanner('foo\r\nbar\r\nbaz');
+      scanner.expect('foo\r\nbar\r\nbaz');
+      scanner.position = 2; // "fo"
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(2));
+    });
+
+    test('backward through CR not followed by LFs sets the line and column',
+        () {
+      scanner = LineScanner('foo\rbar\rbaz');
+      scanner.expect('foo\rbar\rbaz');
+      scanner.position = 2; // "fo"
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(2));
+    });
+
+    test('backward through CR at end sets the line and column', () {
+      scanner = LineScanner('foo\rbar\r');
+      scanner.expect('foo\rbar\r');
+      scanner.position = 2; // "fo"
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(2));
+    });
+
+    test('backward through a mix of CR, LF, CR+LF sets the line and column',
+        () {
+      scanner = LineScanner('0\n1\r2\r\n3');
+      scanner.expect(scanner.string);
+
+      scanner.position = 1;
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(1));
+    });
+
+    test('backward through no newlines sets the column', () {
+      scanner.expect('foo\nbar\r\nbaz');
+      scanner.position = 10; // "foo\nbar\r\nb"
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(1));
+    });
+
+    test("forward halfway through a CR LF doesn't count as a line", () {
+      scanner.position = 8; // "foo\nbar\r"
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(4));
+    });
+
+    test('forward from halfway through a CR LF counts as a line', () {
+      scanner.expect('foo\nbar\r');
+      scanner.position = 11; // "foo\nbar\r\nba"
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(2));
+    });
+
+    test('backward to between CR LF', () {
+      scanner.expect('foo\nbar\r\nbaz');
+      scanner.position = 8; // "foo\nbar\r"
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(4));
+    });
+
+    test('backward from between CR LF', () {
+      scanner.expect('foo\nbar\r');
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(4));
+      scanner.position = 5; // "foo\nb"
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(1));
+    });
+
+    test('backward to after CR LF', () {
+      scanner.expect('foo\nbar\r\nbaz');
+      scanner.position = 9; // "foo\nbar\r\n"
+      expect(scanner.line, equals(2));
+      expect(scanner.column, equals(0));
+    });
+
+    test('backward to before CR LF', () {
+      scanner.expect('foo\nbar\r\nbaz');
+      scanner.position = 7; // "foo\nbar"
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(3));
+    });
+  });
+
+  test('state= restores the line, column, and position', () {
+    scanner.expect('foo\nb');
+    final state = scanner.state;
+
+    scanner.scan('ar\nba');
+    scanner.state = state;
+    expect(scanner.rest, equals('ar\r\nbaz'));
+    expect(scanner.line, equals(1));
+    expect(scanner.column, equals(1));
+  });
+
+  test('state= rejects a foreign state', () {
+    scanner.scan('foo\nb');
+
+    expect(() => LineScanner(scanner.string).state = scanner.state,
+        throwsArgumentError);
+  });
+}
diff --git a/pkgs/string_scanner/test/span_scanner_test.dart b/pkgs/string_scanner/test/span_scanner_test.dart
new file mode 100644
index 0000000..93d9c47
--- /dev/null
+++ b/pkgs/string_scanner/test/span_scanner_test.dart
@@ -0,0 +1,238 @@
+// 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:source_span/source_span.dart';
+import 'package:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  testForImplementation(
+      'lazy',
+      ([String? string]) =>
+          SpanScanner(string ?? 'foo\nbar\nbaz', sourceUrl: 'source'));
+
+  testForImplementation(
+      'eager',
+      ([String? string]) =>
+          SpanScanner.eager(string ?? 'foo\nbar\nbaz', sourceUrl: 'source'));
+
+  group('within', () {
+    const text = 'first\nbefore: foo\nbar\nbaz :after\nlast';
+    final startOffset = text.indexOf('foo');
+
+    late SpanScanner scanner;
+    setUp(() {
+      final file = SourceFile.fromString(text, url: 'source');
+      scanner =
+          SpanScanner.within(file.span(startOffset, text.indexOf(' :after')));
+    });
+
+    test('string only includes the span text', () {
+      expect(scanner.string, equals('foo\nbar\nbaz'));
+    });
+
+    test('line and column are span-relative', () {
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(0));
+
+      scanner.scan('foo');
+      expect(scanner.line, equals(0));
+      expect(scanner.column, equals(3));
+
+      scanner.scan('\n');
+      expect(scanner.line, equals(1));
+      expect(scanner.column, equals(0));
+    });
+
+    test('tracks the span for the last match', () {
+      scanner.scan('fo');
+      scanner.scan('o\nba');
+
+      final span = scanner.lastSpan!;
+      expect(span.start.offset, equals(startOffset + 2));
+      expect(span.start.line, equals(1));
+      expect(span.start.column, equals(10));
+      expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+      expect(span.end.offset, equals(startOffset + 6));
+      expect(span.end.line, equals(2));
+      expect(span.end.column, equals(2));
+      expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+      expect(span.text, equals('o\nba'));
+    });
+
+    test('.spanFrom() returns a span from a previous state', () {
+      scanner.scan('fo');
+      final state = scanner.state;
+      scanner.scan('o\nba');
+      scanner.scan('r\nba');
+
+      final span = scanner.spanFrom(state);
+      expect(span.text, equals('o\nbar\nba'));
+    });
+
+    test('.spanFromPosition() returns a span from a previous state', () {
+      scanner.scan('fo');
+      final start = scanner.position;
+      scanner.scan('o\nba');
+      scanner.scan('r\nba');
+
+      final span = scanner.spanFromPosition(start + 2, start + 5);
+      expect(span.text, equals('bar'));
+    });
+
+    test('.emptySpan returns an empty span at the current location', () {
+      scanner.scan('foo\nba');
+
+      final span = scanner.emptySpan;
+      expect(span.start.offset, equals(startOffset + 6));
+      expect(span.start.line, equals(2));
+      expect(span.start.column, equals(2));
+      expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+      expect(span.end.offset, equals(startOffset + 6));
+      expect(span.end.line, equals(2));
+      expect(span.end.column, equals(2));
+      expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+      expect(span.text, equals(''));
+    });
+
+    test('.error() uses an absolute span', () {
+      scanner.expect('foo');
+      expect(
+          () => scanner.error('oh no!'), throwsStringScannerException('foo'));
+    });
+
+    test('.isDone returns true at the end of the span', () {
+      scanner.expect('foo\nbar\nbaz');
+      expect(scanner.isDone, isTrue);
+    });
+  });
+}
+
+void testForImplementation(
+    String name, SpanScanner Function([String string]) create) {
+  group('for a $name scanner', () {
+    late SpanScanner scanner;
+    setUp(() => scanner = create());
+
+    test('tracks the span for the last match', () {
+      scanner.scan('fo');
+      scanner.scan('o\nba');
+
+      final span = scanner.lastSpan!;
+      expect(span.start.offset, equals(2));
+      expect(span.start.line, equals(0));
+      expect(span.start.column, equals(2));
+      expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+      expect(span.end.offset, equals(6));
+      expect(span.end.line, equals(1));
+      expect(span.end.column, equals(2));
+      expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+      expect(span.text, equals('o\nba'));
+    });
+
+    test('.spanFrom() returns a span from a previous state', () {
+      scanner.scan('fo');
+      final state = scanner.state;
+      scanner.scan('o\nba');
+      scanner.scan('r\nba');
+
+      final span = scanner.spanFrom(state);
+      expect(span.text, equals('o\nbar\nba'));
+    });
+
+    test('.spanFromPosition() returns a span from a previous state', () {
+      scanner.scan('fo');
+      final start = scanner.position;
+      scanner.scan('o\nba');
+      scanner.scan('r\nba');
+
+      final span = scanner.spanFromPosition(start + 2, start + 5);
+      expect(span.text, equals('bar'));
+    });
+
+    test('.emptySpan returns an empty span at the current location', () {
+      scanner.scan('foo\nba');
+
+      final span = scanner.emptySpan;
+      expect(span.start.offset, equals(6));
+      expect(span.start.line, equals(1));
+      expect(span.start.column, equals(2));
+      expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+      expect(span.end.offset, equals(6));
+      expect(span.end.line, equals(1));
+      expect(span.end.column, equals(2));
+      expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+      expect(span.text, equals(''));
+    });
+
+    group('before a surrogate pair', () {
+      final codePoint = '\uD83D\uDC6D'.runes.first;
+      const highSurrogate = 0xD83D;
+
+      late SpanScanner scanner;
+      setUp(() {
+        scanner = create('foo: \uD83D\uDC6D bar');
+        expect(scanner.scan('foo: '), isTrue);
+      });
+
+      test('readChar returns the high surrogate and moves into the pair', () {
+        expect(scanner.readChar(), equals(highSurrogate));
+        expect(scanner.line, equals(0));
+        expect(scanner.column, equals(6));
+        expect(scanner.position, equals(6));
+      });
+
+      test('readCodePoint returns the code unit and moves past the pair', () {
+        expect(scanner.readCodePoint(), equals(codePoint));
+        expect(scanner.line, equals(0));
+        expect(scanner.column, equals(7));
+        expect(scanner.position, equals(7));
+      });
+
+      test('scanChar with the high surrogate moves into the pair', () {
+        expect(scanner.scanChar(highSurrogate), isTrue);
+        expect(scanner.line, equals(0));
+        expect(scanner.column, equals(6));
+        expect(scanner.position, equals(6));
+      });
+
+      test('scanChar with the code point moves past the pair', () {
+        expect(scanner.scanChar(codePoint), isTrue);
+        expect(scanner.line, equals(0));
+        expect(scanner.column, equals(7));
+        expect(scanner.position, equals(7));
+      });
+
+      test('expectChar with the high surrogate moves into the pair', () {
+        scanner.expectChar(highSurrogate);
+        expect(scanner.line, equals(0));
+        expect(scanner.column, equals(6));
+        expect(scanner.position, equals(6));
+      });
+
+      test('expectChar with the code point moves past the pair', () {
+        scanner.expectChar(codePoint);
+        expect(scanner.line, equals(0));
+        expect(scanner.column, equals(7));
+        expect(scanner.position, equals(7));
+      });
+
+      test('spanFrom covers the surrogate pair', () {
+        final state = scanner.state;
+        scanner.scan('\uD83D\uDC6D b');
+        expect(scanner.spanFrom(state).text, equals('\uD83D\uDC6D b'));
+      });
+    });
+  });
+}
diff --git a/pkgs/string_scanner/test/string_scanner_test.dart b/pkgs/string_scanner/test/string_scanner_test.dart
new file mode 100644
index 0000000..36a737e
--- /dev/null
+++ b/pkgs/string_scanner/test/string_scanner_test.dart
@@ -0,0 +1,564 @@
+// 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:string_scanner/src/charcode.dart';
+import 'package:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('with an empty string', () {
+    late StringScanner scanner;
+    setUp(() {
+      scanner = StringScanner('');
+    });
+
+    test('is done', () {
+      expect(scanner.isDone, isTrue);
+      expect(scanner.expectDone, isNot(throwsFormatException));
+    });
+
+    test('rest is empty', () {
+      expect(scanner.rest, isEmpty);
+    });
+
+    test('lastMatch is null', () {
+      expect(scanner.lastMatch, isNull);
+    });
+
+    test('position is zero', () {
+      expect(scanner.position, equals(0));
+    });
+
+    test("readChar fails and doesn't change the state", () {
+      expect(scanner.readChar, throwsFormatException);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test("readCodePoint fails and doesn't change the state", () {
+      expect(scanner.readCodePoint, throwsFormatException);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test("peekChar returns null and doesn't change the state", () {
+      expect(scanner.peekChar(), isNull);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test("peekCodePoint returns null and doesn't change the state", () {
+      expect(scanner.peekCodePoint(), isNull);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test("scanChar returns false and doesn't change the state", () {
+      expect(scanner.scanChar($f), isFalse);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test("expectChar fails and doesn't change the state", () {
+      expect(() => scanner.expectChar($f), throwsFormatException);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test("scan returns false and doesn't change the state", () {
+      expect(scanner.scan(RegExp('.')), isFalse);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test("expect throws a FormatException and doesn't change the state", () {
+      expect(() => scanner.expect(RegExp('.')), throwsFormatException);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test("matches returns false and doesn't change the state", () {
+      expect(scanner.matches(RegExp('.')), isFalse);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test('substring returns the empty string', () {
+      expect(scanner.substring(0), isEmpty);
+    });
+
+    test('setting position to 1 throws an ArgumentError', () {
+      expect(() {
+        scanner.position = 1;
+      }, throwsArgumentError);
+    });
+
+    test('setting position to -1 throws an ArgumentError', () {
+      expect(() {
+        scanner.position = -1;
+      }, throwsArgumentError);
+    });
+  });
+
+  group('at the beginning of a string', () {
+    late StringScanner scanner;
+    setUp(() {
+      scanner = StringScanner('foo bar');
+    });
+
+    test('is not done', () {
+      expect(scanner.isDone, isFalse);
+      expect(scanner.expectDone, throwsFormatException);
+    });
+
+    test('rest is the whole string', () {
+      expect(scanner.rest, equals('foo bar'));
+    });
+
+    test('lastMatch is null', () {
+      expect(scanner.lastMatch, isNull);
+    });
+
+    test('position is zero', () {
+      expect(scanner.position, equals(0));
+    });
+
+    test('readChar returns the first character and moves forward', () {
+      expect(scanner.readChar(), equals(0x66));
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(1));
+    });
+
+    test('readCodePoint returns the first character and moves forward', () {
+      expect(scanner.readCodePoint(), equals(0x66));
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(1));
+    });
+
+    test('peekChar returns the first character', () {
+      expect(scanner.peekChar(), equals(0x66));
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test('peekChar with an argument returns the nth character', () {
+      expect(scanner.peekChar(4), equals(0x62));
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test('peekCodePoint returns the first character', () {
+      expect(scanner.peekCodePoint(), equals(0x66));
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test('a matching scanChar returns true moves forward', () {
+      expect(scanner.scanChar($f), isTrue);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(1));
+    });
+
+    test('a non-matching scanChar returns false and does nothing', () {
+      expect(scanner.scanChar($x), isFalse);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test('a matching expectChar moves forward', () {
+      scanner.expectChar($f);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(1));
+    });
+
+    test('a non-matching expectChar fails', () {
+      expect(() => scanner.expectChar($x), throwsFormatException);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+    });
+
+    test('a matching scan returns true and changes the state', () {
+      expect(scanner.scan(RegExp('f(..)')), isTrue);
+      expect(scanner.lastMatch![1], equals('oo'));
+      expect(scanner.position, equals(3));
+      expect(scanner.rest, equals(' bar'));
+    });
+
+    test('a non-matching scan returns false and sets lastMatch to null', () {
+      expect(scanner.matches(RegExp('f(..)')), isTrue);
+      expect(scanner.lastMatch, isNotNull);
+
+      expect(scanner.scan(RegExp('b(..)')), isFalse);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+      expect(scanner.rest, equals('foo bar'));
+    });
+
+    test('a matching expect changes the state', () {
+      scanner.expect(RegExp('f(..)'));
+      expect(scanner.lastMatch![1], equals('oo'));
+      expect(scanner.position, equals(3));
+      expect(scanner.rest, equals(' bar'));
+    });
+
+    test(
+        'a non-matching expect throws a FormatException and sets lastMatch to '
+        'null', () {
+      expect(scanner.matches(RegExp('f(..)')), isTrue);
+      expect(scanner.lastMatch, isNotNull);
+
+      expect(() => scanner.expect(RegExp('b(..)')), throwsFormatException);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+      expect(scanner.rest, equals('foo bar'));
+    });
+
+    test('a matching matches returns true and only changes lastMatch', () {
+      expect(scanner.matches(RegExp('f(..)')), isTrue);
+      expect(scanner.lastMatch![1], equals('oo'));
+      expect(scanner.position, equals(0));
+      expect(scanner.rest, equals('foo bar'));
+    });
+
+    test("a non-matching matches returns false and doesn't change the state",
+        () {
+      expect(scanner.matches(RegExp('b(..)')), isFalse);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(0));
+      expect(scanner.rest, equals('foo bar'));
+    });
+
+    test('substring from the beginning returns the empty string', () {
+      expect(scanner.substring(0), isEmpty);
+    });
+
+    test('substring with a custom end returns the substring', () {
+      expect(scanner.substring(0, 3), equals('foo'));
+    });
+
+    test('substring with the string length returns the whole string', () {
+      expect(scanner.substring(0, 7), equals('foo bar'));
+    });
+
+    test('setting position to 1 moves the cursor forward', () {
+      scanner.position = 1;
+      expect(scanner.position, equals(1));
+      expect(scanner.rest, equals('oo bar'));
+
+      expect(scanner.scan(RegExp('oo.')), isTrue);
+      expect(scanner.lastMatch![0], equals('oo '));
+      expect(scanner.position, equals(4));
+      expect(scanner.rest, equals('bar'));
+    });
+
+    test('setting position beyond the string throws an ArgumentError', () {
+      expect(() {
+        scanner.position = 8;
+      }, throwsArgumentError);
+    });
+
+    test('setting position to -1 throws an ArgumentError', () {
+      expect(() {
+        scanner.position = -1;
+      }, throwsArgumentError);
+    });
+
+    test('scan accepts any Pattern', () {
+      expect(scanner.scan('foo'), isTrue);
+      expect(scanner.lastMatch![0], equals('foo'));
+      expect(scanner.position, equals(3));
+      expect(scanner.rest, equals(' bar'));
+    });
+
+    test('scans multiple times', () {
+      expect(scanner.scan(RegExp('f(..)')), isTrue);
+      expect(scanner.lastMatch![1], equals('oo'));
+      expect(scanner.position, equals(3));
+      expect(scanner.rest, equals(' bar'));
+
+      expect(scanner.scan(RegExp(' b(..)')), isTrue);
+      expect(scanner.lastMatch![1], equals('ar'));
+      expect(scanner.position, equals(7));
+      expect(scanner.rest, equals(''));
+      expect(scanner.isDone, isTrue);
+      expect(scanner.expectDone, isNot(throwsFormatException));
+    });
+  });
+
+  group('after a scan', () {
+    late StringScanner scanner;
+    setUp(() {
+      scanner = StringScanner('foo bar');
+      expect(scanner.scan('foo'), isTrue);
+    });
+
+    test('readChar returns the first character and unsets the last match', () {
+      expect(scanner.readChar(), equals($space));
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(4));
+    });
+
+    test('readCodePoint returns the first character and unsets the last match',
+        () {
+      expect(scanner.readCodePoint(), equals($space));
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(4));
+    });
+
+    test('a matching scanChar returns true and unsets the last match', () {
+      expect(scanner.scanChar($space), isTrue);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(4));
+    });
+
+    test('a matching expectChar returns true and unsets the last match', () {
+      scanner.expectChar($space);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(4));
+    });
+  });
+
+  group('at the end of a string', () {
+    late StringScanner scanner;
+    setUp(() {
+      scanner = StringScanner('foo bar');
+      expect(scanner.scan('foo bar'), isTrue);
+    });
+
+    test('is done', () {
+      expect(scanner.isDone, isTrue);
+      expect(scanner.expectDone, isNot(throwsFormatException));
+    });
+
+    test('rest is empty', () {
+      expect(scanner.rest, isEmpty);
+    });
+
+    test('position is zero', () {
+      expect(scanner.position, equals(7));
+    });
+
+    test("readChar fails and doesn't change the state", () {
+      expect(scanner.readChar, throwsFormatException);
+      expect(scanner.lastMatch, isNotNull);
+      expect(scanner.position, equals(7));
+    });
+
+    test("readCodePoint fails and doesn't change the state", () {
+      expect(scanner.readCodePoint, throwsFormatException);
+      expect(scanner.lastMatch, isNotNull);
+      expect(scanner.position, equals(7));
+    });
+
+    test("peekChar returns null and doesn't change the state", () {
+      expect(scanner.peekChar(), isNull);
+      expect(scanner.lastMatch, isNotNull);
+      expect(scanner.position, equals(7));
+    });
+
+    test("peekCodePoint returns null and doesn't change the state", () {
+      expect(scanner.peekCodePoint(), isNull);
+      expect(scanner.lastMatch, isNotNull);
+      expect(scanner.position, equals(7));
+    });
+
+    test("scanChar returns false and doesn't change the state", () {
+      expect(scanner.scanChar($f), isFalse);
+      expect(scanner.lastMatch, isNotNull);
+      expect(scanner.position, equals(7));
+    });
+
+    test("expectChar fails and doesn't change the state", () {
+      expect(() => scanner.expectChar($f), throwsFormatException);
+      expect(scanner.lastMatch, isNotNull);
+      expect(scanner.position, equals(7));
+    });
+
+    test('scan returns false and sets lastMatch to null', () {
+      expect(scanner.scan(RegExp('.')), isFalse);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(7));
+    });
+
+    test('expect throws a FormatException and sets lastMatch to null', () {
+      expect(() => scanner.expect(RegExp('.')), throwsFormatException);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(7));
+    });
+
+    test('matches returns false sets lastMatch to null', () {
+      expect(scanner.matches(RegExp('.')), isFalse);
+      expect(scanner.lastMatch, isNull);
+      expect(scanner.position, equals(7));
+    });
+
+    test('substring from the beginning returns the whole string', () {
+      expect(scanner.substring(0), equals('foo bar'));
+    });
+
+    test('substring with a custom start returns a substring from there', () {
+      expect(scanner.substring(4), equals('bar'));
+    });
+
+    test('substring with a custom start and end returns that substring', () {
+      expect(scanner.substring(3, 5), equals(' b'));
+    });
+
+    test('setting position to 1 moves the cursor backward', () {
+      scanner.position = 1;
+      expect(scanner.position, equals(1));
+      expect(scanner.rest, equals('oo bar'));
+
+      expect(scanner.scan(RegExp('oo.')), isTrue);
+      expect(scanner.lastMatch![0], equals('oo '));
+      expect(scanner.position, equals(4));
+      expect(scanner.rest, equals('bar'));
+    });
+
+    test('setting and resetting position clears lastMatch', () {
+      final oldPosition = scanner.position;
+      scanner.position = 1;
+      scanner.position = oldPosition;
+      expect(scanner.lastMatch, isNull);
+    });
+
+    test('setting position beyond the string throws an ArgumentError', () {
+      expect(() {
+        scanner.position = 8;
+      }, throwsArgumentError);
+    });
+
+    test('setting position to -1 throws an ArgumentError', () {
+      expect(() {
+        scanner.position = -1;
+      }, throwsArgumentError);
+    });
+  });
+
+  group('before a surrogate pair', () {
+    final codePoint = '\uD83D\uDC6D'.runes.first;
+    const highSurrogate = 0xD83D;
+
+    late StringScanner scanner;
+    setUp(() {
+      scanner = StringScanner('foo: \uD83D\uDC6D');
+      expect(scanner.scan('foo: '), isTrue);
+    });
+
+    test('readChar returns the high surrogate and moves into the pair', () {
+      expect(scanner.readChar(), equals(highSurrogate));
+      expect(scanner.position, equals(6));
+    });
+
+    test('readCodePoint returns the code unit and moves past the pair', () {
+      expect(scanner.readCodePoint(), equals(codePoint));
+      expect(scanner.position, equals(7));
+    });
+
+    test('peekChar returns the high surrogate', () {
+      expect(scanner.peekChar(), equals(highSurrogate));
+      expect(scanner.position, equals(5));
+    });
+
+    test('peekCodePoint returns the code unit', () {
+      expect(scanner.peekCodePoint(), equals(codePoint));
+      expect(scanner.position, equals(5));
+    });
+
+    test('scanChar with the high surrogate moves into the pair', () {
+      expect(scanner.scanChar(highSurrogate), isTrue);
+      expect(scanner.position, equals(6));
+    });
+
+    test('scanChar with the code point moves past the pair', () {
+      expect(scanner.scanChar(codePoint), isTrue);
+      expect(scanner.position, equals(7));
+    });
+
+    test('expectChar with the high surrogate moves into the pair', () {
+      scanner.expectChar(highSurrogate);
+      expect(scanner.position, equals(6));
+    });
+
+    test('expectChar with the code point moves past the pair', () {
+      scanner.expectChar(codePoint);
+      expect(scanner.position, equals(7));
+    });
+  });
+
+  group('before an invalid surrogate pair', () {
+    // This surrogate pair is invalid because U+E000 is just outside the range
+    // of low surrogates. If it were interpreted as a surrogate pair anyway, the
+    // value would be U+110000, which is outside of the Unicode gamut.
+    const codePoint = 0x110000;
+    const highSurrogate = 0xD800;
+
+    late StringScanner scanner;
+    setUp(() {
+      scanner = StringScanner('foo: \uD800\uE000');
+      expect(scanner.scan('foo: '), isTrue);
+    });
+
+    test('readChar returns the high surrogate and moves into the pair', () {
+      expect(scanner.readChar(), equals(highSurrogate));
+      expect(scanner.position, equals(6));
+    });
+
+    test('readCodePoint returns the high surrogate and moves past the pair',
+        () {
+      expect(scanner.readCodePoint(), equals(highSurrogate));
+      expect(scanner.position, equals(6));
+    });
+
+    test('peekChar returns the high surrogate', () {
+      expect(scanner.peekChar(), equals(highSurrogate));
+      expect(scanner.position, equals(5));
+    });
+
+    test('peekCodePoint returns the high surrogate', () {
+      expect(scanner.peekCodePoint(), equals(highSurrogate));
+      expect(scanner.position, equals(5));
+    });
+
+    test('scanChar with the high surrogate moves into the pair', () {
+      expect(scanner.scanChar(highSurrogate), isTrue);
+      expect(scanner.position, equals(6));
+    });
+
+    test('scanChar with the fake code point returns false', () {
+      expect(scanner.scanChar(codePoint), isFalse);
+      expect(scanner.position, equals(5));
+    });
+
+    test('expectChar with the high surrogate moves into the pair', () {
+      scanner.expectChar(highSurrogate);
+      expect(scanner.position, equals(6));
+    });
+
+    test('expectChar with the fake code point fails', () {
+      expect(() => scanner.expectChar(codePoint), throwsRangeError);
+    });
+  });
+
+  group('a scanner constructed with a custom position', () {
+    test('starts scanning from that position', () {
+      final scanner = StringScanner('foo bar', position: 1);
+      expect(scanner.position, equals(1));
+      expect(scanner.rest, equals('oo bar'));
+
+      expect(scanner.scan(RegExp('oo.')), isTrue);
+      expect(scanner.lastMatch![0], equals('oo '));
+      expect(scanner.position, equals(4));
+      expect(scanner.rest, equals('bar'));
+    });
+
+    test('throws an ArgumentError if the position is -1', () {
+      expect(() => StringScanner('foo bar', position: -1), throwsArgumentError);
+    });
+
+    test('throws an ArgumentError if the position is beyond the string', () {
+      expect(() => StringScanner('foo bar', position: 8), throwsArgumentError);
+    });
+  });
+}
diff --git a/pkgs/string_scanner/test/utils.dart b/pkgs/string_scanner/test/utils.dart
new file mode 100644
index 0000000..ca03c06
--- /dev/null
+++ b/pkgs/string_scanner/test/utils.dart
@@ -0,0 +1,12 @@
+// 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:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+/// Returns a matcher that asserts that a closure throws a
+/// [StringScannerException] with the given [text].
+Matcher throwsStringScannerException(String text) =>
+    throwsA(const TypeMatcher<StringScannerException>()
+        .having((e) => e.span!.text, 'span.text', text));
diff --git a/pkgs/term_glyph/.gitignore b/pkgs/term_glyph/.gitignore
new file mode 100644
index 0000000..01d42c0
--- /dev/null
+++ b/pkgs/term_glyph/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.pub/
+.packages
+pubspec.lock
diff --git a/pkgs/term_glyph/AUTHORS b/pkgs/term_glyph/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/term_glyph/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/term_glyph/CHANGELOG.md b/pkgs/term_glyph/CHANGELOG.md
new file mode 100644
index 0000000..bf8eb79
--- /dev/null
+++ b/pkgs/term_glyph/CHANGELOG.md
@@ -0,0 +1,33 @@
+## 1.2.3-wip
+
+## 1.2.2
+
+* Require Dart 3.1
+* Move to `dart-lang/tools` monorepo.
+
+## 1.2.1
+
+* Migrate to `package:lints`.
+* Populate the pubspec `repository` field.
+
+## 1.2.0
+
+* Stable release for null safety.
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+## 1.1.0
+
+* Add a `GlyphSet` class that can be used to easily choose which set of glyphs
+  to use for a particular chunk of code.
+
+* Add `asciiGlyphs`, `unicodeGlyphs`, and `glyphs` getters that provide access
+  to `GlyphSet`s.
+
+## 1.0.1
+
+* Set max SDK version to `<3.0.0`.
+
+## 1.0.0
+
+* Initial version.
diff --git a/pkgs/term_glyph/LICENSE b/pkgs/term_glyph/LICENSE
new file mode 100644
index 0000000..03af64a
--- /dev/null
+++ b/pkgs/term_glyph/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2017, 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/term_glyph/README.md b/pkgs/term_glyph/README.md
new file mode 100644
index 0000000..75039aa
--- /dev/null
+++ b/pkgs/term_glyph/README.md
@@ -0,0 +1,47 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/term_glyph.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/term_glyph.yaml)
+[![pub package](https://img.shields.io/pub/v/term_glyph.svg)](https://pub.dev/packages/term_glyph)
+[![package publisher](https://img.shields.io/pub/publisher/term_glyph.svg)](https://pub.dev/packages/term_glyph/publisher)
+
+This library contains getters for useful Unicode glyphs as well as plain ASCII
+alternatives. It's intended to be used in command-line applications that may run
+in places where Unicode isn't well-supported and libraries that may be used by
+those applications.
+
+We recommend that you import this library with the prefix "glyph". For example:
+
+```dart
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+/// Formats [items] into a bulleted list, with one item per line.
+String bulletedList(List<String> items) =>
+    items.map((item) => "${glyph.bullet} $item").join("\n");
+```
+
+## ASCII Mode
+
+Some shells are unable to display Unicode characters, so this package is able to
+transparently switch its glyphs to ASCII alternatives by setting [the `ascii`
+attribute][ascii]. When this attribute is `true`, all glyphs use ASCII
+characters instead. It currently defaults to `false`, although in the future it
+may default to `true` for applications running on the Dart VM on Windows. For
+example:
+
+[ascii]: https://pub.dev/documentation/term_glyph/latest/term_glyph/ascii.html
+
+```dart
+import 'dart:io';
+
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+void main() {
+  glyph.ascii = Platform.isWindows;
+
+  // Prints "Unicode => ASCII" on Windows, "Unicode ━▶ ASCII" everywhere else.
+  print("Unicode ${glyph.rightArrow} ASCII");
+}
+```
+
+All ASCII glyphs are guaranteed to be the same number of characters as the
+corresponding Unicode glyphs, so that they line up properly when printed on a
+terminal. The specific ASCII text for a given Unicode glyph may change over
+time; this is not considered a breaking change.
diff --git a/pkgs/term_glyph/analysis_options.yaml b/pkgs/term_glyph/analysis_options.yaml
new file mode 100644
index 0000000..6d74ee9
--- /dev/null
+++ b/pkgs/term_glyph/analysis_options.yaml
@@ -0,0 +1,32 @@
+# 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
+    - 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
+    - prefer_final_locals
+    - unnecessary_await_in_return
+    - unnecessary_breaks
+    - use_if_null_to_convert_nulls_to_bools
+    - use_raw_strings
+    - use_string_buffers
diff --git a/pkgs/term_glyph/data.csv b/pkgs/term_glyph/data.csv
new file mode 100644
index 0000000..92a72f7
--- /dev/null
+++ b/pkgs/term_glyph/data.csv
@@ -0,0 +1,85 @@
+# Miscellaneous
+bullet,•,*,A bullet point.
+
+# Arrows
+leftArrow,←,<,"A left-pointing arrow.
+
+Note that the Unicode arrow glyphs may overlap with adjacent characters in some
+terminal fonts, and should generally be surrounding by spaces."
+rightArrow,→,>,"A right-pointing arrow.
+
+Note that the Unicode arrow glyphs may overlap with adjacent characters in some
+terminal fonts, and should generally be surrounding by spaces."
+upArrow,↑,^,An upwards-pointing arrow.
+downArrow,↓,v,A downwards-pointing arrow.
+longLeftArrow,◀━,<=,A two-character left-pointing arrow.
+longRightArrow,━▶,=>,A two-character right-pointing arrow.
+
+# Box drawing characters
+
+## Normal
+horizontalLine,─,-,A horizontal line that can be used to draw a box.
+verticalLine,│,|,A vertical line that can be used to draw a box.
+topLeftCorner,┌,",",The upper left-hand corner of a box.
+topRightCorner,┐,",",The upper right-hand corner of a box.
+bottomLeftCorner,└,',The lower left-hand corner of a box.
+bottomRightCorner,┘,',The lower right-hand corner of a box.
+cross,┼,+,An intersection of vertical and horizontal box lines.
+teeUp,┴,+,A horizontal box line with a vertical line going up from the middle.
+teeDown,┬,+,A horizontal box line with a vertical line going down from the middle.
+teeLeft,┤,+,A vertical box line with a horizontal line going left from the middle.
+teeRight,├,+,A vertical box line with a horizontal line going right from the middle.
+upEnd,╵,',The top half of a vertical box line.
+downEnd,╷,",",The bottom half of a vertical box line.
+leftEnd,╴,-,The left half of a horizontal box line.
+rightEnd,╶,-,The right half of a horizontal box line.
+
+## Bold
+horizontalLineBold,━,=,A bold horizontal line that can be used to draw a box.
+verticalLineBold,┃,|,A bold vertical line that can be used to draw a box.
+topLeftCornerBold,┏,",",The bold upper left-hand corner of a box.
+topRightCornerBold,┓,",",The bold upper right-hand corner of a box.
+bottomLeftCornerBold,┗,',The bold lower left-hand corner of a box.
+bottomRightCornerBold,┛,',The bold lower right-hand corner of a box.
+crossBold,╋,+,An intersection of bold vertical and horizontal box lines.
+teeUpBold,┻,+,A bold horizontal box line with a vertical line going up from the middle.
+teeDownBold,┳,+,A bold horizontal box line with a vertical line going down from the middle.
+teeLeftBold,┫,+,A bold vertical box line with a horizontal line going left from the middle.
+teeRightBold,┣,+,A bold vertical box line with a horizontal line going right from the middle.
+upEndBold,╹,',The top half of a bold vertical box line.
+downEndBold,╻,",",The bottom half of a bold vertical box line.
+leftEndBold,╸,-,The left half of a bold horizontal box line.
+rightEndBold,╺,-,The right half of a bold horizontal box line.
+
+## Double
+horizontalLineDouble,═,=,A double horizontal line that can be used to draw a box.
+verticalLineDouble,║,|,A double vertical line that can be used to draw a box.
+topLeftCornerDouble,╔,",",The double upper left-hand corner of a box.
+topRightCornerDouble,╗,",",The double upper right-hand corner of a box.
+bottomLeftCornerDouble,╚,"""",The double lower left-hand corner of a box.
+bottomRightCornerDouble,╝,"""",The double lower right-hand corner of a box.
+crossDouble,╬,+,An intersection of double vertical and horizontal box lines.
+teeUpDouble,╩,+,A double horizontal box line with a vertical line going up from the middle.
+teeDownDouble,╦,+,A double horizontal box line with a vertical line going down from the middle.
+teeLeftDouble,╣,+,A double vertical box line with a horizontal line going left from the middle.
+teeRightDouble,╠,+,A double vertical box line with a horizontal line going right from the middle.
+
+## Dashed
+
+### Double
+horizontalLineDoubleDash,╌,-,A dashed horizontal line that can be used to draw a box.
+horizontalLineDoubleDashBold,╍,-,A bold dashed horizontal line that can be used to draw a box.
+verticalLineDoubleDash,╎,|,A dashed vertical line that can be used to draw a box.
+verticalLineDoubleDashBold,╏,|,A bold dashed vertical line that can be used to draw a box.
+
+### Triple
+horizontalLineTripleDash,┄,-,A dashed horizontal line that can be used to draw a box.
+horizontalLineTripleDashBold,┅,-,A bold dashed horizontal line that can be used to draw a box.
+verticalLineTripleDash,┆,|,A dashed vertical line that can be used to draw a box.
+verticalLineTripleDashBold,┇,|,A bold dashed vertical line that can be used to draw a box.
+
+### Quadruple
+horizontalLineQuadrupleDash,┈,-,A dashed horizontal line that can be used to draw a box.
+horizontalLineQuadrupleDashBold,┉,-,A bold dashed horizontal line that can be used to draw a box.
+verticalLineQuadrupleDash,┊,|,A dashed vertical line that can be used to draw a box.
+verticalLineQuadrupleDashBold,┋,|,A bold dashed vertical line that can be used to draw a box.
diff --git a/pkgs/term_glyph/lib/src/generated/ascii_glyph_set.dart b/pkgs/term_glyph/lib/src/generated/ascii_glyph_set.dart
new file mode 100644
index 0000000..08534a0
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/ascii_glyph_set.dart
@@ -0,0 +1,139 @@
+// Copyright (c) 2018, 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.
+
+// Don't modify this file by hand! It's generated by tool/generate.dart.
+
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'glyph_set.dart';
+
+/// A [GlyphSet] that includes only ASCII glyphs.
+class AsciiGlyphSet implements GlyphSet {
+  const AsciiGlyphSet();
+
+  /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+  /// otherwise.
+  @override
+  String glyphOrAscii(String glyph, String alternative) => alternative;
+  @override
+  String get bullet => '*';
+  @override
+  String get leftArrow => '<';
+  @override
+  String get rightArrow => '>';
+  @override
+  String get upArrow => '^';
+  @override
+  String get downArrow => 'v';
+  @override
+  String get longLeftArrow => '<=';
+  @override
+  String get longRightArrow => '=>';
+  @override
+  String get horizontalLine => '-';
+  @override
+  String get verticalLine => '|';
+  @override
+  String get topLeftCorner => ',';
+  @override
+  String get topRightCorner => ',';
+  @override
+  String get bottomLeftCorner => "'";
+  @override
+  String get bottomRightCorner => "'";
+  @override
+  String get cross => '+';
+  @override
+  String get teeUp => '+';
+  @override
+  String get teeDown => '+';
+  @override
+  String get teeLeft => '+';
+  @override
+  String get teeRight => '+';
+  @override
+  String get upEnd => "'";
+  @override
+  String get downEnd => ',';
+  @override
+  String get leftEnd => '-';
+  @override
+  String get rightEnd => '-';
+  @override
+  String get horizontalLineBold => '=';
+  @override
+  String get verticalLineBold => '|';
+  @override
+  String get topLeftCornerBold => ',';
+  @override
+  String get topRightCornerBold => ',';
+  @override
+  String get bottomLeftCornerBold => "'";
+  @override
+  String get bottomRightCornerBold => "'";
+  @override
+  String get crossBold => '+';
+  @override
+  String get teeUpBold => '+';
+  @override
+  String get teeDownBold => '+';
+  @override
+  String get teeLeftBold => '+';
+  @override
+  String get teeRightBold => '+';
+  @override
+  String get upEndBold => "'";
+  @override
+  String get downEndBold => ',';
+  @override
+  String get leftEndBold => '-';
+  @override
+  String get rightEndBold => '-';
+  @override
+  String get horizontalLineDouble => '=';
+  @override
+  String get verticalLineDouble => '|';
+  @override
+  String get topLeftCornerDouble => ',';
+  @override
+  String get topRightCornerDouble => ',';
+  @override
+  String get bottomLeftCornerDouble => '"';
+  @override
+  String get bottomRightCornerDouble => '"';
+  @override
+  String get crossDouble => '+';
+  @override
+  String get teeUpDouble => '+';
+  @override
+  String get teeDownDouble => '+';
+  @override
+  String get teeLeftDouble => '+';
+  @override
+  String get teeRightDouble => '+';
+  @override
+  String get horizontalLineDoubleDash => '-';
+  @override
+  String get horizontalLineDoubleDashBold => '-';
+  @override
+  String get verticalLineDoubleDash => '|';
+  @override
+  String get verticalLineDoubleDashBold => '|';
+  @override
+  String get horizontalLineTripleDash => '-';
+  @override
+  String get horizontalLineTripleDashBold => '-';
+  @override
+  String get verticalLineTripleDash => '|';
+  @override
+  String get verticalLineTripleDashBold => '|';
+  @override
+  String get horizontalLineQuadrupleDash => '-';
+  @override
+  String get horizontalLineQuadrupleDashBold => '-';
+  @override
+  String get verticalLineQuadrupleDash => '|';
+  @override
+  String get verticalLineQuadrupleDashBold => '|';
+}
diff --git a/pkgs/term_glyph/lib/src/generated/glyph_set.dart b/pkgs/term_glyph/lib/src/generated/glyph_set.dart
new file mode 100644
index 0000000..c8cc4a9
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/glyph_set.dart
@@ -0,0 +1,222 @@
+// Copyright (c) 2018, 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.
+
+// Don't modify this file by hand! It's generated by tool/generate.dart.
+
+// ignore_for_file: lines_longer_than_80_chars
+
+/// A class that provides access to every configurable glyph.
+///
+/// This is provided as a class so that individual chunks of code can choose
+/// between `ascii` and `unicode` glyphs. For example:
+///
+/// ```dart
+/// import 'package:term_glyph/term_glyph.dart' as glyph;
+///
+/// /// Adds a vertical line to the left of [text].
+/// ///
+/// /// If [unicode] is `true`, this uses Unicode for the line. If it's
+/// /// `false`, this uses plain ASCII characters. If it's `null`, it
+/// /// defaults to [glyph.ascii].
+/// void addVerticalLine(String text, {bool unicode}) {
+///   var glyphs =
+///       (unicode ?? !glyph.ascii) ? glyph.unicodeGlyphs : glyph.asciiGlyphs;
+///
+///   return text
+///       .split('\n')
+///       .map((line) => '${glyphs.verticalLine} $line')
+///       .join('\n');
+/// }
+/// ```
+abstract class GlyphSet {
+  /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+  /// otherwise.
+  String glyphOrAscii(String glyph, String alternative);
+
+  /// A bullet point.
+  String get bullet;
+
+  /// A left-pointing arrow.
+  ///
+  /// Note that the Unicode arrow glyphs may overlap with adjacent characters in some
+  /// terminal fonts, and should generally be surrounding by spaces.
+  String get leftArrow;
+
+  /// A right-pointing arrow.
+  ///
+  /// Note that the Unicode arrow glyphs may overlap with adjacent characters in some
+  /// terminal fonts, and should generally be surrounding by spaces.
+  String get rightArrow;
+
+  /// An upwards-pointing arrow.
+  String get upArrow;
+
+  /// A downwards-pointing arrow.
+  String get downArrow;
+
+  /// A two-character left-pointing arrow.
+  String get longLeftArrow;
+
+  /// A two-character right-pointing arrow.
+  String get longRightArrow;
+
+  /// A horizontal line that can be used to draw a box.
+  String get horizontalLine;
+
+  /// A vertical line that can be used to draw a box.
+  String get verticalLine;
+
+  /// The upper left-hand corner of a box.
+  String get topLeftCorner;
+
+  /// The upper right-hand corner of a box.
+  String get topRightCorner;
+
+  /// The lower left-hand corner of a box.
+  String get bottomLeftCorner;
+
+  /// The lower right-hand corner of a box.
+  String get bottomRightCorner;
+
+  /// An intersection of vertical and horizontal box lines.
+  String get cross;
+
+  /// A horizontal box line with a vertical line going up from the middle.
+  String get teeUp;
+
+  /// A horizontal box line with a vertical line going down from the middle.
+  String get teeDown;
+
+  /// A vertical box line with a horizontal line going left from the middle.
+  String get teeLeft;
+
+  /// A vertical box line with a horizontal line going right from the middle.
+  String get teeRight;
+
+  /// The top half of a vertical box line.
+  String get upEnd;
+
+  /// The bottom half of a vertical box line.
+  String get downEnd;
+
+  /// The left half of a horizontal box line.
+  String get leftEnd;
+
+  /// The right half of a horizontal box line.
+  String get rightEnd;
+
+  /// A bold horizontal line that can be used to draw a box.
+  String get horizontalLineBold;
+
+  /// A bold vertical line that can be used to draw a box.
+  String get verticalLineBold;
+
+  /// The bold upper left-hand corner of a box.
+  String get topLeftCornerBold;
+
+  /// The bold upper right-hand corner of a box.
+  String get topRightCornerBold;
+
+  /// The bold lower left-hand corner of a box.
+  String get bottomLeftCornerBold;
+
+  /// The bold lower right-hand corner of a box.
+  String get bottomRightCornerBold;
+
+  /// An intersection of bold vertical and horizontal box lines.
+  String get crossBold;
+
+  /// A bold horizontal box line with a vertical line going up from the middle.
+  String get teeUpBold;
+
+  /// A bold horizontal box line with a vertical line going down from the middle.
+  String get teeDownBold;
+
+  /// A bold vertical box line with a horizontal line going left from the middle.
+  String get teeLeftBold;
+
+  /// A bold vertical box line with a horizontal line going right from the middle.
+  String get teeRightBold;
+
+  /// The top half of a bold vertical box line.
+  String get upEndBold;
+
+  /// The bottom half of a bold vertical box line.
+  String get downEndBold;
+
+  /// The left half of a bold horizontal box line.
+  String get leftEndBold;
+
+  /// The right half of a bold horizontal box line.
+  String get rightEndBold;
+
+  /// A double horizontal line that can be used to draw a box.
+  String get horizontalLineDouble;
+
+  /// A double vertical line that can be used to draw a box.
+  String get verticalLineDouble;
+
+  /// The double upper left-hand corner of a box.
+  String get topLeftCornerDouble;
+
+  /// The double upper right-hand corner of a box.
+  String get topRightCornerDouble;
+
+  /// The double lower left-hand corner of a box.
+  String get bottomLeftCornerDouble;
+
+  /// The double lower right-hand corner of a box.
+  String get bottomRightCornerDouble;
+
+  /// An intersection of double vertical and horizontal box lines.
+  String get crossDouble;
+
+  /// A double horizontal box line with a vertical line going up from the middle.
+  String get teeUpDouble;
+
+  /// A double horizontal box line with a vertical line going down from the middle.
+  String get teeDownDouble;
+
+  /// A double vertical box line with a horizontal line going left from the middle.
+  String get teeLeftDouble;
+
+  /// A double vertical box line with a horizontal line going right from the middle.
+  String get teeRightDouble;
+
+  /// A dashed horizontal line that can be used to draw a box.
+  String get horizontalLineDoubleDash;
+
+  /// A bold dashed horizontal line that can be used to draw a box.
+  String get horizontalLineDoubleDashBold;
+
+  /// A dashed vertical line that can be used to draw a box.
+  String get verticalLineDoubleDash;
+
+  /// A bold dashed vertical line that can be used to draw a box.
+  String get verticalLineDoubleDashBold;
+
+  /// A dashed horizontal line that can be used to draw a box.
+  String get horizontalLineTripleDash;
+
+  /// A bold dashed horizontal line that can be used to draw a box.
+  String get horizontalLineTripleDashBold;
+
+  /// A dashed vertical line that can be used to draw a box.
+  String get verticalLineTripleDash;
+
+  /// A bold dashed vertical line that can be used to draw a box.
+  String get verticalLineTripleDashBold;
+
+  /// A dashed horizontal line that can be used to draw a box.
+  String get horizontalLineQuadrupleDash;
+
+  /// A bold dashed horizontal line that can be used to draw a box.
+  String get horizontalLineQuadrupleDashBold;
+
+  /// A dashed vertical line that can be used to draw a box.
+  String get verticalLineQuadrupleDash;
+
+  /// A bold dashed vertical line that can be used to draw a box.
+  String get verticalLineQuadrupleDashBold;
+}
diff --git a/pkgs/term_glyph/lib/src/generated/top_level.dart b/pkgs/term_glyph/lib/src/generated/top_level.dart
new file mode 100644
index 0000000..848ef6d
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/top_level.dart
@@ -0,0 +1,382 @@
+// Copyright (c) 2018, 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.
+
+// Don't modify this file by hand! It's generated by tool/generate.dart.
+
+// ignore_for_file: lines_longer_than_80_chars
+
+import '../../term_glyph.dart' as glyph;
+
+/// A bullet point.
+///
+/// If [glyph.ascii] is `false`, this is "•". If it's `true`, this is
+/// "*" instead.
+String get bullet => glyph.glyphs.bullet;
+
+/// A left-pointing arrow.
+///
+/// Note that the Unicode arrow glyphs may overlap with adjacent characters in some
+/// terminal fonts, and should generally be surrounding by spaces.
+///
+/// If [glyph.ascii] is `false`, this is "←". If it's `true`, this is
+/// "<" instead.
+String get leftArrow => glyph.glyphs.leftArrow;
+
+/// A right-pointing arrow.
+///
+/// Note that the Unicode arrow glyphs may overlap with adjacent characters in some
+/// terminal fonts, and should generally be surrounding by spaces.
+///
+/// If [glyph.ascii] is `false`, this is "→". If it's `true`, this is
+/// ">" instead.
+String get rightArrow => glyph.glyphs.rightArrow;
+
+/// An upwards-pointing arrow.
+///
+/// If [glyph.ascii] is `false`, this is "↑". If it's `true`, this is
+/// "^" instead.
+String get upArrow => glyph.glyphs.upArrow;
+
+/// A downwards-pointing arrow.
+///
+/// If [glyph.ascii] is `false`, this is "↓". If it's `true`, this is
+/// "v" instead.
+String get downArrow => glyph.glyphs.downArrow;
+
+/// A two-character left-pointing arrow.
+///
+/// If [glyph.ascii] is `false`, this is "◀━". If it's `true`, this is
+/// "<=" instead.
+String get longLeftArrow => glyph.glyphs.longLeftArrow;
+
+/// A two-character right-pointing arrow.
+///
+/// If [glyph.ascii] is `false`, this is "━▶". If it's `true`, this is
+/// "=>" instead.
+String get longRightArrow => glyph.glyphs.longRightArrow;
+
+/// A horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "─". If it's `true`, this is
+/// "-" instead.
+String get horizontalLine => glyph.glyphs.horizontalLine;
+
+/// A vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "│". If it's `true`, this is
+/// "|" instead.
+String get verticalLine => glyph.glyphs.verticalLine;
+
+/// The upper left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┌". If it's `true`, this is
+/// "," instead.
+String get topLeftCorner => glyph.glyphs.topLeftCorner;
+
+/// The upper right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┐". If it's `true`, this is
+/// "," instead.
+String get topRightCorner => glyph.glyphs.topRightCorner;
+
+/// The lower left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "└". If it's `true`, this is
+/// "'" instead.
+String get bottomLeftCorner => glyph.glyphs.bottomLeftCorner;
+
+/// The lower right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┘". If it's `true`, this is
+/// "'" instead.
+String get bottomRightCorner => glyph.glyphs.bottomRightCorner;
+
+/// An intersection of vertical and horizontal box lines.
+///
+/// If [glyph.ascii] is `false`, this is "┼". If it's `true`, this is
+/// "+" instead.
+String get cross => glyph.glyphs.cross;
+
+/// A horizontal box line with a vertical line going up from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┴". If it's `true`, this is
+/// "+" instead.
+String get teeUp => glyph.glyphs.teeUp;
+
+/// A horizontal box line with a vertical line going down from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┬". If it's `true`, this is
+/// "+" instead.
+String get teeDown => glyph.glyphs.teeDown;
+
+/// A vertical box line with a horizontal line going left from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┤". If it's `true`, this is
+/// "+" instead.
+String get teeLeft => glyph.glyphs.teeLeft;
+
+/// A vertical box line with a horizontal line going right from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "├". If it's `true`, this is
+/// "+" instead.
+String get teeRight => glyph.glyphs.teeRight;
+
+/// The top half of a vertical box line.
+///
+/// If [glyph.ascii] is `false`, this is "╵". If it's `true`, this is
+/// "'" instead.
+String get upEnd => glyph.glyphs.upEnd;
+
+/// The bottom half of a vertical box line.
+///
+/// If [glyph.ascii] is `false`, this is "╷". If it's `true`, this is
+/// "," instead.
+String get downEnd => glyph.glyphs.downEnd;
+
+/// The left half of a horizontal box line.
+///
+/// If [glyph.ascii] is `false`, this is "╴". If it's `true`, this is
+/// "-" instead.
+String get leftEnd => glyph.glyphs.leftEnd;
+
+/// The right half of a horizontal box line.
+///
+/// If [glyph.ascii] is `false`, this is "╶". If it's `true`, this is
+/// "-" instead.
+String get rightEnd => glyph.glyphs.rightEnd;
+
+/// A bold horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "━". If it's `true`, this is
+/// "=" instead.
+String get horizontalLineBold => glyph.glyphs.horizontalLineBold;
+
+/// A bold vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┃". If it's `true`, this is
+/// "|" instead.
+String get verticalLineBold => glyph.glyphs.verticalLineBold;
+
+/// The bold upper left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┏". If it's `true`, this is
+/// "," instead.
+String get topLeftCornerBold => glyph.glyphs.topLeftCornerBold;
+
+/// The bold upper right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┓". If it's `true`, this is
+/// "," instead.
+String get topRightCornerBold => glyph.glyphs.topRightCornerBold;
+
+/// The bold lower left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┗". If it's `true`, this is
+/// "'" instead.
+String get bottomLeftCornerBold => glyph.glyphs.bottomLeftCornerBold;
+
+/// The bold lower right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┛". If it's `true`, this is
+/// "'" instead.
+String get bottomRightCornerBold => glyph.glyphs.bottomRightCornerBold;
+
+/// An intersection of bold vertical and horizontal box lines.
+///
+/// If [glyph.ascii] is `false`, this is "╋". If it's `true`, this is
+/// "+" instead.
+String get crossBold => glyph.glyphs.crossBold;
+
+/// A bold horizontal box line with a vertical line going up from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┻". If it's `true`, this is
+/// "+" instead.
+String get teeUpBold => glyph.glyphs.teeUpBold;
+
+/// A bold horizontal box line with a vertical line going down from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┳". If it's `true`, this is
+/// "+" instead.
+String get teeDownBold => glyph.glyphs.teeDownBold;
+
+/// A bold vertical box line with a horizontal line going left from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┫". If it's `true`, this is
+/// "+" instead.
+String get teeLeftBold => glyph.glyphs.teeLeftBold;
+
+/// A bold vertical box line with a horizontal line going right from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┣". If it's `true`, this is
+/// "+" instead.
+String get teeRightBold => glyph.glyphs.teeRightBold;
+
+/// The top half of a bold vertical box line.
+///
+/// If [glyph.ascii] is `false`, this is "╹". If it's `true`, this is
+/// "'" instead.
+String get upEndBold => glyph.glyphs.upEndBold;
+
+/// The bottom half of a bold vertical box line.
+///
+/// If [glyph.ascii] is `false`, this is "╻". If it's `true`, this is
+/// "," instead.
+String get downEndBold => glyph.glyphs.downEndBold;
+
+/// The left half of a bold horizontal box line.
+///
+/// If [glyph.ascii] is `false`, this is "╸". If it's `true`, this is
+/// "-" instead.
+String get leftEndBold => glyph.glyphs.leftEndBold;
+
+/// The right half of a bold horizontal box line.
+///
+/// If [glyph.ascii] is `false`, this is "╺". If it's `true`, this is
+/// "-" instead.
+String get rightEndBold => glyph.glyphs.rightEndBold;
+
+/// A double horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "═". If it's `true`, this is
+/// "=" instead.
+String get horizontalLineDouble => glyph.glyphs.horizontalLineDouble;
+
+/// A double vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "║". If it's `true`, this is
+/// "|" instead.
+String get verticalLineDouble => glyph.glyphs.verticalLineDouble;
+
+/// The double upper left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "╔". If it's `true`, this is
+/// "," instead.
+String get topLeftCornerDouble => glyph.glyphs.topLeftCornerDouble;
+
+/// The double upper right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "╗". If it's `true`, this is
+/// "," instead.
+String get topRightCornerDouble => glyph.glyphs.topRightCornerDouble;
+
+/// The double lower left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "╚". If it's `true`, this is
+/// """ instead.
+String get bottomLeftCornerDouble => glyph.glyphs.bottomLeftCornerDouble;
+
+/// The double lower right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "╝". If it's `true`, this is
+/// """ instead.
+String get bottomRightCornerDouble => glyph.glyphs.bottomRightCornerDouble;
+
+/// An intersection of double vertical and horizontal box lines.
+///
+/// If [glyph.ascii] is `false`, this is "╬". If it's `true`, this is
+/// "+" instead.
+String get crossDouble => glyph.glyphs.crossDouble;
+
+/// A double horizontal box line with a vertical line going up from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "╩". If it's `true`, this is
+/// "+" instead.
+String get teeUpDouble => glyph.glyphs.teeUpDouble;
+
+/// A double horizontal box line with a vertical line going down from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "╦". If it's `true`, this is
+/// "+" instead.
+String get teeDownDouble => glyph.glyphs.teeDownDouble;
+
+/// A double vertical box line with a horizontal line going left from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "╣". If it's `true`, this is
+/// "+" instead.
+String get teeLeftDouble => glyph.glyphs.teeLeftDouble;
+
+/// A double vertical box line with a horizontal line going right from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "╠". If it's `true`, this is
+/// "+" instead.
+String get teeRightDouble => glyph.glyphs.teeRightDouble;
+
+/// A dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "╌". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineDoubleDash => glyph.glyphs.horizontalLineDoubleDash;
+
+/// A bold dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "╍". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineDoubleDashBold =>
+    glyph.glyphs.horizontalLineDoubleDashBold;
+
+/// A dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "╎". If it's `true`, this is
+/// "|" instead.
+String get verticalLineDoubleDash => glyph.glyphs.verticalLineDoubleDash;
+
+/// A bold dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "╏". If it's `true`, this is
+/// "|" instead.
+String get verticalLineDoubleDashBold =>
+    glyph.glyphs.verticalLineDoubleDashBold;
+
+/// A dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┄". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineTripleDash => glyph.glyphs.horizontalLineTripleDash;
+
+/// A bold dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┅". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineTripleDashBold =>
+    glyph.glyphs.horizontalLineTripleDashBold;
+
+/// A dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┆". If it's `true`, this is
+/// "|" instead.
+String get verticalLineTripleDash => glyph.glyphs.verticalLineTripleDash;
+
+/// A bold dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┇". If it's `true`, this is
+/// "|" instead.
+String get verticalLineTripleDashBold =>
+    glyph.glyphs.verticalLineTripleDashBold;
+
+/// A dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┈". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineQuadrupleDash =>
+    glyph.glyphs.horizontalLineQuadrupleDash;
+
+/// A bold dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┉". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineQuadrupleDashBold =>
+    glyph.glyphs.horizontalLineQuadrupleDashBold;
+
+/// A dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┊". If it's `true`, this is
+/// "|" instead.
+String get verticalLineQuadrupleDash => glyph.glyphs.verticalLineQuadrupleDash;
+
+/// A bold dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┋". If it's `true`, this is
+/// "|" instead.
+String get verticalLineQuadrupleDashBold =>
+    glyph.glyphs.verticalLineQuadrupleDashBold;
diff --git a/pkgs/term_glyph/lib/src/generated/unicode_glyph_set.dart b/pkgs/term_glyph/lib/src/generated/unicode_glyph_set.dart
new file mode 100644
index 0000000..7264e6d
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/unicode_glyph_set.dart
@@ -0,0 +1,139 @@
+// Copyright (c) 2018, 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.
+
+// Don't modify this file by hand! It's generated by tool/generate.dart.
+
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'glyph_set.dart';
+
+/// A [GlyphSet] that includes only Unicode glyphs.
+class UnicodeGlyphSet implements GlyphSet {
+  const UnicodeGlyphSet();
+
+  /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+  /// otherwise.
+  @override
+  String glyphOrAscii(String glyph, String alternative) => glyph;
+  @override
+  String get bullet => '•';
+  @override
+  String get leftArrow => '←';
+  @override
+  String get rightArrow => '→';
+  @override
+  String get upArrow => '↑';
+  @override
+  String get downArrow => '↓';
+  @override
+  String get longLeftArrow => '◀━';
+  @override
+  String get longRightArrow => '━▶';
+  @override
+  String get horizontalLine => '─';
+  @override
+  String get verticalLine => '│';
+  @override
+  String get topLeftCorner => '┌';
+  @override
+  String get topRightCorner => '┐';
+  @override
+  String get bottomLeftCorner => '└';
+  @override
+  String get bottomRightCorner => '┘';
+  @override
+  String get cross => '┼';
+  @override
+  String get teeUp => '┴';
+  @override
+  String get teeDown => '┬';
+  @override
+  String get teeLeft => '┤';
+  @override
+  String get teeRight => '├';
+  @override
+  String get upEnd => '╵';
+  @override
+  String get downEnd => '╷';
+  @override
+  String get leftEnd => '╴';
+  @override
+  String get rightEnd => '╶';
+  @override
+  String get horizontalLineBold => '━';
+  @override
+  String get verticalLineBold => '┃';
+  @override
+  String get topLeftCornerBold => '┏';
+  @override
+  String get topRightCornerBold => '┓';
+  @override
+  String get bottomLeftCornerBold => '┗';
+  @override
+  String get bottomRightCornerBold => '┛';
+  @override
+  String get crossBold => '╋';
+  @override
+  String get teeUpBold => '┻';
+  @override
+  String get teeDownBold => '┳';
+  @override
+  String get teeLeftBold => '┫';
+  @override
+  String get teeRightBold => '┣';
+  @override
+  String get upEndBold => '╹';
+  @override
+  String get downEndBold => '╻';
+  @override
+  String get leftEndBold => '╸';
+  @override
+  String get rightEndBold => '╺';
+  @override
+  String get horizontalLineDouble => '═';
+  @override
+  String get verticalLineDouble => '║';
+  @override
+  String get topLeftCornerDouble => '╔';
+  @override
+  String get topRightCornerDouble => '╗';
+  @override
+  String get bottomLeftCornerDouble => '╚';
+  @override
+  String get bottomRightCornerDouble => '╝';
+  @override
+  String get crossDouble => '╬';
+  @override
+  String get teeUpDouble => '╩';
+  @override
+  String get teeDownDouble => '╦';
+  @override
+  String get teeLeftDouble => '╣';
+  @override
+  String get teeRightDouble => '╠';
+  @override
+  String get horizontalLineDoubleDash => '╌';
+  @override
+  String get horizontalLineDoubleDashBold => '╍';
+  @override
+  String get verticalLineDoubleDash => '╎';
+  @override
+  String get verticalLineDoubleDashBold => '╏';
+  @override
+  String get horizontalLineTripleDash => '┄';
+  @override
+  String get horizontalLineTripleDashBold => '┅';
+  @override
+  String get verticalLineTripleDash => '┆';
+  @override
+  String get verticalLineTripleDashBold => '┇';
+  @override
+  String get horizontalLineQuadrupleDash => '┈';
+  @override
+  String get horizontalLineQuadrupleDashBold => '┉';
+  @override
+  String get verticalLineQuadrupleDash => '┊';
+  @override
+  String get verticalLineQuadrupleDashBold => '┋';
+}
diff --git a/pkgs/term_glyph/lib/term_glyph.dart b/pkgs/term_glyph/lib/term_glyph.dart
new file mode 100644
index 0000000..9f2b422
--- /dev/null
+++ b/pkgs/term_glyph/lib/term_glyph.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2017, 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 'src/generated/ascii_glyph_set.dart';
+import 'src/generated/glyph_set.dart';
+import 'src/generated/unicode_glyph_set.dart';
+
+export 'src/generated/glyph_set.dart';
+export 'src/generated/top_level.dart';
+
+/// A [GlyphSet] that always returns ASCII glyphs.
+const GlyphSet asciiGlyphs = AsciiGlyphSet();
+
+/// A [GlyphSet] that always returns Unicode glyphs.
+const GlyphSet unicodeGlyphs = UnicodeGlyphSet();
+
+/// Returns [asciiGlyphs] if [ascii] is `true` or [unicodeGlyphs] otherwise.
+///
+/// Returns [unicodeGlyphs] by default.
+GlyphSet get glyphs => _glyphs;
+GlyphSet _glyphs = unicodeGlyphs;
+
+/// Whether the glyph getters return plain ASCII, as opposed to Unicode
+/// characters or sequences.
+///
+/// Defaults to `false`.
+bool get ascii => glyphs == asciiGlyphs;
+
+set ascii(bool value) {
+  _glyphs = value ? asciiGlyphs : unicodeGlyphs;
+}
+
+/// Returns [glyph] if Unicode glyph are allowed, and [alternative] if they
+/// aren't.
+String glyphOrAscii(String glyph, String alternative) =>
+    glyphs.glyphOrAscii(glyph, alternative);
diff --git a/pkgs/term_glyph/pubspec.yaml b/pkgs/term_glyph/pubspec.yaml
new file mode 100644
index 0000000..bac16f1
--- /dev/null
+++ b/pkgs/term_glyph/pubspec.yaml
@@ -0,0 +1,12 @@
+name: term_glyph
+version: 1.2.3-wip
+description: Useful Unicode glyphs and ASCII substitutes.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/term_glyph
+
+environment:
+  sdk: ^3.1.0
+
+dev_dependencies:
+  csv: ^6.0.0
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.6
diff --git a/pkgs/term_glyph/test/symbol_test.dart b/pkgs/term_glyph/test/symbol_test.dart
new file mode 100644
index 0000000..b3b4d09
--- /dev/null
+++ b/pkgs/term_glyph/test/symbol_test.dart
@@ -0,0 +1,60 @@
+// Copyright (c) 2017, 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:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+
+void main() {
+  group('with ascii = false', () {
+    setUpAll(() {
+      glyph.ascii = false;
+    });
+
+    test('glyph getters return Unicode versions', () {
+      expect(glyph.topLeftCorner, equals('┌'));
+      expect(glyph.teeUpBold, equals('┻'));
+      expect(glyph.longLeftArrow, equals('◀━'));
+    });
+
+    test('glyphOrAscii returns the first argument', () {
+      expect(glyph.glyphOrAscii('A', 'B'), equals('A'));
+    });
+
+    test('glyphs returns unicodeGlyphs', () {
+      expect(glyph.glyphs, equals(glyph.unicodeGlyphs));
+    });
+
+    test('asciiGlyphs still returns ASCII characters', () {
+      expect(glyph.asciiGlyphs.topLeftCorner, equals(','));
+      expect(glyph.asciiGlyphs.teeUpBold, equals('+'));
+      expect(glyph.asciiGlyphs.longLeftArrow, equals('<='));
+    });
+  });
+
+  group('with ascii = true', () {
+    setUpAll(() {
+      glyph.ascii = true;
+    });
+
+    test('glyphs return ASCII versions', () {
+      expect(glyph.topLeftCorner, equals(','));
+      expect(glyph.teeUpBold, equals('+'));
+      expect(glyph.longLeftArrow, equals('<='));
+    });
+
+    test('glyphOrAscii returns the second argument', () {
+      expect(glyph.glyphOrAscii('A', 'B'), equals('B'));
+    });
+
+    test('glyphs returns asciiGlyphs', () {
+      expect(glyph.glyphs, equals(glyph.asciiGlyphs));
+    });
+
+    test('unicodeGlyphs still returns Unicode characters', () {
+      expect(glyph.unicodeGlyphs.topLeftCorner, equals('┌'));
+      expect(glyph.unicodeGlyphs.teeUpBold, equals('┻'));
+      expect(glyph.unicodeGlyphs.longLeftArrow, equals('◀━'));
+    });
+  });
+}
diff --git a/pkgs/term_glyph/tool/generate.dart b/pkgs/term_glyph/tool/generate.dart
new file mode 100644
index 0000000..b96b7bd
--- /dev/null
+++ b/pkgs/term_glyph/tool/generate.dart
@@ -0,0 +1,156 @@
+// Copyright (c) 2017, 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:csv/csv.dart';
+
+void main() {
+  final csv = CsvCodec(eol: '\n');
+  final data = csv.decoder.convert(File('data.csv').readAsStringSync());
+
+  // Remove comments and empty lines.
+  data.removeWhere((row) => row.length < 3);
+
+  Directory(_generatedDir).createSync(recursive: true);
+
+  _writeGlyphSetInterface(data);
+  _writeGlyphSet(data, ascii: false);
+  _writeGlyphSet(data, ascii: true);
+  _writeTopLevel(data);
+
+  final result = Process.runSync(Platform.resolvedExecutable, [
+    'format',
+    _generatedDir,
+  ]);
+  print(result.stderr);
+  exit(result.exitCode);
+}
+
+const _generatedDir = 'lib/src/generated';
+
+/// Writes `lib/src/generated/glyph_set.dart`.
+void _writeGlyphSetInterface(List<List<dynamic>> data) {
+  final file =
+      File('$_generatedDir/glyph_set.dart').openSync(mode: FileMode.write);
+  file.writeStringSync(_header);
+  file.writeStringSync(r'''
+
+    /// A class that provides access to every configurable glyph.
+    ///
+    /// This is provided as a class so that individual chunks of code can choose
+    /// between `ascii` and `unicode` glyphs. For example:
+    ///
+    /// ```dart
+    /// import 'package:term_glyph/term_glyph.dart' as glyph;
+    ///
+    /// /// Adds a vertical line to the left of [text].
+    /// ///
+    /// /// If [unicode] is `true`, this uses Unicode for the line. If it's
+    /// /// `false`, this uses plain ASCII characters. If it's `null`, it
+    /// /// defaults to [glyph.ascii].
+    /// void addVerticalLine(String text, {bool unicode}) {
+    ///   var glyphs =
+    ///       (unicode ?? !glyph.ascii) ? glyph.unicodeGlyphs : glyph.asciiGlyphs;
+    ///
+    ///   return text
+    ///       .split('\n')
+    ///       .map((line) => '${glyphs.verticalLine} $line')
+    ///       .join('\n');
+    /// }
+    /// ```
+    abstract class GlyphSet {
+      /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+      /// otherwise.
+      String glyphOrAscii(String glyph, String alternative);
+  ''');
+
+  for (var glyph in data) {
+    for (var line in (glyph[3] as String).split('\n')) {
+      file.writeStringSync('/// $line\n');
+    }
+
+    file.writeStringSync('String get ${glyph[0]};');
+  }
+
+  file.writeStringSync('}');
+  file.closeSync();
+}
+
+/// Writes `lib/src/generated/${prefix.toLowerCase()}_glyph_set.dart`.
+///
+/// If [ascii] is `true`, this writes the ASCII glyph set. Otherwise it writes
+/// the Unicode glyph set.
+void _writeGlyphSet(List<List<dynamic>> data, {required bool ascii}) {
+  final file =
+      File('$_generatedDir/${ascii ? "ascii" : "unicode"}_glyph_set.dart')
+          .openSync(mode: FileMode.write);
+
+  final className = '${ascii ? "Ascii" : "Unicode"}GlyphSet';
+  file.writeStringSync('''
+    $_header
+
+
+    import 'glyph_set.dart';
+
+    /// A [GlyphSet] that includes only ${ascii ? "ASCII" : "Unicode"} glyphs.
+    class $className implements GlyphSet {
+      const $className();
+      /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+      /// otherwise.
+      @override
+      String glyphOrAscii(String glyph, String alternative) =>
+          ${ascii ? "alternative" : "glyph"};
+  ''');
+
+  final index = ascii ? 2 : 1;
+  for (var glyph in data) {
+    file.writeStringSync('''
+      @override
+      String get ${glyph[0]} => ${_quote(glyph[index] as String)};
+    ''');
+  }
+
+  file.writeStringSync('}');
+  file.closeSync();
+}
+
+/// Writes `lib/src/generated/top_level.dart`.
+void _writeTopLevel(List<List<dynamic>> data) {
+  final file =
+      File('$_generatedDir/top_level.dart').openSync(mode: FileMode.write);
+
+  file.writeStringSync('''
+    $_header
+
+    import '../../term_glyph.dart' as glyph;
+  ''');
+
+  for (var glyph in data) {
+    for (var line in (glyph[3] as String).split('\n')) {
+      file.writeStringSync('/// $line\n');
+    }
+
+    file.writeStringSync('''
+      ///
+      /// If [glyph.ascii] is `false`, this is "${glyph[1]}". If it's `true`, this is
+      /// "${glyph[2]}" instead.
+      String get ${glyph[0]} => glyph.glyphs.${glyph[0]};
+    ''');
+  }
+
+  file.closeSync();
+}
+
+String _quote(String input) => input.contains("'") ? '"$input"' : "'$input'";
+
+const _header = '''
+// Copyright (c) 2018, 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.
+
+// Don't modify this file by hand! It's generated by tool/generate.dart.
+
+// ignore_for_file: lines_longer_than_80_chars
+''';
diff --git a/pkgs/test_reflective_loader/.gitignore b/pkgs/test_reflective_loader/.gitignore
new file mode 100644
index 0000000..2a2c261
--- /dev/null
+++ b/pkgs/test_reflective_loader/.gitignore
@@ -0,0 +1,11 @@
+.buildlog
+.DS_Store
+.idea
+.dart_tool/
+.pub/
+.project
+.settings/
+build/
+packages
+.packages
+pubspec.lock
diff --git a/pkgs/test_reflective_loader/AUTHORS b/pkgs/test_reflective_loader/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/test_reflective_loader/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/test_reflective_loader/CHANGELOG.md b/pkgs/test_reflective_loader/CHANGELOG.md
new file mode 100644
index 0000000..803eb0e
--- /dev/null
+++ b/pkgs/test_reflective_loader/CHANGELOG.md
@@ -0,0 +1,72 @@
+## 0.2.3
+
+- Require Dart `^3.1.0`.
+- Move to `dart-lang/tools` monorepo.
+
+## 0.2.2
+
+- Update to package:lints 2.0.0 and move it to a dev dependency.
+
+## 0.2.1
+
+- Use package:lints for analysis.
+- Populate the pubspec `repository` field.
+
+## 0.2.0
+
+- Stable null safety release.
+
+## 0.2.0-nullsafety.0
+
+- Migrate to the null safety language feature.
+
+## 0.1.9
+
+- Add `@SkippedTest` annotation and `skip_test` prefix.
+
+## 0.1.8
+
+- Update `FailingTest` to add named parameters `issue` and `reason`.
+
+## 0.1.7
+
+- Update documentation comments.
+- Remove `@MirrorsUsed` annotation on `dart:mirrors`.
+
+## 0.1.6
+
+- Make `FailingTest` public, with the URI of the issue that causes
+  the test to break.
+
+## 0.1.5
+
+- Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 0.1.3
+
+- Fix `@failingTest` to fail when the test passes.
+
+## 0.1.2
+
+- Update the pubspec `dependencies` section to include `package:test`
+
+## 0.1.1
+
+- For `@failingTest` tests, properly handle when the test fails by throwing an
+  exception in a timer task
+- Analyze this package in strong mode
+
+## 0.1.0
+
+- Switched from 'package:unittest' to 'package:test'.
+- Since 'package:test' does not define 'solo_test', in order to keep this
+  functionality, `defineReflectiveSuite` must be used to wrap all
+  `defineReflectiveTests` invocations.
+
+## 0.0.4
+
+- Added @failingTest, @assertFailingTest and @soloTest annotations.
+
+## 0.0.1
+
+- Initial version
diff --git a/pkgs/test_reflective_loader/LICENSE b/pkgs/test_reflective_loader/LICENSE
new file mode 100644
index 0000000..633672a
--- /dev/null
+++ b/pkgs/test_reflective_loader/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2015, 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/test_reflective_loader/README.md b/pkgs/test_reflective_loader/README.md
new file mode 100644
index 0000000..9b5a83d
--- /dev/null
+++ b/pkgs/test_reflective_loader/README.md
@@ -0,0 +1,28 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/test_reflective_loader.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/test_reflective_loader.yaml)
+[![pub package](https://img.shields.io/pub/v/test_reflective_loader.svg)](https://pub.dev/packages/test_reflective_loader)
+[![package publisher](https://img.shields.io/pub/publisher/test_reflective_loader.svg)](https://pub.dev/packages/test_reflective_loader/publisher)
+
+Support for discovering tests and test suites using reflection.
+
+This package follows the xUnit style where each class is a test suite, and each
+method with the name prefix `test_` is a single test.
+
+Methods with names starting with `test_` are run using the `test()` function with
+the corresponding name. If the class defines methods `setUp()` or `tearDown()`,
+they are executed before / after each test correspondingly, even if the test fails.
+
+Methods with names starting with `solo_test_` are run using the `solo_test()` function.
+
+Methods with names starting with `fail_` are expected to fail.
+
+Methods with names starting with `solo_fail_` are run using the `solo_test()` function
+and expected to fail.
+
+Method returning `Future` class instances are asynchronous, so `tearDown()` is
+executed after the returned `Future` completes.
+
+## Features and bugs
+
+Please file feature requests and bugs at the [issue tracker][tracker].
+
+[tracker]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atest_reflective_loader
diff --git a/pkgs/test_reflective_loader/analysis_options.yaml b/pkgs/test_reflective_loader/analysis_options.yaml
new file mode 100644
index 0000000..ea61158
--- /dev/null
+++ b/pkgs/test_reflective_loader/analysis_options.yaml
@@ -0,0 +1,5 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+linter:
+  rules:
+    - public_member_api_docs
diff --git a/pkgs/test_reflective_loader/lib/test_reflective_loader.dart b/pkgs/test_reflective_loader/lib/test_reflective_loader.dart
new file mode 100644
index 0000000..cb69bf3
--- /dev/null
+++ b/pkgs/test_reflective_loader/lib/test_reflective_loader.dart
@@ -0,0 +1,354 @@
+// 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 'dart:async';
+import 'dart:mirrors';
+
+import 'package:test/test.dart' as test_package;
+
+/// A marker annotation used to annotate test methods which are expected to fail
+/// when asserts are enabled.
+const Object assertFailingTest = _AssertFailingTest();
+
+/// A marker annotation used to annotate test methods which are expected to
+/// fail.
+const Object failingTest = FailingTest();
+
+/// A marker annotation used to instruct dart2js to keep reflection information
+/// for the annotated classes.
+const Object reflectiveTest = _ReflectiveTest();
+
+/// A marker annotation used to annotate test methods that should be skipped.
+const Object skippedTest = SkippedTest();
+
+/// A marker annotation used to annotate "solo" groups and tests.
+const Object soloTest = _SoloTest();
+
+final List<_Group> _currentGroups = <_Group>[];
+int _currentSuiteLevel = 0;
+String _currentSuiteName = '';
+
+/// Is `true` the application is running in the checked mode.
+final bool _isCheckedMode = () {
+  try {
+    assert(false);
+    return false;
+  } catch (_) {
+    return true;
+  }
+}();
+
+/// Run the [define] function parameter that calls [defineReflectiveTests] to
+/// add normal and "solo" tests, and also calls [defineReflectiveSuite] to
+/// create embedded suites.  If the current suite is the top-level one, perform
+/// check for "solo" groups and tests, and run all or only "solo" items.
+void defineReflectiveSuite(void Function() define, {String name = ''}) {
+  var groupName = _currentSuiteName;
+  _currentSuiteLevel++;
+  try {
+    _currentSuiteName = _combineNames(_currentSuiteName, name);
+    define();
+  } finally {
+    _currentSuiteName = groupName;
+    _currentSuiteLevel--;
+  }
+  _addTestsIfTopLevelSuite();
+}
+
+/// Runs test methods existing in the given [type].
+///
+/// If there is a "solo" test method in the top-level suite, only "solo" methods
+/// are run.
+///
+/// If there is a "solo" test type, only its test methods are run.
+///
+/// Otherwise all tests methods of all test types are run.
+///
+/// Each method is run with a new instance of [type].
+/// So, [type] should have a default constructor.
+///
+/// If [type] declares method `setUp`, it methods will be invoked before any
+/// test method invocation.
+///
+/// If [type] declares method `tearDown`, it will be invoked after any test
+/// method invocation. If method returns [Future] to test some asynchronous
+/// behavior, then `tearDown` will be invoked in `Future.complete`.
+void defineReflectiveTests(Type type) {
+  var classMirror = reflectClass(type);
+  if (!classMirror.metadata.any((InstanceMirror annotation) =>
+      annotation.type.reflectedType == _ReflectiveTest)) {
+    var name = MirrorSystem.getName(classMirror.qualifiedName);
+    throw Exception('Class $name must have annotation "@reflectiveTest" '
+        'in order to be run by runReflectiveTests.');
+  }
+
+  _Group group;
+  {
+    var isSolo = _hasAnnotationInstance(classMirror, soloTest);
+    var className = MirrorSystem.getName(classMirror.simpleName);
+    group = _Group(isSolo, _combineNames(_currentSuiteName, className));
+    _currentGroups.add(group);
+  }
+
+  classMirror.instanceMembers
+      .forEach((Symbol symbol, MethodMirror memberMirror) {
+    // we need only methods
+    if (!memberMirror.isRegularMethod) {
+      return;
+    }
+    // prepare information about the method
+    var memberName = MirrorSystem.getName(symbol);
+    var isSolo = memberName.startsWith('solo_') ||
+        _hasAnnotationInstance(memberMirror, soloTest);
+    // test_
+    if (memberName.startsWith('test_')) {
+      if (_hasSkippedTestAnnotation(memberMirror)) {
+        group.addSkippedTest(memberName);
+      } else {
+        group.addTest(isSolo, memberName, memberMirror, () {
+          if (_hasFailingTestAnnotation(memberMirror) ||
+              _isCheckedMode && _hasAssertFailingTestAnnotation(memberMirror)) {
+            return _runFailingTest(classMirror, symbol);
+          } else {
+            return _runTest(classMirror, symbol);
+          }
+        });
+      }
+      return;
+    }
+    // solo_test_
+    if (memberName.startsWith('solo_test_')) {
+      group.addTest(true, memberName, memberMirror, () {
+        return _runTest(classMirror, symbol);
+      });
+    }
+    // fail_test_
+    if (memberName.startsWith('fail_')) {
+      group.addTest(isSolo, memberName, memberMirror, () {
+        return _runFailingTest(classMirror, symbol);
+      });
+    }
+    // solo_fail_test_
+    if (memberName.startsWith('solo_fail_')) {
+      group.addTest(true, memberName, memberMirror, () {
+        return _runFailingTest(classMirror, symbol);
+      });
+    }
+    // skip_test_
+    if (memberName.startsWith('skip_test_')) {
+      group.addSkippedTest(memberName);
+    }
+  });
+
+  // Support for the case of missing enclosing [defineReflectiveSuite].
+  _addTestsIfTopLevelSuite();
+}
+
+/// If the current suite is the top-level one, add tests to the `test` package.
+void _addTestsIfTopLevelSuite() {
+  if (_currentSuiteLevel == 0) {
+    void runTests({required bool allGroups, required bool allTests}) {
+      for (var group in _currentGroups) {
+        if (allGroups || group.isSolo) {
+          for (var test in group.tests) {
+            if (allTests || test.isSolo) {
+              test_package.test(test.name, test.function,
+                  timeout: test.timeout, skip: test.isSkipped);
+            }
+          }
+        }
+      }
+    }
+
+    if (_currentGroups.any((g) => g.hasSoloTest)) {
+      runTests(allGroups: true, allTests: false);
+    } else if (_currentGroups.any((g) => g.isSolo)) {
+      runTests(allGroups: false, allTests: true);
+    } else {
+      runTests(allGroups: true, allTests: true);
+    }
+    _currentGroups.clear();
+  }
+}
+
+/// Return the combination of the [base] and [addition] names.
+/// If any other two is `null`, then the other one is returned.
+String _combineNames(String base, String addition) {
+  if (base.isEmpty) {
+    return addition;
+  } else if (addition.isEmpty) {
+    return base;
+  } else {
+    return '$base | $addition';
+  }
+}
+
+Object? _getAnnotationInstance(DeclarationMirror declaration, Type type) {
+  for (var annotation in declaration.metadata) {
+    if ((annotation.reflectee as Object).runtimeType == type) {
+      return annotation.reflectee;
+    }
+  }
+  return null;
+}
+
+bool _hasAnnotationInstance(DeclarationMirror declaration, Object instance) =>
+    declaration.metadata.any((InstanceMirror annotation) =>
+        identical(annotation.reflectee, instance));
+
+bool _hasAssertFailingTestAnnotation(MethodMirror method) =>
+    _hasAnnotationInstance(method, assertFailingTest);
+
+bool _hasFailingTestAnnotation(MethodMirror method) =>
+    _hasAnnotationInstance(method, failingTest);
+
+bool _hasSkippedTestAnnotation(MethodMirror method) =>
+    _hasAnnotationInstance(method, skippedTest);
+
+Future<Object?> _invokeSymbolIfExists(
+    InstanceMirror instanceMirror, Symbol symbol) {
+  Object? invocationResult;
+  InstanceMirror? closure;
+  try {
+    closure = instanceMirror.getField(symbol);
+    // ignore: avoid_catching_errors
+  } on NoSuchMethodError {
+    // ignore
+  }
+
+  if (closure is ClosureMirror) {
+    invocationResult = closure.apply([]).reflectee;
+  }
+  return Future.value(invocationResult);
+}
+
+/// Run a test that is expected to fail, and confirm that it fails.
+///
+/// This properly handles the following cases:
+/// - The test fails by throwing an exception
+/// - The test returns a future which completes with an error.
+/// - An exception is thrown to the zone handler from a timer task.
+Future<Object?>? _runFailingTest(ClassMirror classMirror, Symbol symbol) {
+  var passed = false;
+  return runZonedGuarded(() {
+    // ignore: void_checks
+    return Future.sync(() => _runTest(classMirror, symbol)).then<void>((_) {
+      passed = true;
+      test_package.fail('Test passed - expected to fail.');
+    }).catchError((Object e) {
+      // if passed, and we call fail(), rethrow this exception
+      if (passed) {
+        // ignore: only_throw_errors
+        throw e;
+      }
+      // otherwise, an exception is not a failure for _runFailingTest
+    });
+  }, (e, st) {
+    // if passed, and we call fail(), rethrow this exception
+    if (passed) {
+      // ignore: only_throw_errors
+      throw e;
+    }
+    // otherwise, an exception is not a failure for _runFailingTest
+  });
+}
+
+Future<void> _runTest(ClassMirror classMirror, Symbol symbol) async {
+  var instanceMirror = classMirror.newInstance(const Symbol(''), []);
+  try {
+    await _invokeSymbolIfExists(instanceMirror, #setUp);
+    await instanceMirror.invoke(symbol, []).reflectee;
+  } finally {
+    await _invokeSymbolIfExists(instanceMirror, #tearDown);
+  }
+}
+
+typedef _TestFunction = dynamic Function();
+
+/// A marker annotation used to annotate test methods which are expected to
+/// fail.
+class FailingTest {
+  /// Initialize this annotation with the given arguments.
+  ///
+  /// [issue] is a full URI describing the failure and used for tracking.
+  /// [reason] is a free form textual description.
+  const FailingTest({String? issue, String? reason});
+}
+
+/// A marker annotation used to annotate test methods which are skipped.
+class SkippedTest {
+  /// Initialize this annotation with the given arguments.
+  ///
+  /// [issue] is a full URI describing the failure and used for tracking.
+  /// [reason] is a free form textual description.
+  const SkippedTest({String? issue, String? reason});
+}
+
+/// A marker annotation used to annotate test methods with additional timeout
+/// information.
+class TestTimeout {
+  final test_package.Timeout _timeout;
+
+  /// Initialize this annotation with the given timeout.
+  const TestTimeout(test_package.Timeout timeout) : _timeout = timeout;
+}
+
+/// A marker annotation used to annotate test methods which are expected to fail
+/// when asserts are enabled.
+class _AssertFailingTest {
+  const _AssertFailingTest();
+}
+
+/// Information about a type based test group.
+class _Group {
+  final bool isSolo;
+  final String name;
+  final List<_Test> tests = <_Test>[];
+
+  _Group(this.isSolo, this.name);
+
+  bool get hasSoloTest => tests.any((test) => test.isSolo);
+
+  void addSkippedTest(String name) {
+    var fullName = _combineNames(this.name, name);
+    tests.add(_Test.skipped(isSolo, fullName));
+  }
+
+  void addTest(bool isSolo, String name, MethodMirror memberMirror,
+      _TestFunction function) {
+    var fullName = _combineNames(this.name, name);
+    var timeout =
+        _getAnnotationInstance(memberMirror, TestTimeout) as TestTimeout?;
+    tests.add(_Test(isSolo, fullName, function, timeout?._timeout));
+  }
+}
+
+/// A marker annotation used to instruct dart2js to keep reflection information
+/// for the annotated classes.
+class _ReflectiveTest {
+  const _ReflectiveTest();
+}
+
+/// A marker annotation used to annotate "solo" groups and tests.
+class _SoloTest {
+  const _SoloTest();
+}
+
+/// Information about a test.
+class _Test {
+  final bool isSolo;
+  final String name;
+  final _TestFunction function;
+  final test_package.Timeout? timeout;
+
+  final bool isSkipped;
+
+  _Test(this.isSolo, this.name, this.function, this.timeout)
+      : isSkipped = false;
+
+  _Test.skipped(this.isSolo, this.name)
+      : isSkipped = true,
+        function = (() {}),
+        timeout = null;
+}
diff --git a/pkgs/test_reflective_loader/pubspec.yaml b/pkgs/test_reflective_loader/pubspec.yaml
new file mode 100644
index 0000000..569933f
--- /dev/null
+++ b/pkgs/test_reflective_loader/pubspec.yaml
@@ -0,0 +1,13 @@
+name: test_reflective_loader
+version: 0.2.3
+description: Support for discovering tests and test suites using reflection.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/test_reflective_loader
+
+environment:
+  sdk: ^3.1.0
+
+dependencies:
+  test: ^1.16.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
diff --git a/pkgs/test_reflective_loader/test/test_reflective_loader_test.dart b/pkgs/test_reflective_loader/test/test_reflective_loader_test.dart
new file mode 100644
index 0000000..fad98a5
--- /dev/null
+++ b/pkgs/test_reflective_loader/test/test_reflective_loader_test.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2017, 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.
+
+// ignore_for_file: non_constant_identifier_names
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(TestReflectiveLoaderTest);
+  });
+}
+
+@reflectiveTest
+class TestReflectiveLoaderTest {
+  void test_passes() {
+    expect(true, true);
+  }
+
+  @failingTest
+  void test_fails() {
+    expect(false, true);
+  }
+
+  @failingTest
+  void test_fails_throws_sync() {
+    throw StateError('foo');
+  }
+
+  @failingTest
+  Future test_fails_throws_async() {
+    return Future.error('foo');
+  }
+
+  @skippedTest
+  void test_fails_but_skipped() {
+    throw StateError('foo');
+  }
+
+  @skippedTest
+  void test_times_out_but_skipped() {
+    while (true) {}
+  }
+}
diff --git a/pkgs/timing/.gitignore b/pkgs/timing/.gitignore
new file mode 100644
index 0000000..1ddf798
--- /dev/null
+++ b/pkgs/timing/.gitignore
@@ -0,0 +1,7 @@
+.packages
+/build/
+pubspec.lock
+
+# Files generated by dart tools
+.dart_tool
+doc/
diff --git a/pkgs/timing/CHANGELOG.md b/pkgs/timing/CHANGELOG.md
new file mode 100644
index 0000000..8cdb8ea
--- /dev/null
+++ b/pkgs/timing/CHANGELOG.md
@@ -0,0 +1,34 @@
+## 1.0.2
+
+- Require Dart `3.4`.
+- Move to `dart-lang/tools` monorepo.
+
+## 1.0.1
+
+- Require Dart `2.14`.
+
+## 1.0.0
+
+- Enable null safety.
+- Require Dart `2.12`.
+
+## 0.1.1+3
+
+- Allow `package:json_annotation` `'>=1.0.0 <5.0.0'`.
+
+## 0.1.1+2
+
+- Support the latest version of `package:json_annotation`.
+- Require Dart 2.2 or later.
+
+## 0.1.1+1
+
+- Support the latest version of `package:json_annotation`.
+
+## 0.1.1
+
+- Add JSON serialization
+
+## 0.1.0
+
+- Initial release
diff --git a/pkgs/timing/LICENSE b/pkgs/timing/LICENSE
new file mode 100644
index 0000000..9972f6e
--- /dev/null
+++ b/pkgs/timing/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2018, 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/timing/README.md b/pkgs/timing/README.md
new file mode 100644
index 0000000..9dab7cc
--- /dev/null
+++ b/pkgs/timing/README.md
@@ -0,0 +1,30 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/timing.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/timing.yaml)
+[![pub package](https://img.shields.io/pub/v/timing.svg)](https://pub.dev/packages/timing)
+[![package publisher](https://img.shields.io/pub/publisher/timing.svg)](https://pub.dev/packages/timing/publisher)
+
+Timing is a simple package for tracking performance of both async and sync actions
+
+## Usage
+
+```dart
+var tracker = AsyncTimeTracker();
+await tracker.track(() async {
+  // some async code here
+});
+
+// Use results
+print('${tracker.duration} ${tracker.innerDuration} ${tracker.slices}');
+```
+
+## Building
+
+Use the following command to re-generate `lib/src/timing.g.dart` file:
+
+```bash
+dart pub run build_runner build
+```
+
+## Publishing automation
+
+For information about our publishing automation and release process, see
+https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.
diff --git a/pkgs/timing/analysis_options.yaml b/pkgs/timing/analysis_options.yaml
new file mode 100644
index 0000000..396236d
--- /dev/null
+++ b/pkgs/timing/analysis_options.yaml
@@ -0,0 +1,2 @@
+# https://dart.dev/tools/analysis#the-analysis-options-file
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/timing/lib/src/clock.dart b/pkgs/timing/lib/src/clock.dart
new file mode 100644
index 0000000..6a9d295
--- /dev/null
+++ b/pkgs/timing/lib/src/clock.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2017, 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';
+
+/// A function that returns the current [DateTime].
+typedef _Clock = DateTime Function();
+DateTime _defaultClock() => DateTime.now();
+
+const _zoneKey = #timing_Clock;
+
+/// Returns the current [DateTime].
+///
+/// May be overridden for tests using [scopeClock].
+DateTime now() => (Zone.current[_zoneKey] as _Clock? ?? _defaultClock)();
+
+/// Runs [f], with [clock] scoped whenever [now] is called.
+T scopeClock<T>(DateTime Function() clock, T Function() f) =>
+    runZoned(f, zoneValues: {_zoneKey: clock});
diff --git a/pkgs/timing/lib/src/timing.dart b/pkgs/timing/lib/src/timing.dart
new file mode 100644
index 0000000..049ba81
--- /dev/null
+++ b/pkgs/timing/lib/src/timing.dart
@@ -0,0 +1,338 @@
+// Copyright (c) 2018, 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:json_annotation/json_annotation.dart';
+
+import 'clock.dart';
+
+part 'timing.g.dart';
+
+/// The timings of an operation, including its [startTime], [stopTime], and
+/// [duration].
+@JsonSerializable()
+class TimeSlice {
+  /// The total duration of this operation, equivalent to taking the difference
+  /// between [stopTime] and [startTime].
+  Duration get duration => stopTime.difference(startTime);
+
+  final DateTime startTime;
+
+  final DateTime stopTime;
+
+  TimeSlice(this.startTime, this.stopTime);
+
+  factory TimeSlice.fromJson(Map<String, dynamic> json) =>
+      _$TimeSliceFromJson(json);
+
+  Map<String, dynamic> toJson() => _$TimeSliceToJson(this);
+
+  @override
+  String toString() => '($startTime + $duration)';
+}
+
+/// The timings of an async operation, consist of several sync [slices] and
+/// includes total [startTime], [stopTime], and [duration].
+@JsonSerializable()
+class TimeSliceGroup implements TimeSlice {
+  final List<TimeSlice> slices;
+
+  @override
+  DateTime get startTime => slices.first.startTime;
+
+  @override
+  DateTime get stopTime => slices.last.stopTime;
+
+  /// The total duration of this operation, equivalent to taking the difference
+  /// between [stopTime] and [startTime].
+  @override
+  Duration get duration => stopTime.difference(startTime);
+
+  /// Sum of [duration]s of all [slices].
+  ///
+  /// If some of slices implements [TimeSliceGroup] [innerDuration] will be used
+  /// to compute sum.
+  Duration get innerDuration => slices.fold(
+      Duration.zero,
+      (duration, slice) =>
+          duration +
+          (slice is TimeSliceGroup ? slice.innerDuration : slice.duration));
+
+  TimeSliceGroup(this.slices);
+
+  /// Constructs TimeSliceGroup from JSON representation
+  factory TimeSliceGroup.fromJson(Map<String, dynamic> json) =>
+      _$TimeSliceGroupFromJson(json);
+
+  @override
+  Map<String, dynamic> toJson() => _$TimeSliceGroupToJson(this);
+
+  @override
+  String toString() => slices.toString();
+}
+
+abstract class TimeTracker implements TimeSlice {
+  /// Whether tracking is active.
+  ///
+  /// Tracking is only active after `isStarted` and before `isFinished`.
+  bool get isTracking;
+
+  /// Whether tracking is finished.
+  ///
+  /// Tracker can't be used as [TimeSlice] before it is finished
+  bool get isFinished;
+
+  /// Whether tracking was started.
+  ///
+  /// Equivalent of `isTracking || isFinished`
+  bool get isStarted;
+
+  T track<T>(T Function() action);
+}
+
+/// Tracks only sync actions
+class SyncTimeTracker implements TimeTracker {
+  /// When this operation started, call [_start] to set this.
+  @override
+  DateTime get startTime => _startTime!;
+  DateTime? _startTime;
+
+  /// When this operation stopped, call [_stop] to set this.
+  @override
+  DateTime get stopTime => _stopTime!;
+  DateTime? _stopTime;
+
+  /// Start tracking this operation, must only be called once, before [_stop].
+  void _start() {
+    assert(_startTime == null && _stopTime == null);
+    _startTime = now();
+  }
+
+  /// Stop tracking this operation, must only be called once, after [_start].
+  void _stop() {
+    assert(_startTime != null && _stopTime == null);
+    _stopTime = now();
+  }
+
+  /// Splits tracker into two slices.
+  ///
+  /// Returns new [TimeSlice] started on [startTime] and ended now. Modifies
+  /// [startTime] of tracker to current time point
+  ///
+  /// Don't change state of tracker. Can be called only while [isTracking], and
+  /// tracker will sill be tracking after call.
+  TimeSlice _split() {
+    if (!isTracking) {
+      throw StateError('Can be only called while tracking');
+    }
+    final splitPoint = now();
+    final prevSlice = TimeSlice(_startTime!, splitPoint);
+    _startTime = splitPoint;
+    return prevSlice;
+  }
+
+  @override
+  T track<T>(T Function() action) {
+    if (isStarted) {
+      throw StateError('Can not be tracked twice');
+    }
+    _start();
+    try {
+      return action();
+    } finally {
+      _stop();
+    }
+  }
+
+  @override
+  bool get isStarted => _startTime != null;
+
+  @override
+  bool get isTracking => _startTime != null && _stopTime == null;
+
+  @override
+  bool get isFinished => _startTime != null && _stopTime != null;
+
+  @override
+  Duration get duration => _stopTime!.difference(_startTime!);
+
+  /// Converts to JSON representation
+  ///
+  /// Can't be used before [isFinished]
+  @override
+  Map<String, dynamic> toJson() => _$TimeSliceToJson(this);
+}
+
+/// Async actions returning [Future] will be tracked as single sync time span
+/// from the beginning of execution till completion of future
+class SimpleAsyncTimeTracker extends SyncTimeTracker {
+  @override
+  T track<T>(T Function() action) {
+    if (isStarted) {
+      throw StateError('Can not be tracked twice');
+    }
+    T result;
+    _start();
+    try {
+      result = action();
+    } catch (_) {
+      _stop();
+      rethrow;
+    }
+    if (result is Future) {
+      return result.whenComplete(_stop) as T;
+    } else {
+      _stop();
+      return result;
+    }
+  }
+}
+
+/// No-op implementation of [SyncTimeTracker] that does nothing.
+class NoOpTimeTracker implements TimeTracker {
+  static final sharedInstance = NoOpTimeTracker();
+
+  @override
+  Duration get duration =>
+      throw UnsupportedError('Unsupported in no-op implementation');
+
+  @override
+  DateTime get startTime =>
+      throw UnsupportedError('Unsupported in no-op implementation');
+
+  @override
+  DateTime get stopTime =>
+      throw UnsupportedError('Unsupported in no-op implementation');
+
+  @override
+  bool get isStarted =>
+      throw UnsupportedError('Unsupported in no-op implementation');
+
+  @override
+  bool get isTracking =>
+      throw UnsupportedError('Unsupported in no-op implementation');
+
+  @override
+  bool get isFinished =>
+      throw UnsupportedError('Unsupported in no-op implementation');
+
+  @override
+  T track<T>(T Function() action) => action();
+
+  @override
+  Map<String, dynamic> toJson() =>
+      throw UnsupportedError('Unsupported in no-op implementation');
+}
+
+/// Track all async execution as disjoint time [slices] in ascending order.
+///
+/// Can [track] both async and sync actions.
+/// Can exclude time of tested trackers.
+///
+/// If tracked action spawns some dangled async executions behavior is't
+/// defined. Tracked might or might not track time of such executions
+class AsyncTimeTracker extends TimeSliceGroup implements TimeTracker {
+  final bool trackNested;
+
+  static const _zoneKey = #timing_AsyncTimeTracker;
+
+  AsyncTimeTracker({this.trackNested = true}) : super([]);
+
+  T _trackSyncSlice<T>(ZoneDelegate parent, Zone zone, T Function() action) {
+    // Ignore dangling runs after tracker completes
+    if (isFinished) {
+      return action();
+    }
+
+    final isNestedRun = slices.isNotEmpty &&
+        slices.last is SyncTimeTracker &&
+        (slices.last as SyncTimeTracker).isTracking;
+    final isExcludedNestedTrack = !trackNested && zone[_zoneKey] != this;
+
+    // Exclude nested sync tracks
+    if (isNestedRun && isExcludedNestedTrack) {
+      final timer = slices.last as SyncTimeTracker;
+      // Split already tracked time into new slice.
+      // Replace tracker in slices.last with splitted slice, to indicate for
+      // recursive calls that we not tracking.
+      slices.last = parent.run(zone, timer._split);
+      try {
+        return action();
+      } finally {
+        // Split tracker again and discard slice from nested tracker
+        parent.run(zone, timer._split);
+        // Add tracker back to list of slices and continue tracking
+        slices.add(timer);
+      }
+    }
+
+    // Exclude nested async tracks
+    if (isExcludedNestedTrack) {
+      return action();
+    }
+
+    // Split time slices in nested sync runs
+    if (isNestedRun) {
+      return action();
+    }
+
+    final timer = SyncTimeTracker();
+    slices.add(timer);
+
+    // Pass to parent zone, in case of overwritten clock
+    return parent.runUnary(zone, timer.track, action);
+  }
+
+  static final asyncTimeTrackerZoneSpecification = ZoneSpecification(
+    run: <R>(Zone self, ZoneDelegate parent, Zone zone, R Function() f) {
+      final tracker = self[_zoneKey] as AsyncTimeTracker;
+      return tracker._trackSyncSlice(parent, zone, () => parent.run(zone, f));
+    },
+    runUnary: <R, T>(Zone self, ZoneDelegate parent, Zone zone, R Function(T) f,
+        T arg) {
+      final tracker = self[_zoneKey] as AsyncTimeTracker;
+      return tracker._trackSyncSlice(
+          parent, zone, () => parent.runUnary(zone, f, arg));
+    },
+    runBinary: <R, T1, T2>(Zone self, ZoneDelegate parent, Zone zone,
+        R Function(T1, T2) f, T1 arg1, T2 arg2) {
+      final tracker = self[_zoneKey] as AsyncTimeTracker;
+      return tracker._trackSyncSlice(
+          parent, zone, () => parent.runBinary(zone, f, arg1, arg2));
+    },
+  );
+
+  @override
+  T track<T>(T Function() action) {
+    if (isStarted) {
+      throw StateError('Can not be tracked twice');
+    }
+    _tracking = true;
+    final result = runZoned(action,
+        zoneSpecification: asyncTimeTrackerZoneSpecification,
+        zoneValues: {_zoneKey: this});
+    if (result is Future) {
+      return result
+          // Break possible sync processing of future completion, so slice
+          // trackers can be finished
+          .whenComplete(Future.value)
+          .whenComplete(() => _tracking = false) as T;
+    } else {
+      _tracking = false;
+      return result;
+    }
+  }
+
+  bool? _tracking;
+
+  @override
+  bool get isStarted => _tracking != null;
+
+  @override
+  bool get isFinished => _tracking == false;
+
+  @override
+  bool get isTracking => _tracking == true;
+}
diff --git a/pkgs/timing/lib/src/timing.g.dart b/pkgs/timing/lib/src/timing.g.dart
new file mode 100644
index 0000000..679c082
--- /dev/null
+++ b/pkgs/timing/lib/src/timing.g.dart
@@ -0,0 +1,29 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'timing.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+TimeSlice _$TimeSliceFromJson(Map<String, dynamic> json) => TimeSlice(
+      DateTime.parse(json['startTime'] as String),
+      DateTime.parse(json['stopTime'] as String),
+    );
+
+Map<String, dynamic> _$TimeSliceToJson(TimeSlice instance) => <String, dynamic>{
+      'startTime': instance.startTime.toIso8601String(),
+      'stopTime': instance.stopTime.toIso8601String(),
+    };
+
+TimeSliceGroup _$TimeSliceGroupFromJson(Map<String, dynamic> json) =>
+    TimeSliceGroup(
+      (json['slices'] as List<dynamic>)
+          .map((e) => TimeSlice.fromJson(e as Map<String, dynamic>))
+          .toList(),
+    );
+
+Map<String, dynamic> _$TimeSliceGroupToJson(TimeSliceGroup instance) =>
+    <String, dynamic>{
+      'slices': instance.slices,
+    };
diff --git a/pkgs/timing/lib/timing.dart b/pkgs/timing/lib/timing.dart
new file mode 100644
index 0000000..5cb16d4
--- /dev/null
+++ b/pkgs/timing/lib/timing.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2018, 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/timing.dart'
+    show
+        AsyncTimeTracker,
+        NoOpTimeTracker,
+        SimpleAsyncTimeTracker,
+        SyncTimeTracker,
+        TimeSlice,
+        TimeSliceGroup,
+        TimeTracker;
diff --git a/pkgs/timing/pubspec.yaml b/pkgs/timing/pubspec.yaml
new file mode 100644
index 0000000..891a8af
--- /dev/null
+++ b/pkgs/timing/pubspec.yaml
@@ -0,0 +1,18 @@
+name: timing
+version: 1.0.2
+description: >-
+  A simple package for tracking the performance of synchronous and asynchronous
+  actions.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/timing
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  json_annotation: ^4.9.0
+
+dev_dependencies:
+  build_runner: ^2.0.6
+  dart_flutter_team_lints: ^3.0.0
+  json_serializable: ^6.0.0
+  test: ^1.17.10
diff --git a/pkgs/timing/test/timing_test.dart b/pkgs/timing/test/timing_test.dart
new file mode 100644
index 0000000..b5836d9
--- /dev/null
+++ b/pkgs/timing/test/timing_test.dart
@@ -0,0 +1,416 @@
+// Copyright (c) 2018, 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.
+
+// ignore_for_file: only_throw_errors, inference_failure_on_instance_creation
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:timing/src/clock.dart';
+import 'package:timing/src/timing.dart';
+
+void _noop() {}
+
+void main() {
+  late DateTime time;
+  final startTime = DateTime(2017);
+  DateTime fakeClock() => time;
+
+  late TimeTracker tracker;
+  late TimeTracker nestedTracker;
+
+  T scopedTrack<T>(T Function() f) =>
+      scopeClock(fakeClock, () => tracker.track(f));
+
+  setUp(() {
+    time = startTime;
+  });
+
+  void canHandleSync([void Function() additionalExpects = _noop]) {
+    test('Can track sync code', () {
+      expect(tracker.isStarted, false);
+      expect(tracker.isTracking, false);
+      expect(tracker.isFinished, false);
+      scopedTrack(() {
+        expect(tracker.isStarted, true);
+        expect(tracker.isTracking, true);
+        expect(tracker.isFinished, false);
+        time = time.add(const Duration(seconds: 5));
+      });
+      expect(tracker.isStarted, true);
+      expect(tracker.isTracking, false);
+      expect(tracker.isFinished, true);
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, time);
+      expect(tracker.duration, const Duration(seconds: 5));
+      additionalExpects();
+    });
+
+    test('Can track handled sync exceptions', () async {
+      scopedTrack(() {
+        try {
+          time = time.add(const Duration(seconds: 4));
+          throw 'error';
+        } on String {
+          time = time.add(const Duration(seconds: 1));
+        }
+      });
+      expect(tracker.isFinished, true);
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, time);
+      expect(tracker.duration, const Duration(seconds: 5));
+      additionalExpects();
+    });
+
+    test('Can track in case of unhandled sync exceptions', () async {
+      expect(
+          () => scopedTrack(() {
+                time = time.add(const Duration(seconds: 5));
+                throw 'error';
+              }),
+          throwsA(const TypeMatcher<String>()));
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, time);
+      expect(tracker.duration, const Duration(seconds: 5));
+      additionalExpects();
+    });
+
+    test('Can be nested sync', () {
+      scopedTrack(() {
+        time = time.add(const Duration(seconds: 1));
+        nestedTracker.track(() {
+          time = time.add(const Duration(seconds: 2));
+        });
+        time = time.add(const Duration(seconds: 4));
+      });
+      expect(tracker.isFinished, true);
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, time);
+      expect(tracker.duration, const Duration(seconds: 7));
+      expect(nestedTracker.startTime.isAfter(startTime), true);
+      expect(nestedTracker.stopTime.isBefore(time), true);
+      expect(nestedTracker.duration, const Duration(seconds: 2));
+      additionalExpects();
+    });
+  }
+
+  void canHandleAsync([void Function() additionalExpects = _noop]) {
+    test('Can track async code', () async {
+      expect(tracker.isStarted, false);
+      expect(tracker.isTracking, false);
+      expect(tracker.isFinished, false);
+      await scopedTrack(() => Future(() {
+            expect(tracker.isStarted, true);
+            expect(tracker.isTracking, true);
+            expect(tracker.isFinished, false);
+            time = time.add(const Duration(seconds: 5));
+          }));
+      expect(tracker.isStarted, true);
+      expect(tracker.isTracking, false);
+      expect(tracker.isFinished, true);
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, time);
+      expect(tracker.duration, const Duration(seconds: 5));
+      additionalExpects();
+    });
+
+    test('Can track handled async exceptions', () async {
+      await scopedTrack(() {
+        time = time.add(const Duration(seconds: 1));
+        return Future(() {
+          time = time.add(const Duration(seconds: 2));
+          throw 'error';
+        }).then((_) {
+          time = time.add(const Duration(seconds: 4));
+        }).catchError((error, stack) {
+          time = time.add(const Duration(seconds: 8));
+        });
+      });
+      expect(tracker.isFinished, true);
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, time);
+      expect(tracker.duration, const Duration(seconds: 11));
+      additionalExpects();
+    });
+
+    test('Can track in case of unhandled async exceptions', () async {
+      final future = scopedTrack(() {
+        time = time.add(const Duration(seconds: 1));
+        return Future(() {
+          time = time.add(const Duration(seconds: 2));
+          throw 'error';
+        }).then((_) {
+          time = time.add(const Duration(seconds: 4));
+        });
+      });
+      await expectLater(future, throwsA(const TypeMatcher<String>()));
+      expect(tracker.isFinished, true);
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, time);
+      expect(tracker.duration, const Duration(seconds: 3));
+      additionalExpects();
+    });
+
+    test('Can be nested async', () async {
+      await scopedTrack(() async {
+        time = time.add(const Duration(milliseconds: 1));
+        await Future.value();
+        time = time.add(const Duration(milliseconds: 2));
+        await nestedTracker.track(() async {
+          time = time.add(const Duration(milliseconds: 4));
+          await Future.value();
+          time = time.add(const Duration(milliseconds: 8));
+          await Future.value();
+          time = time.add(const Duration(milliseconds: 16));
+        });
+        time = time.add(const Duration(milliseconds: 32));
+        await Future.value();
+        time = time.add(const Duration(milliseconds: 64));
+      });
+      expect(tracker.isFinished, true);
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, time);
+      expect(tracker.duration, const Duration(milliseconds: 127));
+      expect(nestedTracker.startTime.isAfter(startTime), true);
+      expect(nestedTracker.stopTime.isBefore(time), true);
+      expect(nestedTracker.duration, const Duration(milliseconds: 28));
+      additionalExpects();
+    });
+  }
+
+  group('SyncTimeTracker', () {
+    setUp(() {
+      tracker = SyncTimeTracker();
+      nestedTracker = SyncTimeTracker();
+    });
+
+    canHandleSync();
+
+    test('Can not track async code', () async {
+      await scopedTrack(() => Future(() {
+            time = time.add(const Duration(seconds: 5));
+          }));
+      expect(tracker.isFinished, true);
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, startTime);
+      expect(tracker.duration, const Duration(seconds: 0));
+    });
+  });
+
+  group('AsyncTimeTracker.simple', () {
+    setUp(() {
+      tracker = SimpleAsyncTimeTracker();
+      nestedTracker = SimpleAsyncTimeTracker();
+    });
+
+    canHandleSync();
+
+    canHandleAsync();
+
+    test('Can not distinguish own async code', () async {
+      final future = scopedTrack(() => Future(() {
+            time = time.add(const Duration(seconds: 5));
+          }));
+      time = time.add(const Duration(seconds: 10));
+      await future;
+      expect(tracker.isFinished, true);
+      expect(tracker.startTime, startTime);
+      expect(tracker.stopTime, time);
+      expect(tracker.duration, const Duration(seconds: 15));
+    });
+  });
+
+  group('AsyncTimeTracker', () {
+    late AsyncTimeTracker asyncTracker;
+    late AsyncTimeTracker nestedAsyncTracker;
+    setUp(() {
+      tracker = asyncTracker = AsyncTimeTracker();
+      nestedTracker = nestedAsyncTracker = AsyncTimeTracker();
+    });
+
+    canHandleSync(() {
+      expect(asyncTracker.innerDuration, asyncTracker.duration);
+      expect(asyncTracker.slices.length, 1);
+    });
+
+    canHandleAsync(() {
+      expect(asyncTracker.innerDuration, asyncTracker.duration);
+      expect(asyncTracker.slices.length, greaterThan(1));
+    });
+
+    test('Can track complex async innerDuration', () async {
+      final completer = Completer();
+      final future = scopedTrack(() async {
+        time = time.add(const Duration(seconds: 1)); // Tracked sync
+        await Future.value();
+        time = time.add(const Duration(seconds: 2)); // Tracked async
+        await completer.future;
+        time = time.add(const Duration(seconds: 4)); // Tracked async, delayed
+      }).then((_) {
+        time = time.add(const Duration(seconds: 8)); // Async, after tracking
+      });
+      time = time.add(const Duration(seconds: 16)); // Sync, between slices
+
+      await Future(() {
+        // Async, between slices
+        time = time.add(const Duration(seconds: 32));
+        completer.complete();
+      });
+      await future;
+      expect(asyncTracker.isFinished, true);
+      expect(asyncTracker.startTime, startTime);
+      expect(asyncTracker.stopTime.isBefore(time), true);
+      expect(asyncTracker.duration, const Duration(seconds: 55));
+      expect(asyncTracker.innerDuration, const Duration(seconds: 7));
+      expect(asyncTracker.slices.length, greaterThan(1));
+    });
+
+    test('Can exclude nested sync', () {
+      tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+      scopedTrack(() {
+        time = time.add(const Duration(seconds: 1));
+        nestedAsyncTracker.track(() {
+          time = time.add(const Duration(seconds: 2));
+        });
+        time = time.add(const Duration(seconds: 4));
+      });
+      expect(asyncTracker.isFinished, true);
+      expect(asyncTracker.startTime, startTime);
+      expect(asyncTracker.stopTime, time);
+      expect(asyncTracker.duration, const Duration(seconds: 7));
+      expect(asyncTracker.innerDuration, const Duration(seconds: 5));
+      expect(asyncTracker.slices.length, greaterThan(1));
+      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+      expect(nestedAsyncTracker.duration, const Duration(seconds: 2));
+      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 2));
+      expect(nestedAsyncTracker.slices.length, 1);
+    });
+
+    test('Can exclude complex nested sync', () {
+      tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+      nestedAsyncTracker = AsyncTimeTracker(trackNested: false);
+      final nestedAsyncTracker2 = AsyncTimeTracker(trackNested: false);
+      scopedTrack(() {
+        time = time.add(const Duration(seconds: 1));
+        nestedAsyncTracker.track(() {
+          time = time.add(const Duration(seconds: 2));
+          nestedAsyncTracker2.track(() {
+            time = time.add(const Duration(seconds: 4));
+          });
+          time = time.add(const Duration(seconds: 8));
+        });
+        time = time.add(const Duration(seconds: 16));
+      });
+      expect(asyncTracker.isFinished, true);
+      expect(asyncTracker.startTime, startTime);
+      expect(asyncTracker.stopTime, time);
+      expect(asyncTracker.duration, const Duration(seconds: 31));
+      expect(asyncTracker.innerDuration, const Duration(seconds: 17));
+      expect(asyncTracker.slices.length, greaterThan(1));
+      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+      expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
+      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 10));
+      expect(nestedAsyncTracker.slices.length, greaterThan(1));
+      expect(nestedAsyncTracker2.startTime.isAfter(startTime), true);
+      expect(nestedAsyncTracker2.stopTime.isBefore(time), true);
+      expect(nestedAsyncTracker2.duration, const Duration(seconds: 4));
+      expect(nestedAsyncTracker2.innerDuration, const Duration(seconds: 4));
+      expect(nestedAsyncTracker2.slices.length, 1);
+    });
+
+    test(
+        'Can track all on grand-parent level and '
+        'exclude grand-childrens from parent', () {
+      tracker = asyncTracker = AsyncTimeTracker(trackNested: true);
+      nestedAsyncTracker = AsyncTimeTracker(trackNested: false);
+      final nestedAsyncTracker2 = AsyncTimeTracker();
+      scopedTrack(() {
+        time = time.add(const Duration(seconds: 1));
+        nestedAsyncTracker.track(() {
+          time = time.add(const Duration(seconds: 2));
+          nestedAsyncTracker2.track(() {
+            time = time.add(const Duration(seconds: 4));
+          });
+          time = time.add(const Duration(seconds: 8));
+        });
+        time = time.add(const Duration(seconds: 16));
+      });
+      expect(asyncTracker.isFinished, true);
+      expect(asyncTracker.startTime, startTime);
+      expect(asyncTracker.stopTime, time);
+      expect(asyncTracker.duration, const Duration(seconds: 31));
+      expect(asyncTracker.innerDuration, const Duration(seconds: 31));
+      expect(asyncTracker.slices.length, 1);
+      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+      expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
+      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 10));
+      expect(nestedAsyncTracker.slices.length, greaterThan(1));
+      expect(nestedAsyncTracker2.startTime.isAfter(startTime), true);
+      expect(nestedAsyncTracker2.stopTime.isBefore(time), true);
+      expect(nestedAsyncTracker2.duration, const Duration(seconds: 4));
+      expect(nestedAsyncTracker2.innerDuration, const Duration(seconds: 4));
+      expect(nestedAsyncTracker2.slices.length, 1);
+    });
+
+    test('Can exclude nested async', () async {
+      tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+      await scopedTrack(() async {
+        time = time.add(const Duration(seconds: 1));
+        await nestedAsyncTracker.track(() async {
+          time = time.add(const Duration(seconds: 2));
+          await Future.value();
+          time = time.add(const Duration(seconds: 4));
+          await Future.value();
+          time = time.add(const Duration(seconds: 8));
+        });
+        time = time.add(const Duration(seconds: 16));
+      });
+      expect(asyncTracker.isFinished, true);
+      expect(asyncTracker.startTime, startTime);
+      expect(asyncTracker.stopTime, time);
+      expect(asyncTracker.duration, const Duration(seconds: 31));
+      expect(asyncTracker.innerDuration, const Duration(seconds: 17));
+      expect(asyncTracker.slices.length, greaterThan(1));
+      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+      expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
+      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 14));
+      expect(nestedAsyncTracker.slices.length, greaterThan(1));
+    });
+
+    test('Can handle callbacks in excluded nested async', () async {
+      tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+      await scopedTrack(() async {
+        time = time.add(const Duration(seconds: 1));
+        final completer = Completer();
+        final future = completer.future.then((_) {
+          time = time.add(const Duration(seconds: 2));
+        });
+        await nestedAsyncTracker.track(() async {
+          time = time.add(const Duration(seconds: 4));
+          await Future.value();
+          time = time.add(const Duration(seconds: 8));
+          completer.complete();
+          await future;
+          time = time.add(const Duration(seconds: 16));
+        });
+        time = time.add(const Duration(seconds: 32));
+      });
+      expect(asyncTracker.isFinished, true);
+      expect(asyncTracker.startTime, startTime);
+      expect(asyncTracker.stopTime, time);
+      expect(asyncTracker.duration, const Duration(seconds: 63));
+      expect(asyncTracker.innerDuration, const Duration(seconds: 35));
+      expect(asyncTracker.slices.length, greaterThan(1));
+      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+      expect(nestedAsyncTracker.duration, const Duration(seconds: 30));
+      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 28));
+      expect(nestedAsyncTracker.slices.length, greaterThan(1));
+    });
+  });
+}
diff --git a/pkgs/watcher/.gitignore b/pkgs/watcher/.gitignore
new file mode 100644
index 0000000..ac98e87
--- /dev/null
+++ b/pkgs/watcher/.gitignore
@@ -0,0 +1,4 @@
+# Don’t commit the following directories created by pub.
+.dart_tool
+.packages
+pubspec.lock
diff --git a/pkgs/watcher/.test_config b/pkgs/watcher/.test_config
new file mode 100644
index 0000000..531426a
--- /dev/null
+++ b/pkgs/watcher/.test_config
@@ -0,0 +1,5 @@
+{
+  "test_package": {
+    "platforms": ["vm"]
+  }
+}
\ No newline at end of file
diff --git a/pkgs/watcher/CHANGELOG.md b/pkgs/watcher/CHANGELOG.md
new file mode 100644
index 0000000..ef3a7e2
--- /dev/null
+++ b/pkgs/watcher/CHANGELOG.md
@@ -0,0 +1,130 @@
+## 1.1.1
+
+- Ensure `PollingFileWatcher.ready` completes for files that do not exist.
+- Require Dart SDK `^3.1.0`
+- Move to `dart-lang/tools` monorepo.
+
+## 1.1.0
+
+- Require Dart SDK >= 3.0.0
+- Remove usage of redundant ConstructableFileSystemEvent classes.
+
+## 1.0.3-dev
+
+- Require Dart SDK >= 2.19
+
+## 1.0.2
+
+- Require Dart SDK >= 2.14
+- Ensure `DirectoryWatcher.ready` completes even when errors occur that close the watcher.
+- Add markdown badges to the readme.
+
+## 1.0.1
+
+* Drop package:pedantic and use package:lints instead.
+
+## 1.0.0
+
+* Require Dart SDK >= 2.12
+* Add the ability to create custom Watcher types for specific file paths.
+
+## 0.9.7+15
+
+* Fix a bug on Mac where modifying a directory with a path exactly matching a
+  prefix of a modified file would suppress change events for that file.
+
+## 0.9.7+14
+
+* Prepare for breaking change in SDK where modified times for not found files
+  becomes meaningless instead of null.
+
+## 0.9.7+13
+
+* Catch & forward `FileSystemException` from unexpectedly closed file watchers
+  on windows; the watcher will also be automatically restarted when this occurs.
+
+## 0.9.7+12
+
+* Catch `FileSystemException` during `existsSync()` on Windows.
+* Internal cleanup.
+
+## 0.9.7+11
+
+* Fix an analysis hint.
+
+## 0.9.7+10
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 0.9.7+9
+
+* Internal changes only.
+
+## 0.9.7+8
+
+* Fix Dart 2.0 type issues on Mac and Windows.
+
+## 0.9.7+7
+
+* Updates to support Dart 2.0 core library changes (wave 2.2).
+  See [issue 31847][sdk#31847] for details.
+
+  [sdk#31847]: https://github.com/dart-lang/sdk/issues/31847
+
+
+## 0.9.7+6
+
+* Internal changes only, namely removing dep on scheduled test.
+
+## 0.9.7+5
+
+* Fix an analysis warning.
+
+## 0.9.7+4
+
+* Declare support for `async` 2.0.0.
+
+## 0.9.7+3
+
+* Fix a crashing bug on Linux.
+
+## 0.9.7+2
+
+* Narrow the constraint on `async` to reflect the APIs this package is actually
+  using.
+
+## 0.9.7+1
+
+* Fix all strong-mode warnings.
+
+## 0.9.7
+
+* Fix a bug in `FileWatcher` where events could be added after watchers were
+  closed.
+
+## 0.9.6
+
+* Add a `Watcher` interface that encompasses watching both files and
+  directories.
+
+* Add `FileWatcher` and `PollingFileWatcher` classes for watching changes to
+  individual files.
+
+* Deprecate `DirectoryWatcher.directory`. Use `DirectoryWatcher.path` instead.
+
+## 0.9.5
+
+* Fix bugs where events could be added after watchers were closed.
+
+## 0.9.4
+
+* Treat add events for known files as modifications instead of discarding them
+  on Mac OS.
+
+## 0.9.3
+
+* Improved support for Windows via `WindowsDirectoryWatcher`.
+
+* Simplified `PollingDirectoryWatcher`.
+
+* Fixed bugs in `MacOSDirectoryWatcher`
diff --git a/pkgs/watcher/LICENSE b/pkgs/watcher/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/watcher/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/watcher/README.md b/pkgs/watcher/README.md
new file mode 100644
index 0000000..83a0324
--- /dev/null
+++ b/pkgs/watcher/README.md
@@ -0,0 +1,10 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/watcher.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/watcher.yaml)
+[![pub package](https://img.shields.io/pub/v/watcher.svg)](https://pub.dev/packages/watcher)
+[![package publisher](https://img.shields.io/pub/publisher/watcher.svg)](https://pub.dev/packages/watcher/publisher)
+
+A file system watcher.
+
+## What's this?
+
+`package:watcher` monitors changes to contents of directories and sends
+notifications when files have been added, removed, or modified.
diff --git a/pkgs/watcher/analysis_options.yaml b/pkgs/watcher/analysis_options.yaml
new file mode 100644
index 0000000..d978f81
--- /dev/null
+++ b/pkgs/watcher/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/watcher/benchmark/path_set.dart b/pkgs/watcher/benchmark/path_set.dart
new file mode 100644
index 0000000..e7929d8
--- /dev/null
+++ b/pkgs/watcher/benchmark/path_set.dart
@@ -0,0 +1,158 @@
+// 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.
+
+/// Benchmarks for the PathSet class.
+library;
+
+import 'dart:io';
+import 'dart:math' as math;
+
+import 'package:benchmark_harness/benchmark_harness.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/src/path_set.dart';
+
+final String root = Platform.isWindows ? r'C:\root' : '/root';
+
+/// Base class for benchmarks on [PathSet].
+abstract class PathSetBenchmark extends BenchmarkBase {
+  PathSetBenchmark(String method) : super('PathSet.$method');
+
+  final PathSet pathSet = PathSet(root);
+
+  /// Use a fixed [math.Random] with a constant seed to ensure the tests are
+  /// deterministic.
+  final math.Random random = math.Random(1234);
+
+  /// Walks over a virtual directory [depth] levels deep invoking [callback]
+  /// for each "file".
+  ///
+  /// Each virtual directory contains ten entries: either subdirectories or
+  /// files.
+  void walkTree(int depth, void Function(String) callback) {
+    void recurse(String path, int remainingDepth) {
+      for (var i = 0; i < 10; i++) {
+        var padded = i.toString().padLeft(2, '0');
+        if (remainingDepth == 0) {
+          callback(p.join(path, 'file_$padded.txt'));
+        } else {
+          var subdir = p.join(path, 'subdirectory_$padded');
+          recurse(subdir, remainingDepth - 1);
+        }
+      }
+    }
+
+    recurse(root, depth);
+  }
+}
+
+class AddBenchmark extends PathSetBenchmark {
+  AddBenchmark() : super('add()');
+
+  final List<String> paths = [];
+
+  @override
+  void setup() {
+    // Make a bunch of paths in about the same order we expect to get them from
+    // Directory.list().
+    walkTree(3, paths.add);
+  }
+
+  @override
+  void run() {
+    for (var path in paths) {
+      pathSet.add(path);
+    }
+  }
+}
+
+class ContainsBenchmark extends PathSetBenchmark {
+  ContainsBenchmark() : super('contains()');
+
+  final List<String> paths = [];
+
+  @override
+  void setup() {
+    // Add a bunch of paths to the set.
+    walkTree(3, (path) {
+      pathSet.add(path);
+      paths.add(path);
+    });
+
+    // Add some non-existent paths to test the false case.
+    for (var i = 0; i < 100; i++) {
+      paths.addAll([
+        '/nope',
+        '/root/nope',
+        '/root/subdirectory_04/nope',
+        '/root/subdirectory_04/subdirectory_04/nope',
+        '/root/subdirectory_04/subdirectory_04/subdirectory_04/nope',
+        '/root/subdirectory_04/subdirectory_04/subdirectory_04/nope/file_04.txt',
+      ]);
+    }
+  }
+
+  @override
+  void run() {
+    var contained = 0;
+    for (var path in paths) {
+      if (pathSet.contains(path)) contained++;
+    }
+
+    if (contained != 10000) throw StateError('Wrong result: $contained');
+  }
+}
+
+class PathsBenchmark extends PathSetBenchmark {
+  PathsBenchmark() : super('toSet()');
+
+  @override
+  void setup() {
+    walkTree(3, pathSet.add);
+  }
+
+  @override
+  void run() {
+    var count = 0;
+    for (var _ in pathSet.paths) {
+      count++;
+    }
+
+    if (count != 10000) throw StateError('Wrong result: $count');
+  }
+}
+
+class RemoveBenchmark extends PathSetBenchmark {
+  RemoveBenchmark() : super('remove()');
+
+  final List<String> paths = [];
+
+  @override
+  void setup() {
+    // Make a bunch of paths. Do this here so that we don't spend benchmarked
+    // time synthesizing paths.
+    walkTree(3, (path) {
+      pathSet.add(path);
+      paths.add(path);
+    });
+
+    // Shuffle the paths so that we delete them in a random order that
+    // hopefully mimics real-world file system usage. Do the shuffling here so
+    // that we don't spend benchmarked time shuffling.
+    paths.shuffle(random);
+  }
+
+  @override
+  void run() {
+    for (var path in paths) {
+      pathSet.remove(path);
+    }
+  }
+}
+
+void main() {
+  AddBenchmark().report();
+  ContainsBenchmark().report();
+  PathsBenchmark().report();
+  RemoveBenchmark().report();
+}
diff --git a/pkgs/watcher/example/watch.dart b/pkgs/watcher/example/watch.dart
new file mode 100644
index 0000000..37931d3
--- /dev/null
+++ b/pkgs/watcher/example/watch.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Watches the given directory and prints each modification to it.
+library;
+
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+void main(List<String> arguments) {
+  if (arguments.length != 1) {
+    print('Usage: watch <directory path>');
+    return;
+  }
+
+  var watcher = DirectoryWatcher(p.absolute(arguments[0]));
+  watcher.events.listen(print);
+}
diff --git a/pkgs/watcher/lib/src/async_queue.dart b/pkgs/watcher/lib/src/async_queue.dart
new file mode 100644
index 0000000..f6c76a9
--- /dev/null
+++ b/pkgs/watcher/lib/src/async_queue.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:collection';
+
+typedef ItemProcessor<T> = Future<void> Function(T item);
+
+/// A queue of items that are sequentially, asynchronously processed.
+///
+/// Unlike [Stream.map] or [Stream.forEach], the callback used to process each
+/// item returns a [Future], and it will not advance to the next item until the
+/// current item is finished processing.
+///
+/// Items can be added at any point in time and processing will be started as
+/// needed. When all items are processed, it stops processing until more items
+/// are added.
+class AsyncQueue<T> {
+  final _items = Queue<T>();
+
+  /// Whether or not the queue is currently waiting on a processing future to
+  /// complete.
+  bool _isProcessing = false;
+
+  /// The callback to invoke on each queued item.
+  ///
+  /// The next item in the queue will not be processed until the [Future]
+  /// returned by this completes.
+  final ItemProcessor<T> _processor;
+
+  /// The handler for errors thrown during processing.
+  ///
+  /// Used to avoid top-leveling asynchronous errors.
+  final void Function(Object, StackTrace) _errorHandler;
+
+  AsyncQueue(this._processor,
+      {required void Function(Object, StackTrace) onError})
+      : _errorHandler = onError;
+
+  /// Enqueues [item] to be processed and starts asynchronously processing it
+  /// if a process isn't already running.
+  void add(T item) {
+    _items.add(item);
+
+    // Start up the asynchronous processing if not already running.
+    if (_isProcessing) return;
+    _isProcessing = true;
+
+    _processNextItem().catchError(_errorHandler);
+  }
+
+  /// Removes all remaining items to be processed.
+  void clear() {
+    _items.clear();
+  }
+
+  /// Pulls the next item off [_items] and processes it.
+  ///
+  /// When complete, recursively calls itself to continue processing unless
+  /// the process was cancelled.
+  Future<void> _processNextItem() async {
+    var item = _items.removeFirst();
+    await _processor(item);
+    if (_items.isNotEmpty) return _processNextItem();
+
+    // We have drained the queue, stop processing and wait until something
+    // has been enqueued.
+    _isProcessing = false;
+  }
+}
diff --git a/pkgs/watcher/lib/src/custom_watcher_factory.dart b/pkgs/watcher/lib/src/custom_watcher_factory.dart
new file mode 100644
index 0000000..fc4e3fb
--- /dev/null
+++ b/pkgs/watcher/lib/src/custom_watcher_factory.dart
@@ -0,0 +1,88 @@
+// 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 '../watcher.dart';
+
+/// A factory to produce custom watchers for specific file paths.
+class _CustomWatcherFactory {
+  final String id;
+  final DirectoryWatcher? Function(String path, {Duration? pollingDelay})
+      createDirectoryWatcher;
+  final FileWatcher? Function(String path, {Duration? pollingDelay})
+      createFileWatcher;
+
+  _CustomWatcherFactory(
+      this.id, this.createDirectoryWatcher, this.createFileWatcher);
+}
+
+/// Registers a custom watcher.
+///
+/// Each custom watcher must have a unique [id] and the same watcher may not be
+/// registered more than once.
+/// [createDirectoryWatcher] and [createFileWatcher] should return watchers for
+/// the file paths they are able to handle. If the custom watcher is not able to
+/// handle the path it should return null.
+/// The paths handled by each custom watch may not overlap, at most one custom
+/// matcher may return a non-null watcher for a given path.
+///
+/// When a file or directory watcher is created the path is checked against each
+/// registered custom watcher, and if exactly one custom watcher is available it
+/// will be used instead of the default.
+void registerCustomWatcher(
+  String id,
+  DirectoryWatcher? Function(String path, {Duration? pollingDelay})?
+      createDirectoryWatcher,
+  FileWatcher? Function(String path, {Duration? pollingDelay})?
+      createFileWatcher,
+) {
+  if (_customWatcherFactories.containsKey(id)) {
+    throw ArgumentError('A custom watcher with id `$id` '
+        'has already been registered');
+  }
+  _customWatcherFactories[id] = _CustomWatcherFactory(
+      id,
+      createDirectoryWatcher ?? (_, {pollingDelay}) => null,
+      createFileWatcher ?? (_, {pollingDelay}) => null);
+}
+
+/// Tries to create a custom [DirectoryWatcher] and returns it.
+///
+/// Returns `null` if no custom watcher was applicable and throws a [StateError]
+/// if more than one was.
+DirectoryWatcher? createCustomDirectoryWatcher(String path,
+    {Duration? pollingDelay}) {
+  DirectoryWatcher? customWatcher;
+  String? customFactoryId;
+  for (var watcherFactory in _customWatcherFactories.values) {
+    if (customWatcher != null) {
+      throw StateError('Two `CustomWatcherFactory`s applicable: '
+          '`$customFactoryId` and `${watcherFactory.id}` for `$path`');
+    }
+    customWatcher =
+        watcherFactory.createDirectoryWatcher(path, pollingDelay: pollingDelay);
+    customFactoryId = watcherFactory.id;
+  }
+  return customWatcher;
+}
+
+/// Tries to create a custom [FileWatcher] and returns it.
+///
+/// Returns `null` if no custom watcher was applicable and throws a [StateError]
+/// if more than one was.
+FileWatcher? createCustomFileWatcher(String path, {Duration? pollingDelay}) {
+  FileWatcher? customWatcher;
+  String? customFactoryId;
+  for (var watcherFactory in _customWatcherFactories.values) {
+    if (customWatcher != null) {
+      throw StateError('Two `CustomWatcherFactory`s applicable: '
+          '`$customFactoryId` and `${watcherFactory.id}` for `$path`');
+    }
+    customWatcher =
+        watcherFactory.createFileWatcher(path, pollingDelay: pollingDelay);
+    customFactoryId = watcherFactory.id;
+  }
+  return customWatcher;
+}
+
+final _customWatcherFactories = <String, _CustomWatcherFactory>{};
diff --git a/pkgs/watcher/lib/src/directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher.dart
new file mode 100644
index 0000000..158b86b
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher.dart
@@ -0,0 +1,41 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import '../watcher.dart';
+import 'custom_watcher_factory.dart';
+import 'directory_watcher/linux.dart';
+import 'directory_watcher/mac_os.dart';
+import 'directory_watcher/windows.dart';
+
+/// Watches the contents of a directory and emits [WatchEvent]s when something
+/// in the directory has changed.
+abstract class DirectoryWatcher implements Watcher {
+  /// The directory whose contents are being monitored.
+  @Deprecated('Expires in 1.0.0. Use DirectoryWatcher.path instead.')
+  String get directory;
+
+  /// Creates a new [DirectoryWatcher] monitoring [directory].
+  ///
+  /// If a native directory watcher is available for this platform, this will
+  /// use it. Otherwise, it will fall back to a [PollingDirectoryWatcher].
+  ///
+  /// If [pollingDelay] is passed, it specifies the amount of time the watcher
+  /// will pause between successive polls of the directory contents. Making this
+  /// shorter will give more immediate feedback at the expense of doing more IO
+  /// and higher CPU usage. Defaults to one second. Ignored for non-polling
+  /// watchers.
+  factory DirectoryWatcher(String directory, {Duration? pollingDelay}) {
+    if (FileSystemEntity.isWatchSupported) {
+      var customWatcher =
+          createCustomDirectoryWatcher(directory, pollingDelay: pollingDelay);
+      if (customWatcher != null) return customWatcher;
+      if (Platform.isLinux) return LinuxDirectoryWatcher(directory);
+      if (Platform.isMacOS) return MacOSDirectoryWatcher(directory);
+      if (Platform.isWindows) return WindowsDirectoryWatcher(directory);
+    }
+    return PollingDirectoryWatcher(directory, pollingDelay: pollingDelay);
+  }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/linux.dart b/pkgs/watcher/lib/src/directory_watcher/linux.dart
new file mode 100644
index 0000000..cb1d077
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/linux.dart
@@ -0,0 +1,294 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+
+import '../directory_watcher.dart';
+import '../path_set.dart';
+import '../resubscribable.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+/// Uses the inotify subsystem to watch for filesystem events.
+///
+/// Inotify doesn't suport recursively watching subdirectories, nor does
+/// [Directory.watch] polyfill that functionality. This class polyfills it
+/// instead.
+///
+/// This class also compensates for the non-inotify-specific issues of
+/// [Directory.watch] producing multiple events for a single logical action
+/// (issue 14372) and providing insufficient information about move events
+/// (issue 14424).
+class LinuxDirectoryWatcher extends ResubscribableWatcher
+    implements DirectoryWatcher {
+  @override
+  String get directory => path;
+
+  LinuxDirectoryWatcher(String directory)
+      : super(directory, () => _LinuxDirectoryWatcher(directory));
+}
+
+class _LinuxDirectoryWatcher
+    implements DirectoryWatcher, ManuallyClosedWatcher {
+  @override
+  String get directory => _files.root;
+  @override
+  String get path => _files.root;
+
+  @override
+  Stream<WatchEvent> get events => _eventsController.stream;
+  final _eventsController = StreamController<WatchEvent>.broadcast();
+
+  @override
+  bool get isReady => _readyCompleter.isCompleted;
+
+  @override
+  Future<void> get ready => _readyCompleter.future;
+  final _readyCompleter = Completer<void>();
+
+  /// A stream group for the [Directory.watch] events of [path] and all its
+  /// subdirectories.
+  final _nativeEvents = StreamGroup<FileSystemEvent>();
+
+  /// All known files recursively within [path].
+  final PathSet _files;
+
+  /// [Directory.watch] streams for [path]'s subdirectories, indexed by name.
+  ///
+  /// A stream is in this map if and only if it's also in [_nativeEvents].
+  final _subdirStreams = <String, Stream<FileSystemEvent>>{};
+
+  /// A set of all subscriptions that this watcher subscribes to.
+  ///
+  /// These are gathered together so that they may all be canceled when the
+  /// watcher is closed.
+  final _subscriptions = <StreamSubscription>{};
+
+  _LinuxDirectoryWatcher(String path) : _files = PathSet(path) {
+    _nativeEvents.add(Directory(path)
+        .watch()
+        .transform(StreamTransformer.fromHandlers(handleDone: (sink) {
+      // Handle the done event here rather than in the call to [_listen] because
+      // [innerStream] won't close until we close the [StreamGroup]. However, if
+      // we close the [StreamGroup] here, we run the risk of new-directory
+      // events being fired after the group is closed, since batching delays
+      // those events. See b/30768513.
+      _onDone();
+    })));
+
+    // Batch the inotify changes together so that we can dedup events.
+    var innerStream = _nativeEvents.stream.batchEvents();
+    _listen(innerStream, _onBatch,
+        onError: (Object error, StackTrace stackTrace) {
+      // Guarantee that ready always completes.
+      if (!isReady) {
+        _readyCompleter.complete();
+      }
+      _eventsController.addError(error, stackTrace);
+    });
+
+    _listen(
+      Directory(path).list(recursive: true),
+      (FileSystemEntity entity) {
+        if (entity is Directory) {
+          _watchSubdir(entity.path);
+        } else {
+          _files.add(entity.path);
+        }
+      },
+      onError: _emitError,
+      onDone: () {
+        if (!isReady) {
+          _readyCompleter.complete();
+        }
+      },
+      cancelOnError: true,
+    );
+  }
+
+  @override
+  void close() {
+    for (var subscription in _subscriptions) {
+      subscription.cancel();
+    }
+
+    _subscriptions.clear();
+    _subdirStreams.clear();
+    _files.clear();
+    _nativeEvents.close();
+    _eventsController.close();
+  }
+
+  /// Watch a subdirectory of [directory] for changes.
+  void _watchSubdir(String path) {
+    // TODO(nweiz): Right now it's possible for the watcher to emit an event for
+    // a file before the directory list is complete. This could lead to the user
+    // seeing a MODIFY or REMOVE event for a file before they see an ADD event,
+    // which is bad. We should handle that.
+    //
+    // One possibility is to provide a general means (e.g.
+    // `DirectoryWatcher.eventsAndExistingFiles`) to tell a watcher to emit
+    // events for all the files that already exist. This would be useful for
+    // top-level clients such as barback as well, and could be implemented with
+    // a wrapper similar to how listening/canceling works now.
+
+    // TODO(nweiz): Catch any errors here that indicate that the directory in
+    // question doesn't exist and silently stop watching it instead of
+    // propagating the errors.
+    var stream = Directory(path).watch();
+    _subdirStreams[path] = stream;
+    _nativeEvents.add(stream);
+  }
+
+  /// The callback that's run when a batch of changes comes in.
+  void _onBatch(List<FileSystemEvent> batch) {
+    var files = <String>{};
+    var dirs = <String>{};
+    var changed = <String>{};
+
+    // inotify event batches are ordered by occurrence, so we treat them as a
+    // log of what happened to a file. We only emit events based on the
+    // difference between the state before the batch and the state after it, not
+    // the intermediate state.
+    for (var event in batch) {
+      // If the watched directory is deleted or moved, we'll get a deletion
+      // event for it. Ignore it; we handle closing [this] when the underlying
+      // stream is closed.
+      if (event.path == path) continue;
+
+      changed.add(event.path);
+
+      if (event is FileSystemMoveEvent) {
+        files.remove(event.path);
+        dirs.remove(event.path);
+
+        var destination = event.destination;
+        if (destination == null) continue;
+
+        changed.add(destination);
+        if (event.isDirectory) {
+          files.remove(destination);
+          dirs.add(destination);
+        } else {
+          files.add(destination);
+          dirs.remove(destination);
+        }
+      } else if (event is FileSystemDeleteEvent) {
+        files.remove(event.path);
+        dirs.remove(event.path);
+      } else if (event.isDirectory) {
+        files.remove(event.path);
+        dirs.add(event.path);
+      } else {
+        files.add(event.path);
+        dirs.remove(event.path);
+      }
+    }
+
+    _applyChanges(files, dirs, changed);
+  }
+
+  /// Applies the net changes computed for a batch.
+  ///
+  /// The [files] and [dirs] sets contain the files and directories that now
+  /// exist, respectively. The [changed] set contains all files and directories
+  /// that have changed (including being removed), and so is a superset of
+  /// [files] and [dirs].
+  void _applyChanges(Set<String> files, Set<String> dirs, Set<String> changed) {
+    for (var path in changed) {
+      var stream = _subdirStreams.remove(path);
+      if (stream != null) _nativeEvents.add(stream);
+
+      // Unless [path] was a file and still is, emit REMOVE events for it or its
+      // contents,
+      if (files.contains(path) && _files.contains(path)) continue;
+      for (var file in _files.remove(path)) {
+        _emitEvent(ChangeType.REMOVE, file);
+      }
+    }
+
+    for (var file in files) {
+      if (_files.contains(file)) {
+        _emitEvent(ChangeType.MODIFY, file);
+      } else {
+        _emitEvent(ChangeType.ADD, file);
+        _files.add(file);
+      }
+    }
+
+    for (var dir in dirs) {
+      _watchSubdir(dir);
+      _addSubdir(dir);
+    }
+  }
+
+  /// Emits [ChangeType.ADD] events for the recursive contents of [path].
+  void _addSubdir(String path) {
+    _listen(Directory(path).list(recursive: true), (FileSystemEntity entity) {
+      if (entity is Directory) {
+        _watchSubdir(entity.path);
+      } else {
+        _files.add(entity.path);
+        _emitEvent(ChangeType.ADD, entity.path);
+      }
+    }, onError: (Object error, StackTrace stackTrace) {
+      // Ignore an exception caused by the dir not existing. It's fine if it
+      // was added and then quickly removed.
+      if (error is FileSystemException) return;
+
+      _emitError(error, stackTrace);
+    }, cancelOnError: true);
+  }
+
+  /// Handles the underlying event stream closing, indicating that the directory
+  /// being watched was removed.
+  void _onDone() {
+    // Most of the time when a directory is removed, its contents will get
+    // individual REMOVE events before the watch stream is closed -- in that
+    // case, [_files] will be empty here. However, if the directory's removal is
+    // caused by a MOVE, we need to manually emit events.
+    if (isReady) {
+      for (var file in _files.paths) {
+        _emitEvent(ChangeType.REMOVE, file);
+      }
+    }
+
+    close();
+  }
+
+  /// Emits a [WatchEvent] with [type] and [path] if this watcher is in a state
+  /// to emit events.
+  void _emitEvent(ChangeType type, String path) {
+    if (!isReady) return;
+    if (_eventsController.isClosed) return;
+    _eventsController.add(WatchEvent(type, path));
+  }
+
+  /// Emit an error, then close the watcher.
+  void _emitError(Object error, StackTrace stackTrace) {
+    // Guarantee that ready always completes.
+    if (!isReady) {
+      _readyCompleter.complete();
+    }
+    _eventsController.addError(error, stackTrace);
+    close();
+  }
+
+  /// Like [Stream.listen], but automatically adds the subscription to
+  /// [_subscriptions] so that it can be canceled when [close] is called.
+  void _listen<T>(Stream<T> stream, void Function(T) onData,
+      {Function? onError,
+      void Function()? onDone,
+      bool cancelOnError = false}) {
+    late StreamSubscription<T> subscription;
+    subscription = stream.listen(onData, onError: onError, onDone: () {
+      _subscriptions.remove(subscription);
+      onDone?.call();
+    }, cancelOnError: cancelOnError);
+    _subscriptions.add(subscription);
+  }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart
new file mode 100644
index 0000000..b461383
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart
@@ -0,0 +1,410 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import '../directory_watcher.dart';
+import '../path_set.dart';
+import '../resubscribable.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+/// Uses the FSEvents subsystem to watch for filesystem events.
+///
+/// FSEvents has two main idiosyncrasies that this class works around. First, it
+/// will occasionally report events that occurred before the filesystem watch
+/// was initiated. Second, if multiple events happen to the same file in close
+/// succession, it won't report them in the order they occurred. See issue
+/// 14373.
+///
+/// This also works around issues 16003 and 14849 in the implementation of
+/// [Directory.watch].
+class MacOSDirectoryWatcher extends ResubscribableWatcher
+    implements DirectoryWatcher {
+  @override
+  String get directory => path;
+
+  MacOSDirectoryWatcher(String directory)
+      : super(directory, () => _MacOSDirectoryWatcher(directory));
+}
+
+class _MacOSDirectoryWatcher
+    implements DirectoryWatcher, ManuallyClosedWatcher {
+  @override
+  String get directory => path;
+  @override
+  final String path;
+
+  @override
+  Stream<WatchEvent> get events => _eventsController.stream;
+  final _eventsController = StreamController<WatchEvent>.broadcast();
+
+  @override
+  bool get isReady => _readyCompleter.isCompleted;
+
+  @override
+  Future<void> get ready => _readyCompleter.future;
+  final _readyCompleter = Completer<void>();
+
+  /// The set of files that are known to exist recursively within the watched
+  /// directory.
+  ///
+  /// The state of files on the filesystem is compared against this to determine
+  /// the real change that occurred when working around issue 14373. This is
+  /// also used to emit REMOVE events when subdirectories are moved out of the
+  /// watched directory.
+  final PathSet _files;
+
+  /// The subscription to the stream returned by [Directory.watch].
+  ///
+  /// This is separate from [_listSubscriptions] because this stream
+  /// occasionally needs to be resubscribed in order to work around issue 14849.
+  StreamSubscription<List<FileSystemEvent>>? _watchSubscription;
+
+  /// The subscription to the [Directory.list] call for the initial listing of
+  /// the directory to determine its initial state.
+  StreamSubscription<FileSystemEntity>? _initialListSubscription;
+
+  /// The subscriptions to [Directory.list] calls for listing the contents of a
+  /// subdirectory that was moved into the watched directory.
+  final _listSubscriptions = <StreamSubscription<FileSystemEntity>>{};
+
+  /// The timer for tracking how long we wait for an initial batch of bogus
+  /// events (see issue 14373).
+  late Timer _bogusEventTimer;
+
+  _MacOSDirectoryWatcher(this.path) : _files = PathSet(path) {
+    _startWatch();
+
+    // Before we're ready to emit events, wait for [_listDir] to complete and
+    // for enough time to elapse that if bogus events (issue 14373) would be
+    // emitted, they will be.
+    //
+    // If we do receive a batch of events, [_onBatch] will ensure that these
+    // futures don't fire and that the directory is re-listed.
+    Future.wait([_listDir(), _waitForBogusEvents()]).then((_) {
+      if (!isReady) {
+        _readyCompleter.complete();
+      }
+    });
+  }
+
+  @override
+  void close() {
+    _watchSubscription?.cancel();
+    _initialListSubscription?.cancel();
+    _watchSubscription = null;
+    _initialListSubscription = null;
+
+    for (var subscription in _listSubscriptions) {
+      subscription.cancel();
+    }
+    _listSubscriptions.clear();
+
+    _eventsController.close();
+  }
+
+  /// The callback that's run when [Directory.watch] emits a batch of events.
+  void _onBatch(List<FileSystemEvent> batch) {
+    // If we get a batch of events before we're ready to begin emitting events,
+    // it's probable that it's a batch of pre-watcher events (see issue 14373).
+    // Ignore those events and re-list the directory.
+    if (!isReady) {
+      // Cancel the timer because bogus events only occur in the first batch, so
+      // we can fire [ready] as soon as we're done listing the directory.
+      _bogusEventTimer.cancel();
+      _listDir().then((_) {
+        if (!isReady) {
+          _readyCompleter.complete();
+        }
+      });
+      return;
+    }
+
+    _sortEvents(batch).forEach((path, eventSet) {
+      var canonicalEvent = _canonicalEvent(eventSet);
+      var events = canonicalEvent == null
+          ? _eventsBasedOnFileSystem(path)
+          : [canonicalEvent];
+
+      for (var event in events) {
+        if (event is FileSystemCreateEvent) {
+          if (!event.isDirectory) {
+            // If we already know about the file, treat it like a modification.
+            // This can happen if a file is copied on top of an existing one.
+            // We'll see an ADD event for the latter file when from the user's
+            // perspective, the file's contents just changed.
+            var type =
+                _files.contains(path) ? ChangeType.MODIFY : ChangeType.ADD;
+
+            _emitEvent(type, path);
+            _files.add(path);
+            continue;
+          }
+
+          if (_files.containsDir(path)) continue;
+
+          var stream = Directory(path).list(recursive: true);
+          var subscription = stream.listen((entity) {
+            if (entity is Directory) return;
+            if (_files.contains(path)) return;
+
+            _emitEvent(ChangeType.ADD, entity.path);
+            _files.add(entity.path);
+          }, cancelOnError: true);
+          subscription.onDone(() {
+            _listSubscriptions.remove(subscription);
+          });
+          subscription.onError(_emitError);
+          _listSubscriptions.add(subscription);
+        } else if (event is FileSystemModifyEvent) {
+          assert(!event.isDirectory);
+          _emitEvent(ChangeType.MODIFY, path);
+        } else {
+          assert(event is FileSystemDeleteEvent);
+          for (var removedPath in _files.remove(path)) {
+            _emitEvent(ChangeType.REMOVE, removedPath);
+          }
+        }
+      }
+    });
+  }
+
+  /// Sort all the events in a batch into sets based on their path.
+  ///
+  /// A single input event may result in multiple events in the returned map;
+  /// for example, a MOVE event becomes a DELETE event for the source and a
+  /// CREATE event for the destination.
+  ///
+  /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
+  /// contain any events relating to [path].
+  Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
+    var eventsForPaths = <String, Set<FileSystemEvent>>{};
+
+    // FSEvents can report past events, including events on the root directory
+    // such as it being created. We want to ignore these. If the directory is
+    // really deleted, that's handled by [_onDone].
+    batch = batch.where((event) => event.path != path).toList();
+
+    // Events within directories that already have events are superfluous; the
+    // directory's full contents will be examined anyway, so we ignore such
+    // events. Emitting them could cause useless or out-of-order events.
+    var directories = unionAll(batch.map((event) {
+      if (!event.isDirectory) return <String>{};
+      if (event is FileSystemMoveEvent) {
+        var destination = event.destination;
+        if (destination != null) {
+          return {event.path, destination};
+        }
+      }
+      return {event.path};
+    }));
+
+    bool isInModifiedDirectory(String path) =>
+        directories.any((dir) => path != dir && p.isWithin(dir, path));
+
+    void addEvent(String path, FileSystemEvent event) {
+      if (isInModifiedDirectory(path)) return;
+      eventsForPaths.putIfAbsent(path, () => <FileSystemEvent>{}).add(event);
+    }
+
+    for (var event in batch) {
+      // The Mac OS watcher doesn't emit move events. See issue 14806.
+      assert(event is! FileSystemMoveEvent);
+      addEvent(event.path, event);
+    }
+
+    return eventsForPaths;
+  }
+
+  /// Returns the canonical event from a batch of events on the same path, if
+  /// one exists.
+  ///
+  /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
+  /// CREATE, or events with different values for `isDirectory`), this returns a
+  /// single event that describes what happened to the path in question.
+  ///
+  /// If [batch] does contain contradictory events, this returns `null` to
+  /// indicate that the state of the path on the filesystem should be checked to
+  /// determine what occurred.
+  FileSystemEvent? _canonicalEvent(Set<FileSystemEvent> batch) {
+    // An empty batch indicates that we've learned earlier that the batch is
+    // contradictory (e.g. because of a move).
+    if (batch.isEmpty) return null;
+
+    var type = batch.first.type;
+    var isDir = batch.first.isDirectory;
+    var hadModifyEvent = false;
+
+    for (var event in batch.skip(1)) {
+      // If one event reports that the file is a directory and another event
+      // doesn't, that's a contradiction.
+      if (isDir != event.isDirectory) return null;
+
+      // Modify events don't contradict either CREATE or REMOVE events. We can
+      // safely assume the file was modified after a CREATE or before the
+      // REMOVE; otherwise there will also be a REMOVE or CREATE event
+      // (respectively) that will be contradictory.
+      if (event is FileSystemModifyEvent) {
+        hadModifyEvent = true;
+        continue;
+      }
+      assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
+
+      // If we previously thought this was a MODIFY, we now consider it to be a
+      // CREATE or REMOVE event. This is safe for the same reason as above.
+      if (type == FileSystemEvent.modify) {
+        type = event.type;
+        continue;
+      }
+
+      // A CREATE event contradicts a REMOVE event and vice versa.
+      assert(type == FileSystemEvent.create || type == FileSystemEvent.delete);
+      if (type != event.type) return null;
+    }
+
+    // If we got a CREATE event for a file we already knew about, that comes
+    // from FSEvents reporting an add that happened prior to the watch
+    // beginning. If we also received a MODIFY event, we want to report that,
+    // but not the CREATE.
+    if (type == FileSystemEvent.create &&
+        hadModifyEvent &&
+        _files.contains(batch.first.path)) {
+      type = FileSystemEvent.modify;
+    }
+
+    switch (type) {
+      case FileSystemEvent.create:
+        // Issue 16003 means that a CREATE event for a directory can indicate
+        // that the directory was moved and then re-created.
+        // [_eventsBasedOnFileSystem] will handle this correctly by producing a
+        // DELETE event followed by a CREATE event if the directory exists.
+        if (isDir) return null;
+        return FileSystemCreateEvent(batch.first.path, false);
+      case FileSystemEvent.delete:
+        return FileSystemDeleteEvent(batch.first.path, isDir);
+      case FileSystemEvent.modify:
+        return FileSystemModifyEvent(batch.first.path, isDir, false);
+      default:
+        throw StateError('unreachable');
+    }
+  }
+
+  /// Returns one or more events that describe the change between the last known
+  /// state of [path] and its current state on the filesystem.
+  ///
+  /// This returns a list whose order should be reflected in the events emitted
+  /// to the user, unlike the batched events from [Directory.watch]. The
+  /// returned list may be empty, indicating that no changes occurred to [path]
+  /// (probably indicating that it was created and then immediately deleted).
+  List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
+    var fileExisted = _files.contains(path);
+    var dirExisted = _files.containsDir(path);
+    var fileExists = File(path).existsSync();
+    var dirExists = Directory(path).existsSync();
+
+    var events = <FileSystemEvent>[];
+    if (fileExisted) {
+      if (fileExists) {
+        events.add(FileSystemModifyEvent(path, false, false));
+      } else {
+        events.add(FileSystemDeleteEvent(path, false));
+      }
+    } else if (dirExisted) {
+      if (dirExists) {
+        // If we got contradictory events for a directory that used to exist and
+        // still exists, we need to rescan the whole thing in case it was
+        // replaced with a different directory.
+        events.add(FileSystemDeleteEvent(path, true));
+        events.add(FileSystemCreateEvent(path, true));
+      } else {
+        events.add(FileSystemDeleteEvent(path, true));
+      }
+    }
+
+    if (!fileExisted && fileExists) {
+      events.add(FileSystemCreateEvent(path, false));
+    } else if (!dirExisted && dirExists) {
+      events.add(FileSystemCreateEvent(path, true));
+    }
+
+    return events;
+  }
+
+  /// The callback that's run when the [Directory.watch] stream is closed.
+  void _onDone() {
+    _watchSubscription = null;
+
+    // If the directory still exists and we're still expecting bogus events,
+    // this is probably issue 14849 rather than a real close event. We should
+    // just restart the watcher.
+    if (!isReady && Directory(path).existsSync()) {
+      _startWatch();
+      return;
+    }
+
+    // FSEvents can fail to report the contents of the directory being removed
+    // when the directory itself is removed, so we need to manually mark the
+    // files as removed.
+    for (var file in _files.paths) {
+      _emitEvent(ChangeType.REMOVE, file);
+    }
+    _files.clear();
+    close();
+  }
+
+  /// Start or restart the underlying [Directory.watch] stream.
+  void _startWatch() {
+    // Batch the FSEvent changes together so that we can dedup events.
+    var innerStream = Directory(path).watch(recursive: true).batchEvents();
+    _watchSubscription = innerStream.listen(_onBatch,
+        onError: _eventsController.addError, onDone: _onDone);
+  }
+
+  /// Starts or restarts listing the watched directory to get an initial picture
+  /// of its state.
+  Future<void> _listDir() {
+    assert(!isReady);
+    _initialListSubscription?.cancel();
+
+    _files.clear();
+    var completer = Completer<void>();
+    var stream = Directory(path).list(recursive: true);
+    _initialListSubscription = stream.listen((entity) {
+      if (entity is! Directory) _files.add(entity.path);
+    }, onError: _emitError, onDone: completer.complete, cancelOnError: true);
+    return completer.future;
+  }
+
+  /// Wait 200ms for a batch of bogus events (issue 14373) to come in.
+  ///
+  /// 200ms is short in terms of human interaction, but longer than any Mac OS
+  /// watcher tests take on the bots, so it should be safe to assume that any
+  /// bogus events will be signaled in that time frame.
+  Future<void> _waitForBogusEvents() {
+    var completer = Completer<void>();
+    _bogusEventTimer =
+        Timer(const Duration(milliseconds: 200), completer.complete);
+    return completer.future;
+  }
+
+  /// Emit an event with the given [type] and [path].
+  void _emitEvent(ChangeType type, String path) {
+    if (!isReady) return;
+    _eventsController.add(WatchEvent(type, path));
+  }
+
+  /// Emit an error, then close the watcher.
+  void _emitError(Object error, StackTrace stackTrace) {
+    // Guarantee that ready always completes.
+    if (!isReady) {
+      _readyCompleter.complete();
+    }
+    _eventsController.addError(error, stackTrace);
+    close();
+  }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/polling.dart b/pkgs/watcher/lib/src/directory_watcher/polling.dart
new file mode 100644
index 0000000..207679b
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/polling.dart
@@ -0,0 +1,191 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import '../async_queue.dart';
+import '../directory_watcher.dart';
+import '../resubscribable.dart';
+import '../stat.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+/// Periodically polls a directory for changes.
+class PollingDirectoryWatcher extends ResubscribableWatcher
+    implements DirectoryWatcher {
+  @override
+  String get directory => path;
+
+  /// Creates a new polling watcher monitoring [directory].
+  ///
+  /// If [pollingDelay] is passed, it specifies the amount of time the watcher
+  /// will pause between successive polls of the directory contents. Making this
+  /// shorter will give more immediate feedback at the expense of doing more IO
+  /// and higher CPU usage. Defaults to one second.
+  PollingDirectoryWatcher(String directory, {Duration? pollingDelay})
+      : super(directory, () {
+          return _PollingDirectoryWatcher(
+              directory, pollingDelay ?? const Duration(seconds: 1));
+        });
+}
+
+class _PollingDirectoryWatcher
+    implements DirectoryWatcher, ManuallyClosedWatcher {
+  @override
+  String get directory => path;
+  @override
+  final String path;
+
+  @override
+  Stream<WatchEvent> get events => _events.stream;
+  final _events = StreamController<WatchEvent>.broadcast();
+
+  @override
+  bool get isReady => _readyCompleter.isCompleted;
+
+  @override
+  Future<void> get ready => _readyCompleter.future;
+  final _readyCompleter = Completer<void>();
+
+  /// The amount of time the watcher pauses between successive polls of the
+  /// directory contents.
+  final Duration _pollingDelay;
+
+  /// The previous modification times of the files in the directory.
+  ///
+  /// Used to tell which files have been modified.
+  final _lastModifieds = <String, DateTime?>{};
+
+  /// The subscription used while [directory] is being listed.
+  ///
+  /// Will be `null` if a list is not currently happening.
+  StreamSubscription<FileSystemEntity>? _listSubscription;
+
+  /// The queue of files waiting to be processed to see if they have been
+  /// modified.
+  ///
+  /// Processing a file is asynchronous, as is listing the directory, so the
+  /// queue exists to let each of those proceed at their own rate. The lister
+  /// will enqueue files as quickly as it can. Meanwhile, files are dequeued
+  /// and processed sequentially.
+  late final AsyncQueue<String?> _filesToProcess =
+      AsyncQueue<String?>(_processFile, onError: (error, stackTrace) {
+    if (!_events.isClosed) _events.addError(error, stackTrace);
+  });
+
+  /// The set of files that have been seen in the current directory listing.
+  ///
+  /// Used to tell which files have been removed: files that are in
+  /// [_lastModifieds] but not in here when a poll completes have been removed.
+  final _polledFiles = <String>{};
+
+  _PollingDirectoryWatcher(this.path, this._pollingDelay) {
+    _poll();
+  }
+
+  @override
+  void close() {
+    _events.close();
+
+    // If we're in the middle of listing the directory, stop.
+    _listSubscription?.cancel();
+
+    // Don't process any remaining files.
+    _filesToProcess.clear();
+    _polledFiles.clear();
+    _lastModifieds.clear();
+  }
+
+  /// Scans the contents of the directory once to see which files have been
+  /// added, removed, and modified.
+  void _poll() {
+    _filesToProcess.clear();
+    _polledFiles.clear();
+
+    void endListing() {
+      assert(!_events.isClosed);
+      _listSubscription = null;
+
+      // Null tells the queue consumer that we're done listing.
+      _filesToProcess.add(null);
+    }
+
+    var stream = Directory(path).list(recursive: true);
+    _listSubscription = stream.listen((entity) {
+      assert(!_events.isClosed);
+
+      if (entity is! File) return;
+      _filesToProcess.add(entity.path);
+    }, onError: (Object error, StackTrace stackTrace) {
+      // Guarantee that ready always completes.
+      if (!isReady) {
+        _readyCompleter.complete();
+      }
+      if (!isDirectoryNotFoundException(error)) {
+        // It's some unknown error. Pipe it over to the event stream so the
+        // user can see it.
+        _events.addError(error, stackTrace);
+      }
+
+      // When an error occurs, we end the listing normally, which has the
+      // desired effect of marking all files that were in the directory as
+      // being removed.
+      endListing();
+    }, onDone: endListing, cancelOnError: true);
+  }
+
+  /// Processes [file] to determine if it has been modified since the last
+  /// time it was scanned.
+  Future<void> _processFile(String? file) async {
+    // `null` is the sentinel which means the directory listing is complete.
+    if (file == null) {
+      await _completePoll();
+      return;
+    }
+
+    final modified = await modificationTime(file);
+
+    if (_events.isClosed) return;
+
+    var lastModified = _lastModifieds[file];
+
+    // If its modification time hasn't changed, assume the file is unchanged.
+    if (lastModified != null && lastModified == modified) {
+      // The file is still here.
+      _polledFiles.add(file);
+      return;
+    }
+
+    if (_events.isClosed) return;
+
+    _lastModifieds[file] = modified;
+    _polledFiles.add(file);
+
+    // Only notify if we're ready to emit events.
+    if (!isReady) return;
+
+    var type = lastModified == null ? ChangeType.ADD : ChangeType.MODIFY;
+    _events.add(WatchEvent(type, file));
+  }
+
+  /// After the directory listing is complete, this determines which files were
+  /// removed and then restarts the next poll.
+  Future<void> _completePoll() async {
+    // Any files that were not seen in the last poll but that we have a
+    // status for must have been removed.
+    var removedFiles = _lastModifieds.keys.toSet().difference(_polledFiles);
+    for (var removed in removedFiles) {
+      if (isReady) _events.add(WatchEvent(ChangeType.REMOVE, removed));
+      _lastModifieds.remove(removed);
+    }
+
+    if (!isReady) _readyCompleter.complete();
+
+    // Wait and then poll again.
+    await Future<void>.delayed(_pollingDelay);
+    if (_events.isClosed) return;
+    _poll();
+  }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/windows.dart b/pkgs/watcher/lib/src/directory_watcher/windows.dart
new file mode 100644
index 0000000..d1c98be
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/windows.dart
@@ -0,0 +1,437 @@
+// 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.
+// TODO(rnystrom): Merge with mac_os version.
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import '../directory_watcher.dart';
+import '../path_set.dart';
+import '../resubscribable.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+class WindowsDirectoryWatcher extends ResubscribableWatcher
+    implements DirectoryWatcher {
+  @override
+  String get directory => path;
+
+  WindowsDirectoryWatcher(String directory)
+      : super(directory, () => _WindowsDirectoryWatcher(directory));
+}
+
+class _EventBatcher {
+  static const Duration _batchDelay = Duration(milliseconds: 100);
+  final List<FileSystemEvent> events = [];
+  Timer? timer;
+
+  void addEvent(FileSystemEvent event, void Function() callback) {
+    events.add(event);
+    timer?.cancel();
+    timer = Timer(_batchDelay, callback);
+  }
+
+  void cancelTimer() {
+    timer?.cancel();
+  }
+}
+
+class _WindowsDirectoryWatcher
+    implements DirectoryWatcher, ManuallyClosedWatcher {
+  @override
+  String get directory => path;
+  @override
+  final String path;
+
+  @override
+  Stream<WatchEvent> get events => _eventsController.stream;
+  final _eventsController = StreamController<WatchEvent>.broadcast();
+
+  @override
+  bool get isReady => _readyCompleter.isCompleted;
+
+  @override
+  Future<void> get ready => _readyCompleter.future;
+  final _readyCompleter = Completer<void>();
+
+  final Map<String, _EventBatcher> _eventBatchers =
+      HashMap<String, _EventBatcher>();
+
+  /// The set of files that are known to exist recursively within the watched
+  /// directory.
+  ///
+  /// The state of files on the filesystem is compared against this to determine
+  /// the real change that occurred. This is also used to emit REMOVE events
+  /// when subdirectories are moved out of the watched directory.
+  final PathSet _files;
+
+  /// The subscription to the stream returned by [Directory.watch].
+  StreamSubscription<FileSystemEvent>? _watchSubscription;
+
+  /// The subscription to the stream returned by [Directory.watch] of the
+  /// parent directory to [directory]. This is needed to detect changes to
+  /// [directory], as they are not included on Windows.
+  StreamSubscription<FileSystemEvent>? _parentWatchSubscription;
+
+  /// The subscription to the [Directory.list] call for the initial listing of
+  /// the directory to determine its initial state.
+  StreamSubscription<FileSystemEntity>? _initialListSubscription;
+
+  /// The subscriptions to the [Directory.list] calls for listing the contents
+  /// of subdirectories that were moved into the watched directory.
+  final Set<StreamSubscription<FileSystemEntity>> _listSubscriptions =
+      HashSet<StreamSubscription<FileSystemEntity>>();
+
+  _WindowsDirectoryWatcher(this.path) : _files = PathSet(path) {
+    // Before we're ready to emit events, wait for [_listDir] to complete.
+    _listDir().then((_) {
+      _startWatch();
+      _startParentWatcher();
+      if (!isReady) {
+        _readyCompleter.complete();
+      }
+    });
+  }
+
+  @override
+  void close() {
+    _watchSubscription?.cancel();
+    _parentWatchSubscription?.cancel();
+    _initialListSubscription?.cancel();
+    for (var sub in _listSubscriptions) {
+      sub.cancel();
+    }
+    _listSubscriptions.clear();
+    for (var batcher in _eventBatchers.values) {
+      batcher.cancelTimer();
+    }
+    _eventBatchers.clear();
+    _watchSubscription = null;
+    _parentWatchSubscription = null;
+    _initialListSubscription = null;
+    _eventsController.close();
+  }
+
+  /// On Windows, if [directory] is deleted, we will not receive any event.
+  ///
+  /// Instead, we add a watcher on the parent folder (if any), that can notify
+  /// us about [path]. This also includes events such as moves.
+  void _startParentWatcher() {
+    var absoluteDir = p.absolute(path);
+    var parent = p.dirname(absoluteDir);
+    // Check if [path] is already the root directory.
+    if (FileSystemEntity.identicalSync(parent, path)) return;
+    var parentStream = Directory(parent).watch(recursive: false);
+    _parentWatchSubscription = parentStream.listen((event) {
+      // Only look at events for 'directory'.
+      if (p.basename(event.path) != p.basename(absoluteDir)) return;
+      // Test if the directory is removed. FileSystemEntity.typeSync will
+      // return NOT_FOUND if it's unable to decide upon the type, including
+      // access denied issues, which may happen when the directory is deleted.
+      // FileSystemMoveEvent and FileSystemDeleteEvent events will always mean
+      // the directory is now gone.
+      if (event is FileSystemMoveEvent ||
+          event is FileSystemDeleteEvent ||
+          (FileSystemEntity.typeSync(path) == FileSystemEntityType.notFound)) {
+        for (var path in _files.paths) {
+          _emitEvent(ChangeType.REMOVE, path);
+        }
+        _files.clear();
+        close();
+      }
+    }, onError: (error) {
+      // Ignore errors, simply close the stream. The user listens on
+      // [directory], and while it can fail to listen on the parent, we may
+      // still be able to listen on the path requested.
+      _parentWatchSubscription?.cancel();
+      _parentWatchSubscription = null;
+    });
+  }
+
+  void _onEvent(FileSystemEvent event) {
+    assert(isReady);
+    final batcher = _eventBatchers.putIfAbsent(event.path, _EventBatcher.new);
+    batcher.addEvent(event, () {
+      _eventBatchers.remove(event.path);
+      _onBatch(batcher.events);
+    });
+  }
+
+  /// The callback that's run when [Directory.watch] emits a batch of events.
+  void _onBatch(List<FileSystemEvent> batch) {
+    _sortEvents(batch).forEach((path, eventSet) {
+      var canonicalEvent = _canonicalEvent(eventSet);
+      var events = canonicalEvent == null
+          ? _eventsBasedOnFileSystem(path)
+          : [canonicalEvent];
+
+      for (var event in events) {
+        if (event is FileSystemCreateEvent) {
+          if (!event.isDirectory) {
+            if (_files.contains(path)) continue;
+
+            _emitEvent(ChangeType.ADD, path);
+            _files.add(path);
+            continue;
+          }
+
+          if (_files.containsDir(path)) continue;
+
+          var stream = Directory(path).list(recursive: true);
+          var subscription = stream.listen((entity) {
+            if (entity is Directory) return;
+            if (_files.contains(path)) return;
+
+            _emitEvent(ChangeType.ADD, entity.path);
+            _files.add(entity.path);
+          }, cancelOnError: true);
+          subscription.onDone(() {
+            _listSubscriptions.remove(subscription);
+          });
+          subscription.onError((Object e, StackTrace stackTrace) {
+            _listSubscriptions.remove(subscription);
+            _emitError(e, stackTrace);
+          });
+          _listSubscriptions.add(subscription);
+        } else if (event is FileSystemModifyEvent) {
+          if (!event.isDirectory) {
+            _emitEvent(ChangeType.MODIFY, path);
+          }
+        } else {
+          assert(event is FileSystemDeleteEvent);
+          for (var removedPath in _files.remove(path)) {
+            _emitEvent(ChangeType.REMOVE, removedPath);
+          }
+        }
+      }
+    });
+  }
+
+  /// Sort all the events in a batch into sets based on their path.
+  ///
+  /// A single input event may result in multiple events in the returned map;
+  /// for example, a MOVE event becomes a DELETE event for the source and a
+  /// CREATE event for the destination.
+  ///
+  /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
+  /// contain any events relating to [path].
+  Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
+    var eventsForPaths = <String, Set<FileSystemEvent>>{};
+
+    // Events within directories that already have events are superfluous; the
+    // directory's full contents will be examined anyway, so we ignore such
+    // events. Emitting them could cause useless or out-of-order events.
+    var directories = unionAll(batch.map((event) {
+      if (!event.isDirectory) return <String>{};
+      if (event is FileSystemMoveEvent) {
+        var destination = event.destination;
+        if (destination != null) {
+          return {event.path, destination};
+        }
+      }
+      return {event.path};
+    }));
+
+    bool isInModifiedDirectory(String path) =>
+        directories.any((dir) => path != dir && p.isWithin(dir, path));
+
+    void addEvent(String path, FileSystemEvent event) {
+      if (isInModifiedDirectory(path)) return;
+      eventsForPaths.putIfAbsent(path, () => <FileSystemEvent>{}).add(event);
+    }
+
+    for (var event in batch) {
+      if (event is FileSystemMoveEvent) {
+        var destination = event.destination;
+        if (destination != null) {
+          addEvent(destination, event);
+        }
+      }
+      addEvent(event.path, event);
+    }
+
+    return eventsForPaths;
+  }
+
+  /// Returns the canonical event from a batch of events on the same path, if
+  /// one exists.
+  ///
+  /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
+  /// CREATE, or events with different values for `isDirectory`), this returns a
+  /// single event that describes what happened to the path in question.
+  ///
+  /// If [batch] does contain contradictory events, this returns `null` to
+  /// indicate that the state of the path on the filesystem should be checked to
+  /// determine what occurred.
+  FileSystemEvent? _canonicalEvent(Set<FileSystemEvent> batch) {
+    // An empty batch indicates that we've learned earlier that the batch is
+    // contradictory (e.g. because of a move).
+    if (batch.isEmpty) return null;
+
+    var type = batch.first.type;
+    var isDir = batch.first.isDirectory;
+
+    for (var event in batch.skip(1)) {
+      // If one event reports that the file is a directory and another event
+      // doesn't, that's a contradiction.
+      if (isDir != event.isDirectory) return null;
+
+      // Modify events don't contradict either CREATE or REMOVE events. We can
+      // safely assume the file was modified after a CREATE or before the
+      // REMOVE; otherwise there will also be a REMOVE or CREATE event
+      // (respectively) that will be contradictory.
+      if (event is FileSystemModifyEvent) continue;
+      assert(event is FileSystemCreateEvent ||
+          event is FileSystemDeleteEvent ||
+          event is FileSystemMoveEvent);
+
+      // If we previously thought this was a MODIFY, we now consider it to be a
+      // CREATE or REMOVE event. This is safe for the same reason as above.
+      if (type == FileSystemEvent.modify) {
+        type = event.type;
+        continue;
+      }
+
+      // A CREATE event contradicts a REMOVE event and vice versa.
+      assert(type == FileSystemEvent.create ||
+          type == FileSystemEvent.delete ||
+          type == FileSystemEvent.move);
+      if (type != event.type) return null;
+    }
+
+    switch (type) {
+      case FileSystemEvent.create:
+        return FileSystemCreateEvent(batch.first.path, isDir);
+      case FileSystemEvent.delete:
+        return FileSystemDeleteEvent(batch.first.path, isDir);
+      case FileSystemEvent.modify:
+        return FileSystemModifyEvent(batch.first.path, isDir, false);
+      case FileSystemEvent.move:
+        return null;
+      default:
+        throw StateError('unreachable');
+    }
+  }
+
+  /// Returns zero or more events that describe the change between the last
+  /// known state of [path] and its current state on the filesystem.
+  ///
+  /// This returns a list whose order should be reflected in the events emitted
+  /// to the user, unlike the batched events from [Directory.watch]. The
+  /// returned list may be empty, indicating that no changes occurred to [path]
+  /// (probably indicating that it was created and then immediately deleted).
+  List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
+    var fileExisted = _files.contains(path);
+    var dirExisted = _files.containsDir(path);
+
+    bool fileExists;
+    bool dirExists;
+    try {
+      fileExists = File(path).existsSync();
+      dirExists = Directory(path).existsSync();
+    } on FileSystemException {
+      return const <FileSystemEvent>[];
+    }
+
+    var events = <FileSystemEvent>[];
+    if (fileExisted) {
+      if (fileExists) {
+        events.add(FileSystemModifyEvent(path, false, false));
+      } else {
+        events.add(FileSystemDeleteEvent(path, false));
+      }
+    } else if (dirExisted) {
+      if (dirExists) {
+        // If we got contradictory events for a directory that used to exist and
+        // still exists, we need to rescan the whole thing in case it was
+        // replaced with a different directory.
+        events.add(FileSystemDeleteEvent(path, true));
+        events.add(FileSystemCreateEvent(path, true));
+      } else {
+        events.add(FileSystemDeleteEvent(path, true));
+      }
+    }
+
+    if (!fileExisted && fileExists) {
+      events.add(FileSystemCreateEvent(path, false));
+    } else if (!dirExisted && dirExists) {
+      events.add(FileSystemCreateEvent(path, true));
+    }
+
+    return events;
+  }
+
+  /// The callback that's run when the [Directory.watch] stream is closed.
+  /// Note that this is unlikely to happen on Windows, unless the system itself
+  /// closes the handle.
+  void _onDone() {
+    _watchSubscription = null;
+
+    // Emit remove events for any remaining files.
+    for (var file in _files.paths) {
+      _emitEvent(ChangeType.REMOVE, file);
+    }
+    _files.clear();
+    close();
+  }
+
+  /// Start or restart the underlying [Directory.watch] stream.
+  void _startWatch() {
+    // Note: "watcher closed" exceptions do not get sent over the stream
+    // returned by watch, and must be caught via a zone handler.
+    runZonedGuarded(() {
+      var innerStream = Directory(path).watch(recursive: true);
+      _watchSubscription = innerStream.listen(_onEvent,
+          onError: _eventsController.addError, onDone: _onDone);
+    }, (error, stackTrace) {
+      if (error is FileSystemException &&
+          error.message.startsWith('Directory watcher closed unexpectedly')) {
+        _watchSubscription?.cancel();
+        _eventsController.addError(error, stackTrace);
+        _startWatch();
+      } else {
+        // ignore: only_throw_errors
+        throw error;
+      }
+    });
+  }
+
+  /// Starts or restarts listing the watched directory to get an initial picture
+  /// of its state.
+  Future<void> _listDir() {
+    assert(!isReady);
+    _initialListSubscription?.cancel();
+
+    _files.clear();
+    var completer = Completer<void>();
+    var stream = Directory(path).list(recursive: true);
+    void handleEntity(FileSystemEntity entity) {
+      if (entity is! Directory) _files.add(entity.path);
+    }
+
+    _initialListSubscription = stream.listen(handleEntity,
+        onError: _emitError, onDone: completer.complete, cancelOnError: true);
+    return completer.future;
+  }
+
+  /// Emit an event with the given [type] and [path].
+  void _emitEvent(ChangeType type, String path) {
+    if (!isReady) return;
+
+    _eventsController.add(WatchEvent(type, path));
+  }
+
+  /// Emit an error, then close the watcher.
+  void _emitError(Object error, StackTrace stackTrace) {
+    // Guarantee that ready always completes.
+    if (!isReady) {
+      _readyCompleter.complete();
+    }
+    _eventsController.addError(error, stackTrace);
+    close();
+  }
+}
diff --git a/pkgs/watcher/lib/src/file_watcher.dart b/pkgs/watcher/lib/src/file_watcher.dart
new file mode 100644
index 0000000..143aa31
--- /dev/null
+++ b/pkgs/watcher/lib/src/file_watcher.dart
@@ -0,0 +1,44 @@
+// 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 'dart:io';
+
+import '../watcher.dart';
+import 'custom_watcher_factory.dart';
+import 'file_watcher/native.dart';
+
+/// Watches a file and emits [WatchEvent]s when the file has changed.
+///
+/// Note that since each watcher only watches a single file, it will only emit
+/// [ChangeType.MODIFY] events, except when the file is deleted at which point
+/// it will emit a single [ChangeType.REMOVE] event and then close the stream.
+///
+/// If the file is deleted and quickly replaced (when a new file is moved in its
+/// place, for example) this will emit a [ChangeType.MODIFY] event.
+abstract class FileWatcher implements Watcher {
+  /// Creates a new [FileWatcher] monitoring [file].
+  ///
+  /// If a native file watcher is available for this platform, this will use it.
+  /// Otherwise, it will fall back to a [PollingFileWatcher]. Notably, native
+  /// file watching is *not* supported on Windows.
+  ///
+  /// If [pollingDelay] is passed, it specifies the amount of time the watcher
+  /// will pause between successive polls of the directory contents. Making this
+  /// shorter will give more immediate feedback at the expense of doing more IO
+  /// and higher CPU usage. Defaults to one second. Ignored for non-polling
+  /// watchers.
+  factory FileWatcher(String file, {Duration? pollingDelay}) {
+    var customWatcher =
+        createCustomFileWatcher(file, pollingDelay: pollingDelay);
+    if (customWatcher != null) return customWatcher;
+
+    // [File.watch] doesn't work on Windows, but
+    // [FileSystemEntity.isWatchSupported] is still true because directory
+    // watching does work.
+    if (FileSystemEntity.isWatchSupported && !Platform.isWindows) {
+      return NativeFileWatcher(file);
+    }
+    return PollingFileWatcher(file, pollingDelay: pollingDelay);
+  }
+}
diff --git a/pkgs/watcher/lib/src/file_watcher/native.dart b/pkgs/watcher/lib/src/file_watcher/native.dart
new file mode 100644
index 0000000..502aa10
--- /dev/null
+++ b/pkgs/watcher/lib/src/file_watcher/native.dart
@@ -0,0 +1,90 @@
+// 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 'dart:async';
+import 'dart:io';
+
+import '../file_watcher.dart';
+import '../resubscribable.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+/// Uses the native file system notifications to watch for filesystem events.
+///
+/// Single-file notifications are much simpler than those for multiple files, so
+/// this doesn't need to be split out into multiple OS-specific classes.
+class NativeFileWatcher extends ResubscribableWatcher implements FileWatcher {
+  NativeFileWatcher(String path) : super(path, () => _NativeFileWatcher(path));
+}
+
+class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
+  @override
+  final String path;
+
+  @override
+  Stream<WatchEvent> get events => _eventsController.stream;
+  final _eventsController = StreamController<WatchEvent>.broadcast();
+
+  @override
+  bool get isReady => _readyCompleter.isCompleted;
+
+  @override
+  Future<void> get ready => _readyCompleter.future;
+  final _readyCompleter = Completer<void>();
+
+  StreamSubscription<List<FileSystemEvent>>? _subscription;
+
+  _NativeFileWatcher(this.path) {
+    _listen();
+
+    // We don't need to do any initial set-up, so we're ready immediately after
+    // being listened to.
+    _readyCompleter.complete();
+  }
+
+  void _listen() {
+    // Batch the events together so that we can dedup them.
+    _subscription = File(path)
+        .watch()
+        .batchEvents()
+        .listen(_onBatch, onError: _eventsController.addError, onDone: _onDone);
+  }
+
+  void _onBatch(List<FileSystemEvent> batch) {
+    if (batch.any((event) => event.type == FileSystemEvent.delete)) {
+      // If the file is deleted, the underlying stream will close. We handle
+      // emitting our own REMOVE event in [_onDone].
+      return;
+    }
+
+    _eventsController.add(WatchEvent(ChangeType.MODIFY, path));
+  }
+
+  void _onDone() async {
+    var fileExists = await File(path).exists();
+
+    // Check for this after checking whether the file exists because it's
+    // possible that [close] was called between [File.exists] being called and
+    // it completing.
+    if (_eventsController.isClosed) return;
+
+    if (fileExists) {
+      // If the file exists now, it was probably removed and quickly replaced;
+      // this can happen for example when another file is moved on top of it.
+      // Re-subscribe and report a modify event.
+      _eventsController.add(WatchEvent(ChangeType.MODIFY, path));
+      _listen();
+    } else {
+      _eventsController.add(WatchEvent(ChangeType.REMOVE, path));
+      close();
+    }
+  }
+
+  @override
+  void close() {
+    _subscription?.cancel();
+    _subscription = null;
+    _eventsController.close();
+  }
+}
diff --git a/pkgs/watcher/lib/src/file_watcher/polling.dart b/pkgs/watcher/lib/src/file_watcher/polling.dart
new file mode 100644
index 0000000..15ff9ab
--- /dev/null
+++ b/pkgs/watcher/lib/src/file_watcher/polling.dart
@@ -0,0 +1,106 @@
+// 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 'dart:async';
+import 'dart:io';
+
+import '../file_watcher.dart';
+import '../resubscribable.dart';
+import '../stat.dart';
+import '../watch_event.dart';
+
+/// Periodically polls a file for changes.
+class PollingFileWatcher extends ResubscribableWatcher implements FileWatcher {
+  PollingFileWatcher(String path, {Duration? pollingDelay})
+      : super(path, () {
+          return _PollingFileWatcher(
+              path, pollingDelay ?? const Duration(seconds: 1));
+        });
+}
+
+class _PollingFileWatcher implements FileWatcher, ManuallyClosedWatcher {
+  @override
+  final String path;
+
+  @override
+  Stream<WatchEvent> get events => _eventsController.stream;
+  final _eventsController = StreamController<WatchEvent>.broadcast();
+
+  @override
+  bool get isReady => _readyCompleter.isCompleted;
+
+  @override
+  Future<void> get ready => _readyCompleter.future;
+  final _readyCompleter = Completer<void>();
+
+  /// The timer that controls polling.
+  late final Timer _timer;
+
+  /// The previous modification time of the file.
+  ///
+  /// `null` indicates the file does not (or did not on the last poll) exist.
+  DateTime? _lastModified;
+
+  _PollingFileWatcher(this.path, Duration pollingDelay) {
+    _timer = Timer.periodic(pollingDelay, (_) => _poll());
+    _poll();
+  }
+
+  /// Checks the mtime of the file and whether it's been removed.
+  Future<void> _poll() async {
+    // We don't mark the file as removed if this is the first poll. Instead,
+    // below we forward the dart:io error that comes from trying to read the
+    // mtime below.
+    var pathExists = await File(path).exists();
+    if (_eventsController.isClosed) return;
+
+    if (_lastModified != null && !pathExists) {
+      _flagReady();
+      _eventsController.add(WatchEvent(ChangeType.REMOVE, path));
+      unawaited(close());
+      return;
+    }
+
+    DateTime? modified;
+    try {
+      modified = await modificationTime(path);
+    } on FileSystemException catch (error, stackTrace) {
+      if (!_eventsController.isClosed) {
+        _flagReady();
+        _eventsController.addError(error, stackTrace);
+        await close();
+      }
+    }
+    if (_eventsController.isClosed) {
+      _flagReady();
+      return;
+    }
+
+    if (!isReady) {
+      // If this is the first poll, don't emit an event, just set the last mtime
+      // and complete the completer.
+      _lastModified = modified;
+      _flagReady();
+      return;
+    }
+
+    if (_lastModified == modified) return;
+
+    _lastModified = modified;
+    _eventsController.add(WatchEvent(ChangeType.MODIFY, path));
+  }
+
+  /// Flags this watcher as ready if it has not already been done.
+  void _flagReady() {
+    if (!isReady) {
+      _readyCompleter.complete();
+    }
+  }
+
+  @override
+  Future<void> close() async {
+    _timer.cancel();
+    await _eventsController.close();
+  }
+}
diff --git a/pkgs/watcher/lib/src/path_set.dart b/pkgs/watcher/lib/src/path_set.dart
new file mode 100644
index 0000000..4f41cf9
--- /dev/null
+++ b/pkgs/watcher/lib/src/path_set.dart
@@ -0,0 +1,190 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:collection';
+
+import 'package:path/path.dart' as p;
+
+/// A set of paths, organized into a directory hierarchy.
+///
+/// When a path is [add]ed, it creates an implicit directory structure above
+/// that path. Directories can be inspected using [containsDir] and removed
+/// using [remove]. If they're removed, their contents are removed as well.
+///
+/// The paths in the set are normalized so that they all begin with [root].
+class PathSet {
+  /// The root path, which all paths in the set must be under.
+  final String root;
+
+  /// The path set's directory hierarchy.
+  ///
+  /// Each entry represents a directory or file. It may be a file or directory
+  /// that was explicitly added, or a parent directory that was implicitly
+  /// added in order to add a child.
+  final _Entry _entries = _Entry();
+
+  PathSet(this.root);
+
+  /// Adds [path] to the set.
+  void add(String path) {
+    path = _normalize(path);
+
+    var parts = p.split(path);
+    var entry = _entries;
+    for (var part in parts) {
+      entry = entry.contents.putIfAbsent(part, _Entry.new);
+    }
+
+    entry.isExplicit = true;
+  }
+
+  /// Removes [path] and any paths beneath it from the set and returns the
+  /// removed paths.
+  ///
+  /// Even if [path] itself isn't in the set, if it's a directory containing
+  /// paths that are in the set those paths will be removed and returned.
+  ///
+  /// If neither [path] nor any paths beneath it are in the set, returns an
+  /// empty set.
+  Set<String> remove(String path) {
+    path = _normalize(path);
+    var parts = Queue.of(p.split(path));
+
+    // Remove the children of [dir], as well as [dir] itself if necessary.
+    //
+    // [partialPath] is the path to [dir], and a prefix of [path]; the remaining
+    // components of [path] are in [parts].
+    Set<String> recurse(_Entry dir, String partialPath) {
+      if (parts.length > 1) {
+        // If there's more than one component left in [path], recurse down to
+        // the next level.
+        var part = parts.removeFirst();
+        var entry = dir.contents[part];
+        if (entry == null || entry.contents.isEmpty) return <String>{};
+
+        partialPath = p.join(partialPath, part);
+        var paths = recurse(entry, partialPath);
+        // After removing this entry's children, if it has no more children and
+        // it's not in the set in its own right, remove it as well.
+        if (entry.contents.isEmpty && !entry.isExplicit) {
+          dir.contents.remove(part);
+        }
+        return paths;
+      }
+
+      // If there's only one component left in [path], we should remove it.
+      var entry = dir.contents.remove(parts.first);
+      if (entry == null) return <String>{};
+
+      if (entry.contents.isEmpty) {
+        return {p.join(root, path)};
+      }
+
+      var set = _explicitPathsWithin(entry, path);
+      if (entry.isExplicit) {
+        set.add(p.join(root, path));
+      }
+
+      return set;
+    }
+
+    return recurse(_entries, root);
+  }
+
+  /// Recursively lists all of the explicit paths within [dir].
+  ///
+  /// [dirPath] should be the path to [dir].
+  Set<String> _explicitPathsWithin(_Entry dir, String dirPath) {
+    var paths = <String>{};
+    void recurse(_Entry dir, String path) {
+      dir.contents.forEach((name, entry) {
+        var entryPath = p.join(path, name);
+        if (entry.isExplicit) paths.add(p.join(root, entryPath));
+
+        recurse(entry, entryPath);
+      });
+    }
+
+    recurse(dir, dirPath);
+    return paths;
+  }
+
+  /// Returns whether this set contains [path].
+  ///
+  /// This only returns true for paths explicitly added to this set.
+  /// Implicitly-added directories can be inspected using [containsDir].
+  bool contains(String path) {
+    path = _normalize(path);
+    var entry = _entries;
+
+    for (var part in p.split(path)) {
+      var child = entry.contents[part];
+      if (child == null) return false;
+      entry = child;
+    }
+
+    return entry.isExplicit;
+  }
+
+  /// Returns whether this set contains paths beneath [path].
+  bool containsDir(String path) {
+    path = _normalize(path);
+    var entry = _entries;
+
+    for (var part in p.split(path)) {
+      var child = entry.contents[part];
+      if (child == null) return false;
+      entry = child;
+    }
+
+    return entry.contents.isNotEmpty;
+  }
+
+  /// All of the paths explicitly added to this set.
+  List<String> get paths {
+    var result = <String>[];
+
+    void recurse(_Entry dir, String path) {
+      for (var mapEntry in dir.contents.entries) {
+        var entry = mapEntry.value;
+        var entryPath = p.join(path, mapEntry.key);
+        if (entry.isExplicit) result.add(entryPath);
+        recurse(entry, entryPath);
+      }
+    }
+
+    recurse(_entries, root);
+    return result;
+  }
+
+  /// Removes all paths from this set.
+  void clear() {
+    _entries.contents.clear();
+  }
+
+  /// Returns a normalized version of [path].
+  ///
+  /// This removes any extra ".." or "."s and ensure that the returned path
+  /// begins with [root]. It's an error if [path] isn't within [root].
+  String _normalize(String path) {
+    assert(p.isWithin(root, path));
+
+    return p.relative(p.normalize(path), from: root);
+  }
+}
+
+/// A virtual file system entity tracked by the [PathSet].
+///
+/// It may have child entries in [contents], which implies it's a directory.
+class _Entry {
+  /// The child entries contained in this directory.
+  final Map<String, _Entry> contents = {};
+
+  /// If this entry was explicitly added as a leaf file system entity, this
+  /// will be true.
+  ///
+  /// Otherwise, it represents a parent directory that was implicitly added
+  /// when added some child of it.
+  bool isExplicit = false;
+}
diff --git a/pkgs/watcher/lib/src/resubscribable.dart b/pkgs/watcher/lib/src/resubscribable.dart
new file mode 100644
index 0000000..b99e9d7
--- /dev/null
+++ b/pkgs/watcher/lib/src/resubscribable.dart
@@ -0,0 +1,79 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../watcher.dart';
+
+/// A wrapper for [ManuallyClosedWatcher] that encapsulates support for closing
+/// the watcher when it has no subscribers and re-opening it when it's
+/// re-subscribed.
+///
+/// It's simpler to implement watchers without worrying about this behavior.
+/// This class wraps a watcher class which can be written with the simplifying
+/// assumption that it can continue emitting events until an explicit `close`
+/// method is called, at which point it will cease emitting events entirely. The
+/// [ManuallyClosedWatcher] interface is used for these watchers.
+///
+/// This would be more cleanly implemented as a function that takes a class and
+/// emits a new class, but Dart doesn't support that sort of thing. Instead it
+/// takes a factory function that produces instances of the inner class.
+abstract class ResubscribableWatcher implements Watcher {
+  /// The factory function that produces instances of the inner class.
+  final ManuallyClosedWatcher Function() _factory;
+
+  @override
+  final String path;
+
+  @override
+  Stream<WatchEvent> get events => _eventsController.stream;
+  late StreamController<WatchEvent> _eventsController;
+
+  @override
+  bool get isReady => _readyCompleter.isCompleted;
+
+  @override
+  Future<void> get ready => _readyCompleter.future;
+  var _readyCompleter = Completer<void>();
+
+  /// Creates a new [ResubscribableWatcher] wrapping the watchers
+  /// emitted by [_factory].
+  ResubscribableWatcher(this.path, this._factory) {
+    late ManuallyClosedWatcher watcher;
+    late StreamSubscription<WatchEvent> subscription;
+
+    _eventsController = StreamController<WatchEvent>.broadcast(
+        onListen: () async {
+          watcher = _factory();
+          subscription = watcher.events.listen(_eventsController.add,
+              onError: _eventsController.addError,
+              onDone: _eventsController.close);
+
+          // It's important that we complete the value of [_readyCompleter] at
+          // the time [onListen] is called, as opposed to the value when
+          // [watcher.ready] fires. A new completer may be created by that time.
+          await watcher.ready;
+          _readyCompleter.complete();
+        },
+        onCancel: () {
+          // Cancel the subscription before closing the watcher so that the
+          // watcher's `onDone` event doesn't close [events].
+          subscription.cancel();
+          watcher.close();
+          _readyCompleter = Completer();
+        },
+        sync: true);
+  }
+}
+
+/// An interface for watchers with an explicit, manual [close] method.
+///
+/// See [ResubscribableWatcher].
+abstract class ManuallyClosedWatcher implements Watcher {
+  /// Closes the watcher.
+  ///
+  /// Subclasses should close their [events] stream and release any internal
+  /// resources.
+  void close();
+}
diff --git a/pkgs/watcher/lib/src/stat.dart b/pkgs/watcher/lib/src/stat.dart
new file mode 100644
index 0000000..fe0f155
--- /dev/null
+++ b/pkgs/watcher/lib/src/stat.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+/// A function that takes a file path and returns the last modified time for
+/// the file at that path.
+typedef MockTimeCallback = DateTime? Function(String path);
+
+MockTimeCallback? _mockTimeCallback;
+
+/// Overrides the default behavior for accessing a file's modification time
+/// with [callback].
+///
+/// The OS file modification time has pretty rough granularity (like a few
+/// seconds) which can make for slow tests that rely on modtime. This lets you
+/// replace it with something you control.
+void mockGetModificationTime(MockTimeCallback callback) {
+  _mockTimeCallback = callback;
+}
+
+/// Gets the modification time for the file at [path].
+/// Completes with `null` if the file does not exist.
+Future<DateTime?> modificationTime(String path) async {
+  var mockTimeCallback = _mockTimeCallback;
+  if (mockTimeCallback != null) {
+    return mockTimeCallback(path);
+  }
+
+  final stat = await FileStat.stat(path);
+  if (stat.type == FileSystemEntityType.notFound) return null;
+  return stat.modified;
+}
diff --git a/pkgs/watcher/lib/src/utils.dart b/pkgs/watcher/lib/src/utils.dart
new file mode 100644
index 0000000..c2e71b3
--- /dev/null
+++ b/pkgs/watcher/lib/src/utils.dart
@@ -0,0 +1,52 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:io';
+
+/// Returns `true` if [error] is a [FileSystemException] for a missing
+/// directory.
+bool isDirectoryNotFoundException(Object error) {
+  if (error is! FileSystemException) return false;
+
+  // See dartbug.com/12461 and tests/standalone/io/directory_error_test.dart.
+  var notFoundCode = Platform.operatingSystem == 'windows' ? 3 : 2;
+  return error.osError?.errorCode == notFoundCode;
+}
+
+/// Returns the union of all elements in each set in [sets].
+Set<T> unionAll<T>(Iterable<Set<T>> sets) =>
+    sets.fold(<T>{}, (union, set) => union.union(set));
+
+extension BatchEvents<T> on Stream<T> {
+  /// Batches all events that are sent at the same time.
+  ///
+  /// When multiple events are synchronously added to a stream controller, the
+  /// [StreamController] implementation uses [scheduleMicrotask] to schedule the
+  /// asynchronous firing of each event. In order to recreate the synchronous
+  /// batches, this collates all the events that are received in "nearby"
+  /// microtasks.
+  Stream<List<T>> batchEvents() {
+    var batch = Queue<T>();
+    return StreamTransformer<T, List<T>>.fromHandlers(
+        handleData: (event, sink) {
+      batch.add(event);
+
+      // [Timer.run] schedules an event that runs after any microtasks that have
+      // been scheduled.
+      Timer.run(() {
+        if (batch.isEmpty) return;
+        sink.add(batch.toList());
+        batch.clear();
+      });
+    }, handleDone: (sink) {
+      if (batch.isNotEmpty) {
+        sink.add(batch.toList());
+        batch.clear();
+      }
+      sink.close();
+    }).bind(this);
+  }
+}
diff --git a/pkgs/watcher/lib/src/watch_event.dart b/pkgs/watcher/lib/src/watch_event.dart
new file mode 100644
index 0000000..b65afc2
--- /dev/null
+++ b/pkgs/watcher/lib/src/watch_event.dart
@@ -0,0 +1,38 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// An event describing a single change to the file system.
+class WatchEvent {
+  /// The manner in which the file at [path] has changed.
+  final ChangeType type;
+
+  /// The path of the file that changed.
+  final String path;
+
+  WatchEvent(this.type, this.path);
+
+  @override
+  String toString() => '$type $path';
+}
+
+/// Enum for what kind of change has happened to a file.
+class ChangeType {
+  /// A new file has been added.
+  // ignore: constant_identifier_names
+  static const ADD = ChangeType('add');
+
+  /// A file has been removed.
+  // ignore: constant_identifier_names
+  static const REMOVE = ChangeType('remove');
+
+  /// The contents of a file have changed.
+  // ignore: constant_identifier_names
+  static const MODIFY = ChangeType('modify');
+
+  final String _name;
+  const ChangeType(this._name);
+
+  @override
+  String toString() => _name;
+}
diff --git a/pkgs/watcher/lib/watcher.dart b/pkgs/watcher/lib/watcher.dart
new file mode 100644
index 0000000..12a5369
--- /dev/null
+++ b/pkgs/watcher/lib/watcher.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'src/directory_watcher.dart';
+import 'src/file_watcher.dart';
+import 'src/watch_event.dart';
+
+export 'src/custom_watcher_factory.dart' show registerCustomWatcher;
+export 'src/directory_watcher.dart';
+export 'src/directory_watcher/polling.dart';
+export 'src/file_watcher.dart';
+export 'src/file_watcher/polling.dart';
+export 'src/watch_event.dart';
+
+abstract class Watcher {
+  /// The path to the file or directory whose contents are being monitored.
+  String get path;
+
+  /// The broadcast [Stream] of events that have occurred to the watched file or
+  /// files in the watched directory.
+  ///
+  /// Changes will only be monitored while this stream has subscribers. Any
+  /// changes that occur during periods when there are no subscribers will not
+  /// be reported the next time a subscriber is added.
+  Stream<WatchEvent> get events;
+
+  /// Whether the watcher is initialized and watching for changes.
+  ///
+  /// This is true if and only if [ready] is complete.
+  bool get isReady;
+
+  /// A [Future] that completes when the watcher is initialized and watching for
+  /// changes.
+  ///
+  /// If the watcher is not currently monitoring the file or directory (because
+  /// there are no subscribers to [events]), this returns a future that isn't
+  /// complete yet. It will complete when a subscriber starts listening and the
+  /// watcher finishes any initialization work it needs to do.
+  ///
+  /// If the watcher is already monitoring, this returns an already complete
+  /// future.
+  ///
+  /// This future always completes successfully as errors are provided through
+  /// the [events] stream.
+  Future get ready;
+
+  /// Creates a new [DirectoryWatcher] or [FileWatcher] monitoring [path],
+  /// depending on whether it's a file or directory.
+  ///
+  /// If a native watcher is available for this platform, this will use it.
+  /// Otherwise, it will fall back to a polling watcher. Notably, watching
+  /// individual files is not natively supported on Windows, although watching
+  /// directories is.
+  ///
+  /// If [pollingDelay] is passed, it specifies the amount of time the watcher
+  /// will pause between successive polls of the contents of [path]. Making this
+  /// shorter will give more immediate feedback at the expense of doing more IO
+  /// and higher CPU usage. Defaults to one second. Ignored for non-polling
+  /// watchers.
+  factory Watcher(String path, {Duration? pollingDelay}) {
+    if (File(path).existsSync()) {
+      return FileWatcher(path, pollingDelay: pollingDelay);
+    } else {
+      return DirectoryWatcher(path, pollingDelay: pollingDelay);
+    }
+  }
+}
diff --git a/pkgs/watcher/pubspec.yaml b/pkgs/watcher/pubspec.yaml
new file mode 100644
index 0000000..7781bd4
--- /dev/null
+++ b/pkgs/watcher/pubspec.yaml
@@ -0,0 +1,19 @@
+name: watcher
+version: 1.1.1
+description: >-
+  A file system watcher. It monitors changes to contents of directories and
+  sends notifications when files have been added, removed, or modified.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/watcher
+
+environment:
+  sdk: ^3.1.0
+
+dependencies:
+  async: ^2.5.0
+  path: ^1.8.0
+
+dev_dependencies:
+  benchmark_harness: ^2.0.0
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.6
+  test_descriptor: ^2.0.0
diff --git a/pkgs/watcher/test/custom_watcher_factory_test.dart b/pkgs/watcher/test/custom_watcher_factory_test.dart
new file mode 100644
index 0000000..e9d65bb
--- /dev/null
+++ b/pkgs/watcher/test/custom_watcher_factory_test.dart
@@ -0,0 +1,142 @@
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+void main() {
+  late _MemFs memFs;
+  final memFsFactoryId = 'MemFs';
+  final noOpFactoryId = 'NoOp';
+
+  setUpAll(() {
+    memFs = _MemFs();
+    var memFsWatcherFactory = _MemFsWatcherFactory(memFs);
+    var noOpWatcherFactory = _NoOpWatcherFactory();
+    registerCustomWatcher(
+        noOpFactoryId,
+        noOpWatcherFactory.createDirectoryWatcher,
+        noOpWatcherFactory.createFileWatcher);
+    registerCustomWatcher(
+        memFsFactoryId,
+        memFsWatcherFactory.createDirectoryWatcher,
+        memFsWatcherFactory.createFileWatcher);
+  });
+
+  test('notifies for files', () async {
+    var watcher = FileWatcher('file.txt');
+
+    var completer = Completer<WatchEvent>();
+    watcher.events.listen((event) => completer.complete(event));
+    await watcher.ready;
+    memFs.add('file.txt');
+    var event = await completer.future;
+
+    expect(event.type, ChangeType.ADD);
+    expect(event.path, 'file.txt');
+  });
+
+  test('notifies for directories', () async {
+    var watcher = DirectoryWatcher('dir');
+
+    var completer = Completer<WatchEvent>();
+    watcher.events.listen((event) => completer.complete(event));
+    await watcher.ready;
+    memFs.add('dir');
+    var event = await completer.future;
+
+    expect(event.type, ChangeType.ADD);
+    expect(event.path, 'dir');
+  });
+
+  test('registering twice throws', () async {
+    expect(
+      () => registerCustomWatcher(
+          memFsFactoryId,
+          (_, {pollingDelay}) => throw UnimplementedError(),
+          (_, {pollingDelay}) => throw UnimplementedError()),
+      throwsA(isA<ArgumentError>()),
+    );
+  });
+
+  test('finding two applicable factories throws', () async {
+    // Note that _MemFsWatcherFactory always returns a watcher, so having two
+    // will always produce a conflict.
+    var watcherFactory = _MemFsWatcherFactory(memFs);
+    registerCustomWatcher('Different id', watcherFactory.createDirectoryWatcher,
+        watcherFactory.createFileWatcher);
+    expect(() => FileWatcher('file.txt'), throwsA(isA<StateError>()));
+    expect(() => DirectoryWatcher('dir'), throwsA(isA<StateError>()));
+  });
+}
+
+class _MemFs {
+  final _streams = <String, Set<StreamController<WatchEvent>>>{};
+
+  StreamController<WatchEvent> watchStream(String path) {
+    var controller = StreamController<WatchEvent>();
+    _streams
+        .putIfAbsent(path, () => <StreamController<WatchEvent>>{})
+        .add(controller);
+    return controller;
+  }
+
+  void add(String path) {
+    var controllers = _streams[path];
+    if (controllers != null) {
+      for (var controller in controllers) {
+        controller.add(WatchEvent(ChangeType.ADD, path));
+      }
+    }
+  }
+
+  void remove(String path) {
+    var controllers = _streams[path];
+    if (controllers != null) {
+      for (var controller in controllers) {
+        controller.add(WatchEvent(ChangeType.REMOVE, path));
+      }
+    }
+  }
+}
+
+class _MemFsWatcher implements FileWatcher, DirectoryWatcher, Watcher {
+  final String _path;
+  final StreamController<WatchEvent> _controller;
+
+  _MemFsWatcher(this._path, this._controller);
+
+  @override
+  String get path => _path;
+
+  @override
+  String get directory => throw UnsupportedError('directory is not supported');
+
+  @override
+  Stream<WatchEvent> get events => _controller.stream;
+
+  @override
+  bool get isReady => true;
+
+  @override
+  Future<void> get ready async {}
+}
+
+class _MemFsWatcherFactory {
+  final _MemFs _memFs;
+  _MemFsWatcherFactory(this._memFs);
+
+  DirectoryWatcher? createDirectoryWatcher(String path,
+          {Duration? pollingDelay}) =>
+      _MemFsWatcher(path, _memFs.watchStream(path));
+
+  FileWatcher? createFileWatcher(String path, {Duration? pollingDelay}) =>
+      _MemFsWatcher(path, _memFs.watchStream(path));
+}
+
+class _NoOpWatcherFactory {
+  DirectoryWatcher? createDirectoryWatcher(String path,
+          {Duration? pollingDelay}) =>
+      null;
+
+  FileWatcher? createFileWatcher(String path, {Duration? pollingDelay}) => null;
+}
diff --git a/pkgs/watcher/test/directory_watcher/linux_test.dart b/pkgs/watcher/test/directory_watcher/linux_test.dart
new file mode 100644
index 0000000..a10a72c
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/linux_test.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('linux')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/linux.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = LinuxDirectoryWatcher.new;
+
+  sharedTests();
+
+  test('DirectoryWatcher creates a LinuxDirectoryWatcher on Linux', () {
+    expect(DirectoryWatcher('.'), const TypeMatcher<LinuxDirectoryWatcher>());
+  });
+
+  test('emits events for many nested files moved out then immediately back in',
+      () async {
+    withPermutations(
+        (i, j, k) => writeFile('dir/sub/sub-$i/sub-$j/file-$k.txt'));
+    await startWatcher(path: 'dir');
+
+    renameDir('dir/sub', 'sub');
+    renameDir('sub', 'dir/sub');
+
+    await allowEither(() {
+      inAnyOrder(withPermutations(
+          (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+
+      inAnyOrder(withPermutations(
+          (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+    }, () {
+      inAnyOrder(withPermutations(
+          (i, j, k) => isModifyEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+    });
+  });
+}
diff --git a/pkgs/watcher/test/directory_watcher/mac_os_test.dart b/pkgs/watcher/test/directory_watcher/mac_os_test.dart
new file mode 100644
index 0000000..3376626
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/mac_os_test.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('mac-os')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/mac_os.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = MacOSDirectoryWatcher.new;
+
+  sharedTests();
+
+  test('DirectoryWatcher creates a MacOSDirectoryWatcher on Mac OS', () {
+    expect(DirectoryWatcher('.'), const TypeMatcher<MacOSDirectoryWatcher>());
+  });
+
+  test(
+      'does not notify about the watched directory being deleted and '
+      'recreated immediately before watching', () async {
+    createDir('dir');
+    writeFile('dir/old.txt');
+    deleteDir('dir');
+    createDir('dir');
+
+    await startWatcher(path: 'dir');
+    writeFile('dir/newer.txt');
+    await expectAddEvent('dir/newer.txt');
+  });
+
+  test('emits events for many nested files moved out then immediately back in',
+      () async {
+    withPermutations(
+        (i, j, k) => writeFile('dir/sub/sub-$i/sub-$j/file-$k.txt'));
+
+    await startWatcher(path: 'dir');
+
+    renameDir('dir/sub', 'sub');
+    renameDir('sub', 'dir/sub');
+
+    await allowEither(() {
+      inAnyOrder(withPermutations(
+          (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+
+      inAnyOrder(withPermutations(
+          (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+    }, () {
+      inAnyOrder(withPermutations(
+          (i, j, k) => isModifyEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+    });
+  });
+  test('does not suppress files with the same prefix as a directory', () async {
+    // Regression test for https://github.com/dart-lang/watcher/issues/83
+    writeFile('some_name.txt');
+
+    await startWatcher();
+
+    writeFile('some_name/some_name.txt');
+    deleteFile('some_name.txt');
+
+    await expectRemoveEvent('some_name.txt');
+  });
+}
diff --git a/pkgs/watcher/test/directory_watcher/polling_test.dart b/pkgs/watcher/test/directory_watcher/polling_test.dart
new file mode 100644
index 0000000..f4ec8f4
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/polling_test.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  // Use a short delay to make the tests run quickly.
+  watcherFactory = (dir) => PollingDirectoryWatcher(dir,
+      pollingDelay: const Duration(milliseconds: 100));
+
+  sharedTests();
+
+  test('does not notify if the modification time did not change', () async {
+    writeFile('a.txt', contents: 'before');
+    writeFile('b.txt', contents: 'before');
+    await startWatcher();
+    writeFile('a.txt', contents: 'after', updateModified: false);
+    writeFile('b.txt', contents: 'after');
+    await expectModifyEvent('b.txt');
+  });
+}
diff --git a/pkgs/watcher/test/directory_watcher/shared.dart b/pkgs/watcher/test/directory_watcher/shared.dart
new file mode 100644
index 0000000..1ebc78d
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/shared.dart
@@ -0,0 +1,344 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:watcher/src/utils.dart';
+
+import '../utils.dart';
+
+void sharedTests() {
+  test('does not notify for files that already exist when started', () async {
+    // Make some pre-existing files.
+    writeFile('a.txt');
+    writeFile('b.txt');
+
+    await startWatcher();
+
+    // Change one after the watcher is running.
+    writeFile('b.txt', contents: 'modified');
+
+    // We should get a modify event for the changed file, but no add events
+    // for them before this.
+    await expectModifyEvent('b.txt');
+  });
+
+  test('notifies when a file is added', () async {
+    await startWatcher();
+    writeFile('file.txt');
+    await expectAddEvent('file.txt');
+  });
+
+  test('notifies when a file is modified', () async {
+    writeFile('file.txt');
+    await startWatcher();
+    writeFile('file.txt', contents: 'modified');
+    await expectModifyEvent('file.txt');
+  });
+
+  test('notifies when a file is removed', () async {
+    writeFile('file.txt');
+    await startWatcher();
+    deleteFile('file.txt');
+    await expectRemoveEvent('file.txt');
+  });
+
+  test('notifies when a file is modified multiple times', () async {
+    writeFile('file.txt');
+    await startWatcher();
+    writeFile('file.txt', contents: 'modified');
+    await expectModifyEvent('file.txt');
+    writeFile('file.txt', contents: 'modified again');
+    await expectModifyEvent('file.txt');
+  });
+
+  test('notifies even if the file contents are unchanged', () async {
+    writeFile('a.txt', contents: 'same');
+    writeFile('b.txt', contents: 'before');
+    await startWatcher();
+
+    writeFile('a.txt', contents: 'same');
+    writeFile('b.txt', contents: 'after');
+    await inAnyOrder([isModifyEvent('a.txt'), isModifyEvent('b.txt')]);
+  });
+
+  test('when the watched directory is deleted, removes all files', () async {
+    writeFile('dir/a.txt');
+    writeFile('dir/b.txt');
+
+    await startWatcher(path: 'dir');
+
+    deleteDir('dir');
+    await inAnyOrder([isRemoveEvent('dir/a.txt'), isRemoveEvent('dir/b.txt')]);
+  });
+
+  test('when the watched directory is moved, removes all files', () async {
+    writeFile('dir/a.txt');
+    writeFile('dir/b.txt');
+
+    await startWatcher(path: 'dir');
+
+    renameDir('dir', 'moved_dir');
+    createDir('dir');
+    await inAnyOrder([isRemoveEvent('dir/a.txt'), isRemoveEvent('dir/b.txt')]);
+  });
+
+  // Regression test for b/30768513.
+  test(
+      "doesn't crash when the directory is moved immediately after a subdir "
+      'is added', () async {
+    writeFile('dir/a.txt');
+    writeFile('dir/b.txt');
+
+    await startWatcher(path: 'dir');
+
+    createDir('dir/subdir');
+    renameDir('dir', 'moved_dir');
+    createDir('dir');
+    await inAnyOrder([isRemoveEvent('dir/a.txt'), isRemoveEvent('dir/b.txt')]);
+  });
+
+  group('moves', () {
+    test('notifies when a file is moved within the watched directory',
+        () async {
+      writeFile('old.txt');
+      await startWatcher();
+      renameFile('old.txt', 'new.txt');
+
+      await inAnyOrder([isAddEvent('new.txt'), isRemoveEvent('old.txt')]);
+    });
+
+    test('notifies when a file is moved from outside the watched directory',
+        () async {
+      writeFile('old.txt');
+      createDir('dir');
+      await startWatcher(path: 'dir');
+
+      renameFile('old.txt', 'dir/new.txt');
+      await expectAddEvent('dir/new.txt');
+    });
+
+    test('notifies when a file is moved outside the watched directory',
+        () async {
+      writeFile('dir/old.txt');
+      await startWatcher(path: 'dir');
+
+      renameFile('dir/old.txt', 'new.txt');
+      await expectRemoveEvent('dir/old.txt');
+    });
+
+    test('notifies when a file is moved onto an existing one', () async {
+      writeFile('from.txt');
+      writeFile('to.txt');
+      await startWatcher();
+
+      renameFile('from.txt', 'to.txt');
+      await inAnyOrder([isRemoveEvent('from.txt'), isModifyEvent('to.txt')]);
+    }, onPlatform: {
+      'windows': const Skip('https://github.com/dart-lang/watcher/issues/125')
+    });
+  });
+
+  // Most of the time, when multiple filesystem actions happen in sequence,
+  // they'll be batched together and the watcher will see them all at once.
+  // These tests verify that the watcher normalizes and combine these events
+  // properly. However, very occasionally the events will be reported in
+  // separate batches, and the watcher will report them as though they occurred
+  // far apart in time, so each of these tests has a "backup case" to allow for
+  // that as well.
+  group('clustered changes', () {
+    test("doesn't notify when a file is created and then immediately removed",
+        () async {
+      writeFile('test.txt');
+      await startWatcher();
+      writeFile('file.txt');
+      deleteFile('file.txt');
+
+      // Backup case.
+      startClosingEventStream();
+      await allowEvents(() {
+        expectAddEvent('file.txt');
+        expectRemoveEvent('file.txt');
+      });
+    });
+
+    test(
+        'reports a modification when a file is deleted and then immediately '
+        'recreated', () async {
+      writeFile('file.txt');
+      await startWatcher();
+
+      deleteFile('file.txt');
+      writeFile('file.txt', contents: 're-created');
+
+      await allowEither(() {
+        expectModifyEvent('file.txt');
+      }, () {
+        // Backup case.
+        expectRemoveEvent('file.txt');
+        expectAddEvent('file.txt');
+      });
+    });
+
+    test(
+        'reports a modification when a file is moved and then immediately '
+        'recreated', () async {
+      writeFile('old.txt');
+      await startWatcher();
+
+      renameFile('old.txt', 'new.txt');
+      writeFile('old.txt', contents: 're-created');
+
+      await allowEither(() {
+        inAnyOrder([isModifyEvent('old.txt'), isAddEvent('new.txt')]);
+      }, () {
+        // Backup case.
+        expectRemoveEvent('old.txt');
+        expectAddEvent('new.txt');
+        expectAddEvent('old.txt');
+      });
+    });
+
+    test(
+        'reports a removal when a file is modified and then immediately '
+        'removed', () async {
+      writeFile('file.txt');
+      await startWatcher();
+
+      writeFile('file.txt', contents: 'modified');
+      deleteFile('file.txt');
+
+      // Backup case.
+      await allowModifyEvent('file.txt');
+
+      await expectRemoveEvent('file.txt');
+    });
+
+    test('reports an add when a file is added and then immediately modified',
+        () async {
+      await startWatcher();
+
+      writeFile('file.txt');
+      writeFile('file.txt', contents: 'modified');
+
+      await expectAddEvent('file.txt');
+
+      // Backup case.
+      startClosingEventStream();
+      await allowModifyEvent('file.txt');
+    });
+  });
+
+  group('subdirectories', () {
+    test('watches files in subdirectories', () async {
+      await startWatcher();
+      writeFile('a/b/c/d/file.txt');
+      await expectAddEvent('a/b/c/d/file.txt');
+    });
+
+    test(
+        'notifies when a subdirectory is moved within the watched directory '
+        'and then its contents are modified', () async {
+      writeFile('old/file.txt');
+      await startWatcher();
+
+      renameDir('old', 'new');
+      await inAnyOrder(
+          [isRemoveEvent('old/file.txt'), isAddEvent('new/file.txt')]);
+
+      writeFile('new/file.txt', contents: 'modified');
+      await expectModifyEvent('new/file.txt');
+    });
+
+    test('notifies when a file is replaced by a subdirectory', () async {
+      writeFile('new');
+      writeFile('old/file.txt');
+      await startWatcher();
+
+      deleteFile('new');
+      renameDir('old', 'new');
+      await inAnyOrder([
+        isRemoveEvent('new'),
+        isRemoveEvent('old/file.txt'),
+        isAddEvent('new/file.txt')
+      ]);
+    });
+
+    test('notifies when a subdirectory is replaced by a file', () async {
+      writeFile('old');
+      writeFile('new/file.txt');
+      await startWatcher();
+
+      renameDir('new', 'newer');
+      renameFile('old', 'new');
+      await inAnyOrder([
+        isRemoveEvent('new/file.txt'),
+        isAddEvent('newer/file.txt'),
+        isRemoveEvent('old'),
+        isAddEvent('new')
+      ]);
+    }, onPlatform: {
+      'windows': const Skip('https://github.com/dart-lang/watcher/issues/21')
+    });
+
+    test('emits events for many nested files added at once', () async {
+      withPermutations((i, j, k) => writeFile('sub/sub-$i/sub-$j/file-$k.txt'));
+
+      createDir('dir');
+      await startWatcher(path: 'dir');
+      renameDir('sub', 'dir/sub');
+
+      await inAnyOrder(withPermutations(
+          (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+    });
+
+    test('emits events for many nested files removed at once', () async {
+      withPermutations(
+          (i, j, k) => writeFile('dir/sub/sub-$i/sub-$j/file-$k.txt'));
+
+      createDir('dir');
+      await startWatcher(path: 'dir');
+
+      // Rename the directory rather than deleting it because native watchers
+      // report a rename as a single DELETE event for the directory, whereas
+      // they report recursive deletion with DELETE events for every file in the
+      // directory.
+      renameDir('dir/sub', 'sub');
+
+      await inAnyOrder(withPermutations(
+          (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+    });
+
+    test('emits events for many nested files moved at once', () async {
+      withPermutations(
+          (i, j, k) => writeFile('dir/old/sub-$i/sub-$j/file-$k.txt'));
+
+      createDir('dir');
+      await startWatcher(path: 'dir');
+      renameDir('dir/old', 'dir/new');
+
+      await inAnyOrder(unionAll(withPermutations((i, j, k) {
+        return {
+          isRemoveEvent('dir/old/sub-$i/sub-$j/file-$k.txt'),
+          isAddEvent('dir/new/sub-$i/sub-$j/file-$k.txt')
+        };
+      })));
+    });
+
+    test(
+        'emits events for many files added at once in a subdirectory with the '
+        'same name as a removed file', () async {
+      writeFile('dir/sub');
+      withPermutations((i, j, k) => writeFile('old/sub-$i/sub-$j/file-$k.txt'));
+      await startWatcher(path: 'dir');
+
+      deleteFile('dir/sub');
+      renameDir('old', 'dir/sub');
+
+      var events = withPermutations(
+          (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'));
+      events.add(isRemoveEvent('dir/sub'));
+      await inAnyOrder(events);
+    });
+  });
+}
diff --git a/pkgs/watcher/test/directory_watcher/windows_test.dart b/pkgs/watcher/test/directory_watcher/windows_test.dart
new file mode 100644
index 0000000..499e7fb
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/windows_test.dart
@@ -0,0 +1,23 @@
+// 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.
+
+@TestOn('windows')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/windows.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = WindowsDirectoryWatcher.new;
+
+  group('Shared Tests:', sharedTests);
+
+  test('DirectoryWatcher creates a WindowsDirectoryWatcher on Windows', () {
+    expect(DirectoryWatcher('.'), const TypeMatcher<WindowsDirectoryWatcher>());
+  });
+}
diff --git a/pkgs/watcher/test/file_watcher/native_test.dart b/pkgs/watcher/test/file_watcher/native_test.dart
new file mode 100644
index 0000000..0d4ad63
--- /dev/null
+++ b/pkgs/watcher/test/file_watcher/native_test.dart
@@ -0,0 +1,22 @@
+// 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.
+
+@TestOn('linux || mac-os')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/file_watcher/native.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = NativeFileWatcher.new;
+
+  setUp(() {
+    writeFile('file.txt');
+  });
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/file_watcher/polling_test.dart b/pkgs/watcher/test/file_watcher/polling_test.dart
new file mode 100644
index 0000000..861fcb2
--- /dev/null
+++ b/pkgs/watcher/test/file_watcher/polling_test.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = (file) =>
+      PollingFileWatcher(file, pollingDelay: const Duration(milliseconds: 100));
+
+  setUp(() {
+    writeFile('file.txt');
+  });
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/file_watcher/shared.dart b/pkgs/watcher/test/file_watcher/shared.dart
new file mode 100644
index 0000000..081b92e
--- /dev/null
+++ b/pkgs/watcher/test/file_watcher/shared.dart
@@ -0,0 +1,73 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void sharedTests() {
+  test("doesn't notify if the file isn't modified", () async {
+    await startWatcher(path: 'file.txt');
+    await pumpEventQueue();
+    deleteFile('file.txt');
+    await expectRemoveEvent('file.txt');
+  });
+
+  test('notifies when a file is modified', () async {
+    await startWatcher(path: 'file.txt');
+    writeFile('file.txt', contents: 'modified');
+    await expectModifyEvent('file.txt');
+  });
+
+  test('notifies when a file is removed', () async {
+    await startWatcher(path: 'file.txt');
+    deleteFile('file.txt');
+    await expectRemoveEvent('file.txt');
+  });
+
+  test('notifies when a file is modified multiple times', () async {
+    await startWatcher(path: 'file.txt');
+    writeFile('file.txt', contents: 'modified');
+    await expectModifyEvent('file.txt');
+    writeFile('file.txt', contents: 'modified again');
+    await expectModifyEvent('file.txt');
+  });
+
+  test('notifies even if the file contents are unchanged', () async {
+    await startWatcher(path: 'file.txt');
+    writeFile('file.txt');
+    await expectModifyEvent('file.txt');
+  });
+
+  test('emits a remove event when the watched file is moved away', () async {
+    await startWatcher(path: 'file.txt');
+    renameFile('file.txt', 'new.txt');
+    await expectRemoveEvent('file.txt');
+  });
+
+  test(
+      'emits a modify event when another file is moved on top of the watched '
+      'file', () async {
+    writeFile('old.txt');
+    await startWatcher(path: 'file.txt');
+    renameFile('old.txt', 'file.txt');
+    await expectModifyEvent('file.txt');
+  });
+
+  // Regression test for a race condition.
+  test('closes the watcher immediately after deleting the file', () async {
+    writeFile('old.txt');
+    var watcher = createWatcher(path: 'file.txt');
+    var sub = watcher.events.listen(null);
+
+    deleteFile('file.txt');
+    await Future<void>.delayed(const Duration(milliseconds: 10));
+    await sub.cancel();
+  });
+
+  test('ready completes even if file does not exist', () async {
+    // startWatcher awaits 'ready'
+    await startWatcher(path: 'foo/bar/baz');
+  });
+}
diff --git a/pkgs/watcher/test/no_subscription/linux_test.dart b/pkgs/watcher/test/no_subscription/linux_test.dart
new file mode 100644
index 0000000..aac0810
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/linux_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('linux')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/linux.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = LinuxDirectoryWatcher.new;
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/no_subscription/mac_os_test.dart b/pkgs/watcher/test/no_subscription/mac_os_test.dart
new file mode 100644
index 0000000..55a8308
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/mac_os_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('mac-os')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/mac_os.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = MacOSDirectoryWatcher.new;
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/no_subscription/polling_test.dart b/pkgs/watcher/test/no_subscription/polling_test.dart
new file mode 100644
index 0000000..bfd2958
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/polling_test.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = PollingDirectoryWatcher.new;
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/no_subscription/shared.dart b/pkgs/watcher/test/no_subscription/shared.dart
new file mode 100644
index 0000000..e7a6144
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/shared.dart
@@ -0,0 +1,54 @@
+// Copyright (c) 2012, 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:async/async.dart';
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+
+void sharedTests() {
+  test('does not notify for changes when there are no subscribers', () async {
+    // Note that this test doesn't rely as heavily on the test functions in
+    // utils.dart because it needs to be very explicit about when the event
+    // stream is and is not subscribed.
+    var watcher = createWatcher();
+    var queue = StreamQueue(watcher.events);
+    unawaited(queue.hasNext);
+
+    var future =
+        expectLater(queue, emits(isWatchEvent(ChangeType.ADD, 'file.txt')));
+    expect(queue, neverEmits(anything));
+
+    await watcher.ready;
+
+    writeFile('file.txt');
+
+    await future;
+
+    // Unsubscribe.
+    await queue.cancel(immediate: true);
+
+    // Now write a file while we aren't listening.
+    writeFile('unwatched.txt');
+
+    queue = StreamQueue(watcher.events);
+    future =
+        expectLater(queue, emits(isWatchEvent(ChangeType.ADD, 'added.txt')));
+    expect(queue, neverEmits(isWatchEvent(ChangeType.ADD, 'unwatched.txt')));
+
+    // Wait until the watcher is ready to dispatch events again.
+    await watcher.ready;
+
+    // And add a third file.
+    writeFile('added.txt');
+
+    // Wait until we get an event for the third file.
+    await future;
+
+    await queue.cancel(immediate: true);
+  });
+}
diff --git a/pkgs/watcher/test/no_subscription/windows_test.dart b/pkgs/watcher/test/no_subscription/windows_test.dart
new file mode 100644
index 0000000..9f9e5a9
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/windows_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2022, 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('windows')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/windows.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = WindowsDirectoryWatcher.new;
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/path_set_test.dart b/pkgs/watcher/test/path_set_test.dart
new file mode 100644
index 0000000..61ab2cd
--- /dev/null
+++ b/pkgs/watcher/test/path_set_test.dart
@@ -0,0 +1,228 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:watcher/src/path_set.dart';
+
+Matcher containsPath(String path) => predicate(
+    (paths) => paths is PathSet && paths.contains(path),
+    'set contains "$path"');
+
+Matcher containsDir(String path) => predicate(
+    (paths) => paths is PathSet && paths.containsDir(path),
+    'set contains directory "$path"');
+
+void main() {
+  late PathSet paths;
+  setUp(() => paths = PathSet('root'));
+
+  group('adding a path', () {
+    test('stores the path in the set', () {
+      paths.add('root/path/to/file');
+      expect(paths, containsPath('root/path/to/file'));
+    });
+
+    test("that's a subdir of another path keeps both in the set", () {
+      paths.add('root/path');
+      paths.add('root/path/to/file');
+      expect(paths, containsPath('root/path'));
+      expect(paths, containsPath('root/path/to/file'));
+    });
+
+    test("that's not normalized normalizes the path before storing it", () {
+      paths.add('root/../root/path/to/../to/././file');
+      expect(paths, containsPath('root/path/to/file'));
+    });
+
+    test("that's absolute normalizes the path before storing it", () {
+      paths.add(p.absolute('root/path/to/file'));
+      expect(paths, containsPath('root/path/to/file'));
+    });
+  });
+
+  group('removing a path', () {
+    test("that's in the set removes and returns that path", () {
+      paths.add('root/path/to/file');
+      expect(paths.remove('root/path/to/file'),
+          unorderedEquals([p.normalize('root/path/to/file')]));
+      expect(paths, isNot(containsPath('root/path/to/file')));
+    });
+
+    test("that's not in the set returns an empty set", () {
+      paths.add('root/path/to/file');
+      expect(paths.remove('root/path/to/nothing'), isEmpty);
+    });
+
+    test("that's a directory removes and returns all files beneath it", () {
+      paths.add('root/outside');
+      paths.add('root/path/to/one');
+      paths.add('root/path/to/two');
+      paths.add('root/path/to/sub/three');
+
+      expect(
+          paths.remove('root/path'),
+          unorderedEquals([
+            'root/path/to/one',
+            'root/path/to/two',
+            'root/path/to/sub/three'
+          ].map(p.normalize)));
+
+      expect(paths, containsPath('root/outside'));
+      expect(paths, isNot(containsPath('root/path/to/one')));
+      expect(paths, isNot(containsPath('root/path/to/two')));
+      expect(paths, isNot(containsPath('root/path/to/sub/three')));
+    });
+
+    test(
+        "that's a directory in the set removes and returns it and all files "
+        'beneath it', () {
+      paths.add('root/path');
+      paths.add('root/path/to/one');
+      paths.add('root/path/to/two');
+      paths.add('root/path/to/sub/three');
+
+      expect(
+          paths.remove('root/path'),
+          unorderedEquals([
+            'root/path',
+            'root/path/to/one',
+            'root/path/to/two',
+            'root/path/to/sub/three'
+          ].map(p.normalize)));
+
+      expect(paths, isNot(containsPath('root/path')));
+      expect(paths, isNot(containsPath('root/path/to/one')));
+      expect(paths, isNot(containsPath('root/path/to/two')));
+      expect(paths, isNot(containsPath('root/path/to/sub/three')));
+    });
+
+    test("that's not normalized removes and returns the normalized path", () {
+      paths.add('root/path/to/file');
+      expect(paths.remove('root/../root/path/to/../to/./file'),
+          unorderedEquals([p.normalize('root/path/to/file')]));
+    });
+
+    test("that's absolute removes and returns the normalized path", () {
+      paths.add('root/path/to/file');
+      expect(paths.remove(p.absolute('root/path/to/file')),
+          unorderedEquals([p.normalize('root/path/to/file')]));
+    });
+  });
+
+  group('containsPath()', () {
+    test('returns false for a non-existent path', () {
+      paths.add('root/path/to/file');
+      expect(paths, isNot(containsPath('root/path/to/nothing')));
+    });
+
+    test("returns false for a directory that wasn't added explicitly", () {
+      paths.add('root/path/to/file');
+      expect(paths, isNot(containsPath('root/path')));
+    });
+
+    test('returns true for a directory that was added explicitly', () {
+      paths.add('root/path');
+      paths.add('root/path/to/file');
+      expect(paths, containsPath('root/path'));
+    });
+
+    test('with a non-normalized path normalizes the path before looking it up',
+        () {
+      paths.add('root/path/to/file');
+      expect(paths, containsPath('root/../root/path/to/../to/././file'));
+    });
+
+    test('with an absolute path normalizes the path before looking it up', () {
+      paths.add('root/path/to/file');
+      expect(paths, containsPath(p.absolute('root/path/to/file')));
+    });
+  });
+
+  group('containsDir()', () {
+    test('returns true for a directory that was added implicitly', () {
+      paths.add('root/path/to/file');
+      expect(paths, containsDir('root/path'));
+      expect(paths, containsDir('root/path/to'));
+    });
+
+    test('returns true for a directory that was added explicitly', () {
+      paths.add('root/path');
+      paths.add('root/path/to/file');
+      expect(paths, containsDir('root/path'));
+    });
+
+    test("returns false for a directory that wasn't added", () {
+      expect(paths, isNot(containsDir('root/nothing')));
+    });
+
+    test('returns false for a non-directory path that was added', () {
+      paths.add('root/path/to/file');
+      expect(paths, isNot(containsDir('root/path/to/file')));
+    });
+
+    test(
+        'returns false for a directory that was added implicitly and then '
+        'removed implicitly', () {
+      paths.add('root/path/to/file');
+      paths.remove('root/path/to/file');
+      expect(paths, isNot(containsDir('root/path')));
+    });
+
+    test(
+        'returns false for a directory that was added explicitly whose '
+        'children were then removed', () {
+      paths.add('root/path');
+      paths.add('root/path/to/file');
+      paths.remove('root/path/to/file');
+      expect(paths, isNot(containsDir('root/path')));
+    });
+
+    test('with a non-normalized path normalizes the path before looking it up',
+        () {
+      paths.add('root/path/to/file');
+      expect(paths, containsDir('root/../root/path/to/../to/.'));
+    });
+
+    test('with an absolute path normalizes the path before looking it up', () {
+      paths.add('root/path/to/file');
+      expect(paths, containsDir(p.absolute('root/path')));
+    });
+  });
+
+  group('paths', () {
+    test('returns paths added to the set', () {
+      paths.add('root/path');
+      paths.add('root/path/to/one');
+      paths.add('root/path/to/two');
+
+      expect(
+          paths.paths,
+          unorderedEquals([
+            'root/path',
+            'root/path/to/one',
+            'root/path/to/two',
+          ].map(p.normalize)));
+    });
+
+    test("doesn't return paths removed from the set", () {
+      paths.add('root/path/to/one');
+      paths.add('root/path/to/two');
+      paths.remove('root/path/to/two');
+
+      expect(paths.paths, unorderedEquals([p.normalize('root/path/to/one')]));
+    });
+  });
+
+  group('clear', () {
+    test('removes all paths from the set', () {
+      paths.add('root/path');
+      paths.add('root/path/to/one');
+      paths.add('root/path/to/two');
+
+      paths.clear();
+      expect(paths.paths, isEmpty);
+    });
+  });
+}
diff --git a/pkgs/watcher/test/ready/linux_test.dart b/pkgs/watcher/test/ready/linux_test.dart
new file mode 100644
index 0000000..aac0810
--- /dev/null
+++ b/pkgs/watcher/test/ready/linux_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('linux')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/linux.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = LinuxDirectoryWatcher.new;
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/ready/mac_os_test.dart b/pkgs/watcher/test/ready/mac_os_test.dart
new file mode 100644
index 0000000..55a8308
--- /dev/null
+++ b/pkgs/watcher/test/ready/mac_os_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('mac-os')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/mac_os.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = MacOSDirectoryWatcher.new;
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/ready/polling_test.dart b/pkgs/watcher/test/ready/polling_test.dart
new file mode 100644
index 0000000..bfd2958
--- /dev/null
+++ b/pkgs/watcher/test/ready/polling_test.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = PollingDirectoryWatcher.new;
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/ready/shared.dart b/pkgs/watcher/test/ready/shared.dart
new file mode 100644
index 0000000..ab2c3e1
--- /dev/null
+++ b/pkgs/watcher/test/ready/shared.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2012, 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:test/test.dart';
+
+import '../utils.dart';
+
+void sharedTests() {
+  test('ready does not complete until after subscription', () async {
+    var watcher = createWatcher();
+
+    var ready = false;
+    unawaited(watcher.ready.then((_) {
+      ready = true;
+    }));
+    await pumpEventQueue();
+
+    expect(ready, isFalse);
+
+    // Subscribe to the events.
+    var subscription = watcher.events.listen((event) {});
+
+    await watcher.ready;
+
+    // Should eventually be ready.
+    expect(watcher.isReady, isTrue);
+
+    await subscription.cancel();
+  });
+
+  test('ready completes immediately when already ready', () async {
+    var watcher = createWatcher();
+
+    // Subscribe to the events.
+    var subscription = watcher.events.listen((event) {});
+
+    // Allow watcher to become ready
+    await watcher.ready;
+
+    // Ensure ready completes immediately
+    expect(
+      watcher.ready.timeout(
+        const Duration(milliseconds: 0),
+        onTimeout: () => throw StateError('Does not complete immediately'),
+      ),
+      completes,
+    );
+
+    await subscription.cancel();
+  });
+
+  test('ready returns a future that does not complete after unsubscribing',
+      () async {
+    var watcher = createWatcher();
+
+    // Subscribe to the events.
+    var subscription = watcher.events.listen((event) {});
+
+    // Wait until ready.
+    await watcher.ready;
+
+    // Now unsubscribe.
+    await subscription.cancel();
+
+    // Should be back to not ready.
+    expect(watcher.ready, doesNotComplete);
+  });
+
+  test('ready completes even if directory does not exist', () async {
+    var watcher = createWatcher(path: 'does/not/exist');
+
+    // Subscribe to the events (else ready will never fire).
+    var subscription = watcher.events.listen((event) {}, onError: (error) {});
+
+    // Expect ready still completes.
+    await watcher.ready;
+
+    // Now unsubscribe.
+    await subscription.cancel();
+  });
+}
diff --git a/pkgs/watcher/test/ready/windows_test.dart b/pkgs/watcher/test/ready/windows_test.dart
new file mode 100644
index 0000000..9f9e5a9
--- /dev/null
+++ b/pkgs/watcher/test/ready/windows_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2022, 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('windows')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/windows.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+  watcherFactory = WindowsDirectoryWatcher.new;
+
+  sharedTests();
+}
diff --git a/pkgs/watcher/test/utils.dart b/pkgs/watcher/test/utils.dart
new file mode 100644
index 0000000..7867b9f
--- /dev/null
+++ b/pkgs/watcher/test/utils.dart
@@ -0,0 +1,288 @@
+// Copyright (c) 2012, 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:io';
+
+import 'package:async/async.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:watcher/src/stat.dart';
+import 'package:watcher/watcher.dart';
+
+typedef WatcherFactory = Watcher Function(String directory);
+
+/// Sets the function used to create the watcher.
+set watcherFactory(WatcherFactory factory) {
+  _watcherFactory = factory;
+}
+
+/// The mock modification times (in milliseconds since epoch) for each file.
+///
+/// The actual file system has pretty coarse granularity for file modification
+/// times. This means using the real file system requires us to put delays in
+/// the tests to ensure we wait long enough between operations for the mod time
+/// to be different.
+///
+/// Instead, we'll just mock that out. Each time a file is written, we manually
+/// increment the mod time for that file instantly.
+final _mockFileModificationTimes = <String, int>{};
+
+late WatcherFactory _watcherFactory;
+
+/// Creates a new [Watcher] that watches a temporary file or directory.
+///
+/// If [path] is provided, watches a subdirectory in the sandbox with that name.
+Watcher createWatcher({String? path}) {
+  if (path == null) {
+    path = d.sandbox;
+  } else {
+    path = p.join(d.sandbox, path);
+  }
+
+  return _watcherFactory(path);
+}
+
+/// The stream of events from the watcher started with [startWatcher].
+late StreamQueue<WatchEvent> _watcherEvents;
+
+/// Whether the event stream has been closed.
+///
+/// If this is not done by a test (by calling [startClosingEventStream]) it will
+/// be done automatically via [addTearDown] in [startWatcher].
+var _hasClosedStream = true;
+
+/// Creates a new [Watcher] that watches a temporary file or directory and
+/// starts monitoring it for events.
+///
+/// If [path] is provided, watches a path in the sandbox with that name.
+Future<void> startWatcher({String? path}) async {
+  mockGetModificationTime((path) {
+    final normalized = p.normalize(p.relative(path, from: d.sandbox));
+
+    // Make sure we got a path in the sandbox.
+    assert(p.isRelative(normalized) && !normalized.startsWith('..'),
+        'Path is not in the sandbox: $path not in ${d.sandbox}');
+
+    var mtime = _mockFileModificationTimes[normalized];
+    return mtime != null ? DateTime.fromMillisecondsSinceEpoch(mtime) : null;
+  });
+
+  // We want to wait until we're ready *after* we subscribe to the watcher's
+  // events.
+  var watcher = createWatcher(path: path);
+  _watcherEvents = StreamQueue(watcher.events);
+  // Forces a subscription to the underlying stream.
+  unawaited(_watcherEvents.hasNext);
+
+  _hasClosedStream = false;
+  addTearDown(startClosingEventStream);
+
+  await watcher.ready;
+}
+
+/// Schedule closing the watcher stream after the event queue has been pumped.
+///
+/// This is necessary when events are allowed to occur, but don't have to occur,
+/// at the end of a test. Otherwise, if they don't occur, the test will wait
+/// indefinitely because they might in the future and because the watcher is
+/// normally only closed after the test completes.
+void startClosingEventStream() async {
+  if (_hasClosedStream) return;
+  _hasClosedStream = true;
+  await pumpEventQueue();
+  await _watcherEvents.cancel(immediate: true);
+}
+
+/// A list of [StreamMatcher]s that have been collected using
+/// [_collectStreamMatcher].
+List<StreamMatcher>? _collectedStreamMatchers;
+
+/// Collects all stream matchers that are registered within [block] into a
+/// single stream matcher.
+///
+/// The returned matcher will match each of the collected matchers in order.
+StreamMatcher _collectStreamMatcher(void Function() block) {
+  var oldStreamMatchers = _collectedStreamMatchers;
+  var collectedStreamMatchers = _collectedStreamMatchers = <StreamMatcher>[];
+  try {
+    block();
+    return emitsInOrder(collectedStreamMatchers);
+  } finally {
+    _collectedStreamMatchers = oldStreamMatchers;
+  }
+}
+
+/// Either add [streamMatcher] as an expectation to [_watcherEvents], or collect
+/// it with [_collectStreamMatcher].
+///
+/// [streamMatcher] can be a [StreamMatcher], a [Matcher], or a value.
+Future _expectOrCollect(Matcher streamMatcher) {
+  var collectedStreamMatchers = _collectedStreamMatchers;
+  if (collectedStreamMatchers != null) {
+    collectedStreamMatchers.add(emits(streamMatcher));
+    return Future.sync(() {});
+  } else {
+    return expectLater(_watcherEvents, emits(streamMatcher));
+  }
+}
+
+/// Expects that [matchers] will match emitted events in any order.
+///
+/// [matchers] may be [Matcher]s or values, but not [StreamMatcher]s.
+Future inAnyOrder(Iterable matchers) {
+  matchers = matchers.toSet();
+  return _expectOrCollect(emitsInAnyOrder(matchers));
+}
+
+/// Expects that the expectations established in either [block1] or [block2]
+/// will match the emitted events.
+///
+/// If both blocks match, the one that consumed more events will be used.
+Future allowEither(void Function() block1, void Function() block2) =>
+    _expectOrCollect(emitsAnyOf(
+        [_collectStreamMatcher(block1), _collectStreamMatcher(block2)]));
+
+/// Allows the expectations established in [block] to match the emitted events.
+///
+/// If the expectations in [block] don't match, no error will be raised and no
+/// events will be consumed. If this is used at the end of a test,
+/// [startClosingEventStream] should be called before it.
+Future allowEvents(void Function() block) =>
+    _expectOrCollect(mayEmit(_collectStreamMatcher(block)));
+
+/// Returns a StreamMatcher that matches a [WatchEvent] with the given [type]
+/// and [path].
+Matcher isWatchEvent(ChangeType type, String path) {
+  return predicate((e) {
+    return e is WatchEvent &&
+        e.type == type &&
+        e.path == p.join(d.sandbox, p.normalize(path));
+  }, 'is $type $path');
+}
+
+/// Returns a [Matcher] that matches a [WatchEvent] for an add event for [path].
+Matcher isAddEvent(String path) => isWatchEvent(ChangeType.ADD, path);
+
+/// Returns a [Matcher] that matches a [WatchEvent] for a modification event for
+/// [path].
+Matcher isModifyEvent(String path) => isWatchEvent(ChangeType.MODIFY, path);
+
+/// Returns a [Matcher] that matches a [WatchEvent] for a removal event for
+/// [path].
+Matcher isRemoveEvent(String path) => isWatchEvent(ChangeType.REMOVE, path);
+
+/// Expects that the next event emitted will be for an add event for [path].
+Future expectAddEvent(String path) =>
+    _expectOrCollect(isWatchEvent(ChangeType.ADD, path));
+
+/// Expects that the next event emitted will be for a modification event for
+/// [path].
+Future expectModifyEvent(String path) =>
+    _expectOrCollect(isWatchEvent(ChangeType.MODIFY, path));
+
+/// Expects that the next event emitted will be for a removal event for [path].
+Future expectRemoveEvent(String path) =>
+    _expectOrCollect(isWatchEvent(ChangeType.REMOVE, path));
+
+/// Consumes a modification event for [path] if one is emitted at this point in
+/// the schedule, but doesn't throw an error if it isn't.
+///
+/// If this is used at the end of a test, [startClosingEventStream] should be
+/// called before it.
+Future allowModifyEvent(String path) =>
+    _expectOrCollect(mayEmit(isWatchEvent(ChangeType.MODIFY, path)));
+
+/// Track a fake timestamp to be used when writing files. This always increases
+/// so that files that are deleted and re-created do not have their timestamp
+/// set back to a previously used value.
+int _nextTimestamp = 1;
+
+/// Schedules writing a file in the sandbox at [path] with [contents].
+///
+/// If [contents] is omitted, creates an empty file. If [updateModified] is
+/// `false`, the mock file modification time is not changed.
+void writeFile(String path, {String? contents, bool? updateModified}) {
+  contents ??= '';
+  updateModified ??= true;
+
+  var fullPath = p.join(d.sandbox, path);
+
+  // Create any needed subdirectories.
+  var dir = Directory(p.dirname(fullPath));
+  if (!dir.existsSync()) {
+    dir.createSync(recursive: true);
+  }
+
+  File(fullPath).writeAsStringSync(contents);
+
+  if (updateModified) {
+    path = p.normalize(path);
+
+    _mockFileModificationTimes[path] = _nextTimestamp++;
+  }
+}
+
+/// Schedules deleting a file in the sandbox at [path].
+void deleteFile(String path) {
+  File(p.join(d.sandbox, path)).deleteSync();
+
+  _mockFileModificationTimes.remove(path);
+}
+
+/// Schedules renaming a file in the sandbox from [from] to [to].
+void renameFile(String from, String to) {
+  File(p.join(d.sandbox, from)).renameSync(p.join(d.sandbox, to));
+
+  // Make sure we always use the same separator on Windows.
+  to = p.normalize(to);
+
+  _mockFileModificationTimes.update(to, (value) => value + 1,
+      ifAbsent: () => 1);
+}
+
+/// Schedules creating a directory in the sandbox at [path].
+void createDir(String path) {
+  Directory(p.join(d.sandbox, path)).createSync();
+}
+
+/// Schedules renaming a directory in the sandbox from [from] to [to].
+void renameDir(String from, String to) {
+  Directory(p.join(d.sandbox, from)).renameSync(p.join(d.sandbox, to));
+
+  // Migrate timestamps for any files in this folder.
+  final knownFilePaths = _mockFileModificationTimes.keys.toList();
+  for (final filePath in knownFilePaths) {
+    if (p.isWithin(from, filePath)) {
+      _mockFileModificationTimes[filePath.replaceAll(from, to)] =
+          _mockFileModificationTimes[filePath]!;
+      _mockFileModificationTimes.remove(filePath);
+    }
+  }
+}
+
+/// Schedules deleting a directory in the sandbox at [path].
+void deleteDir(String path) {
+  Directory(p.join(d.sandbox, path)).deleteSync(recursive: true);
+}
+
+/// Runs [callback] with every permutation of non-negative numbers for each
+/// argument less than [limit].
+///
+/// Returns a set of all values returns by [callback].
+///
+/// [limit] defaults to 3.
+Set<S> withPermutations<S>(S Function(int, int, int) callback, {int? limit}) {
+  limit ??= 3;
+  var results = <S>{};
+  for (var i = 0; i < limit; i++) {
+    for (var j = 0; j < limit; j++) {
+      for (var k = 0; k < limit; k++) {
+        results.add(callback(i, j, k));
+      }
+    }
+  }
+  return results;
+}
diff --git a/pkgs/yaml/.gitignore b/pkgs/yaml/.gitignore
new file mode 100644
index 0000000..ab3cb76
--- /dev/null
+++ b/pkgs/yaml/.gitignore
@@ -0,0 +1,16 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.dart_tool/
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pkgs/yaml/CHANGELOG.md b/pkgs/yaml/CHANGELOG.md
new file mode 100644
index 0000000..3f9d3fd
--- /dev/null
+++ b/pkgs/yaml/CHANGELOG.md
@@ -0,0 +1,199 @@
+## 3.1.3
+
+* Require Dart 3.4
+* Fix UTF-16 surrogate pair handling in plain scaler.
+* Move to `dart-lang/tools` monorepo.
+
+## 3.1.2
+
+* Require Dart 2.19
+* Added `topics` in `pubspec.yaml`.
+
+## 3.1.1
+
+* Switch to using package:lints.
+* Populate the pubspec `repository` field.
+
+## 3.1.0
+
+* `loadYaml` and related functions now accept a `recover` flag instructing the parser
+  to attempt to recover from parse errors and may return invalid or synthetic nodes.
+  When recovering, an `ErrorListener` can also be supplied to listen for errors that
+  are recovered from.
+* Drop dependency on `package:charcode`.
+
+## 3.0.0
+
+* Stable null safety release.
+
+## 3.0.0-nullsafety.0
+
+* Updated to support 2.12.0 and null safety.
+* Allow `YamlNode`s to be wrapped with an optional `style` parameter.
+* **BREAKING** The `sourceUrl` named argument is statically typed as `Uri`
+  instead of allowing `String` or `Uri`.
+
+## 2.2.1
+
+* Update min Dart SDK to `2.4.0`.
+* Fixed span for null nodes in block lists.
+
+## 2.2.0
+
+* POSSIBLY BREAKING CHANGE: Make `YamlMap` preserve parsed key order.
+  This is breaking because some programs may rely on the
+  `HashMap` sort order.
+
+## 2.1.16
+
+* Fixed deprecated API usage in README.
+* Fixed lints that affect package score.
+
+## 2.1.15
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 2.1.14
+
+* Remove use of deprecated features.
+* Updated SDK version to 2.0.0-dev.17.0
+
+## 2.1.13
+
+* Stop using comment-based generic syntax.
+
+## 2.1.12
+
+* Properly refuse mappings with duplicate keys.
+
+## 2.1.11
+
+* Fix an infinite loop when parsing some invalid documents.
+
+## 2.1.10
+
+* Support `string_scanner` 1.0.0.
+
+## 2.1.9
+
+* Fix all strong-mode warnings.
+
+## 2.1.8
+
+* Remove the dependency on `path`, since we don't actually import it.
+
+## 2.1.7
+
+* Fix more strong mode warnings.
+
+## 2.1.6
+
+* Fix two analysis issues with DDC's strong mode.
+
+## 2.1.5
+
+* Fix a bug with 2.1.4 where source span information was being discarded for
+  scalar values.
+
+## 2.1.4
+
+* Substantially improve performance.
+
+## 2.1.3
+
+* Add a hint that a colon might be missing when a mapping value is found in the
+  wrong context.
+
+## 2.1.2
+
+* Fix a crashing bug when parsing block scalars.
+
+## 2.1.1
+
+* Properly scope `SourceSpan`s for scalar values surrounded by whitespace.
+
+## 2.1.0
+
+* Rewrite the parser for a 10x speed improvement.
+
+* Support anchors and aliases (`&foo` and `*foo`).
+
+* Support explicit tags (e.g. `!!str`). Note that user-defined tags are still
+  not fully supported.
+
+* `%YAML` and `%TAG` directives are now parsed, although again user-defined tags
+  are not fully supported.
+
+* `YamlScalar`, `YamlList`, and `YamlMap` now expose the styles in which they
+  were written (for example plain vs folded, block vs flow).
+
+* A `yamlWarningCallback` field is exposed. This field can be used to customize
+  how YAML warnings are displayed.
+
+## 2.0.1+1
+
+* Fix an import in a test.
+
+* Widen the version constraint on the `collection` package.
+
+## 2.0.1
+
+* Fix a few lingering references to the old `Span` class in documentation and
+  tests.
+
+## 2.0.0
+
+* Switch from `source_maps`' `Span` class to `source_span`'s `SourceSpan` class.
+
+* For consistency with `source_span` and `string_scanner`, all `sourceName`
+  parameters have been renamed to `sourceUrl`. They now accept Urls as well as
+  Strings.
+
+## 1.1.1
+
+* Fix broken type arguments that caused breakage on dart2js.
+
+* Fix an analyzer warning in `yaml_node_wrapper.dart`.
+
+## 1.1.0
+
+* Add new publicly-accessible constructors for `YamlNode` subclasses. These
+  constructors make it possible to use the same API to access non-YAML data as
+  YAML data.
+
+* Make `YamlException` inherit from source_map's `SpanFormatException`. This
+  improves the error formatting and allows callers access to source range
+  information.
+
+## 1.0.0+1
+
+* Fix a variable name typo.
+
+## 1.0.0
+
+* **Backwards incompatibility**: The data structures returned by `loadYaml` and
+  `loadYamlStream` are now immutable.
+
+* **Backwards incompatibility**: The interface of the `YamlMap` class has
+  changed substantially in numerous ways. External users may no longer construct
+  their own instances.
+
+* Maps and lists returned by `loadYaml` and `loadYamlStream` now contain
+  information about their source locations.
+
+* A new `loadYamlNode` function returns the source location of top-level scalars
+  as well.
+
+## 0.10.0
+
+* Improve error messages when a file fails to parse.
+
+## 0.9.0+2
+
+* Ensure that maps are order-independent when used as map keys.
+
+## 0.9.0+1
+
+* The `YamlMap` class is deprecated. In a future version, maps returned by
+  `loadYaml` and `loadYamlStream` will be Dart `HashMap`s with a custom equality
+  operation.
diff --git a/pkgs/yaml/LICENSE b/pkgs/yaml/LICENSE
new file mode 100644
index 0000000..e7589cb
--- /dev/null
+++ b/pkgs/yaml/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2014, the Dart project authors.
+Copyright (c) 2006, Kirill Simonov.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pkgs/yaml/README.md b/pkgs/yaml/README.md
new file mode 100644
index 0000000..ba56893
--- /dev/null
+++ b/pkgs/yaml/README.md
@@ -0,0 +1,33 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/yaml.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/yaml.yaml)
+[![pub package](https://img.shields.io/pub/v/yaml.svg)](https://pub.dev/packages/yaml)
+[![package publisher](https://img.shields.io/pub/publisher/yaml.svg)](https://pub.dev/packages/yaml/publisher)
+
+
+A parser for [YAML](https://yaml.org/).
+
+## Usage
+
+Use `loadYaml` to load a single document, or `loadYamlStream` to load a
+stream of documents. For example:
+
+```dart
+import 'package:yaml/yaml.dart';
+
+main() {
+  var doc = loadYaml("YAML: YAML Ain't Markup Language");
+  print(doc['YAML']);
+}
+```
+
+This library currently doesn't support dumping to YAML. You should use
+`json.encode` from `dart:convert` instead:
+
+```dart
+import 'dart:convert';
+import 'package:yaml/yaml.dart';
+
+main() {
+  var doc = loadYaml("YAML: YAML Ain't Markup Language");
+  print(json.encode(doc));
+}
+```
diff --git a/pkgs/yaml/analysis_options.yaml b/pkgs/yaml/analysis_options.yaml
new file mode 100644
index 0000000..46e45f0
--- /dev/null
+++ b/pkgs/yaml/analysis_options.yaml
@@ -0,0 +1,18 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+
+linter:
+  rules:
+    - avoid_private_typedef_functions
+    - avoid_redundant_argument_values
+    - avoid_unused_constructor_parameters
+    - cancel_subscriptions
+    - join_return_with_assignment
+    - missing_whitespace_between_adjacent_strings
+    - no_runtimeType_toString
+    - prefer_const_declarations
+    - prefer_expression_function_bodies
+    - use_string_buffers
diff --git a/pkgs/yaml/benchmark/benchmark.dart b/pkgs/yaml/benchmark/benchmark.dart
new file mode 100644
index 0000000..afc3c97
--- /dev/null
+++ b/pkgs/yaml/benchmark/benchmark.dart
@@ -0,0 +1,65 @@
+// Copyright (c) 2015, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:yaml/yaml.dart';
+
+const numTrials = 100;
+const runsPerTrial = 1000;
+
+final source = _loadFile('input.yaml');
+final expected = _loadFile('output.json');
+
+void main(List<String> args) {
+  var best = double.infinity;
+
+  // Run the benchmark several times. This ensures the VM is warmed up and lets
+  // us see how much variance there is.
+  for (var i = 0; i <= numTrials; i++) {
+    var start = DateTime.now();
+
+    // For a single benchmark, convert the source multiple times.
+    Object? result;
+    for (var j = 0; j < runsPerTrial; j++) {
+      result = loadYaml(source);
+    }
+
+    var elapsed =
+        DateTime.now().difference(start).inMilliseconds / runsPerTrial;
+
+    // Keep track of the best run so far.
+    if (elapsed >= best) continue;
+    best = elapsed;
+
+    // Sanity check to make sure the output is what we expect and to make sure
+    // the VM doesn't optimize "dead" code away.
+    if (jsonEncode(result) != expected) {
+      print('Incorrect output:\n${jsonEncode(result)}');
+      exit(1);
+    }
+
+    // Don't print the first run. It's always terrible since the VM hasn't
+    // warmed up yet.
+    if (i == 0) continue;
+    _printResult("Run ${'#$i'.padLeft(3, '')}", elapsed);
+  }
+
+  _printResult('Best   ', best);
+}
+
+String _loadFile(String name) {
+  var path = p.join(p.dirname(p.fromUri(Platform.script)), name);
+  return File(path).readAsStringSync();
+}
+
+void _printResult(String label, double time) {
+  print('$label: ${time.toStringAsFixed(3).padLeft(4, '0')}ms '
+      "${'=' * ((time * 100).toInt())}");
+}
diff --git a/pkgs/yaml/benchmark/input.yaml b/pkgs/yaml/benchmark/input.yaml
new file mode 100644
index 0000000..89bf9dc
--- /dev/null
+++ b/pkgs/yaml/benchmark/input.yaml
@@ -0,0 +1,48 @@
+verb: RecommendCafes
+recipe:
+  - verb: List
+    outputs: ["Cafe[]"]
+  - verb: Fetch
+    inputs: ["Cafe[]"]
+    outputs: ["CafeWithMenu[]"]
+  - verb: Flatten
+    inputs: ["CafeWithMenu[]"]
+    outputs: ["DishOffering[]"]
+  - verb: Score
+    inputs: ["DishOffering[]"]
+    outputs: ["DishOffering[]/Scored"]
+  - verb: Display
+    inputs: ["DishOffering[]/Scored"]
+tags:
+  booleans: [ true, false ]
+  dates:
+  - canonical: 2001-12-15T02:59:43.1Z
+  - iso8601: 2001-12-14t21:59:43.10-05:00
+  - spaced: 2001-12-14 21:59:43.10 -5
+  - date: 2002-12-14
+  numbers:
+  - int: 12345
+  - negative: -345
+  - floating-point: 345.678
+  - hexidecimal: 0x123abc
+  - exponential: 12.3015e+02
+  - octal: 0o14
+  strings:
+  - unicode: "Sosa did fine.\u263A"
+  - control: "\b1998\t1999\t2000\n"
+  - hex esc: "\x0d\x0a is \r\n"
+  - single: '"Howdy!" he cried.'
+  - quoted: ' # Not a ''comment''.'
+  - tie-fighter: '|\-*-/|'
+  - plain:
+      This unquoted scalar
+      spans many lines.
+
+  - quoted: "So does this
+      quoted scalar.\n"
+  - accomplishment: >
+      Mark set a major league
+      home run record in 1998.
+  - stats: |
+      65 Home Runs
+      0.278 Batting Average
diff --git a/pkgs/yaml/benchmark/output.json b/pkgs/yaml/benchmark/output.json
new file mode 100644
index 0000000..9e6cb84
--- /dev/null
+++ b/pkgs/yaml/benchmark/output.json
@@ -0,0 +1 @@
+{"verb":"RecommendCafes","recipe":[{"verb":"List","outputs":["Cafe[]"]},{"verb":"Fetch","inputs":["Cafe[]"],"outputs":["CafeWithMenu[]"]},{"verb":"Flatten","inputs":["CafeWithMenu[]"],"outputs":["DishOffering[]"]},{"verb":"Score","inputs":["DishOffering[]"],"outputs":["DishOffering[]/Scored"]},{"verb":"Display","inputs":["DishOffering[]/Scored"]}],"tags":{"booleans":[true,false],"dates":[{"canonical":"2001-12-15T02:59:43.1Z"},{"iso8601":"2001-12-14t21:59:43.10-05:00"},{"spaced":"2001-12-14 21:59:43.10 -5"},{"date":"2002-12-14"}],"numbers":[{"int":12345},{"negative":-345},{"floating-point":345.678},{"hexidecimal":1194684},{"exponential":1230.15},{"octal":12}],"strings":[{"unicode":"Sosa did fine.☺"},{"control":"\b1998\t1999\t2000\n"},{"hex esc":"\r\n is \r\n"},{"single":"\"Howdy!\" he cried."},{"quoted":" # Not a 'comment'."},{"tie-fighter":"|\\-*-/|"},{"plain":"This unquoted scalar spans many lines."},{"quoted":"So does this quoted scalar.\n"},{"accomplishment":"Mark set a major league home run record in 1998.\n"},{"stats":"65 Home Runs\n0.278 Batting Average\n"}]}}
\ No newline at end of file
diff --git a/pkgs/yaml/example/example.dart b/pkgs/yaml/example/example.dart
new file mode 100644
index 0000000..bb283a3
--- /dev/null
+++ b/pkgs/yaml/example/example.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2020, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:yaml/yaml.dart';
+
+void main() {
+  var doc = loadYaml("YAML: YAML Ain't Markup Language") as Map;
+  print(doc['YAML']);
+}
diff --git a/pkgs/yaml/lib/src/charcodes.dart b/pkgs/yaml/lib/src/charcodes.dart
new file mode 100644
index 0000000..602d597
--- /dev/null
+++ b/pkgs/yaml/lib/src/charcodes.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2021, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+/// Character `+`.
+const int $plus = 0x2b;
+
+/// Character `-`.
+const int $minus = 0x2d;
+
+/// Character `.`.
+const int $dot = 0x2e;
+
+/// Character `0`.
+const int $0 = 0x30;
+
+/// Character `9`.
+const int $9 = 0x39;
+
+/// Character `F`.
+const int $F = 0x46;
+
+/// Character `N`.
+const int $N = 0x4e;
+
+/// Character `T`.
+const int $T = 0x54;
+
+/// Character `f`.
+const int $f = 0x66;
+
+/// Character `n`.
+const int $n = 0x6e;
+
+/// Character `o`.
+const int $o = 0x6f;
+
+/// Character `t`.
+const int $t = 0x74;
+
+/// Character `x`.
+const int $x = 0x78;
+
+/// Character `~`.
+const int $tilde = 0x7e;
diff --git a/pkgs/yaml/lib/src/equality.dart b/pkgs/yaml/lib/src/equality.dart
new file mode 100644
index 0000000..c833dc6
--- /dev/null
+++ b/pkgs/yaml/lib/src/equality.dart
@@ -0,0 +1,128 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart';
+
+import 'yaml_node.dart';
+
+/// Returns a [Map] that compares its keys based on [deepEquals].
+Map<K, V> deepEqualsMap<K, V>() =>
+    LinkedHashMap(equals: deepEquals, hashCode: deepHashCode);
+
+/// Returns whether two objects are structurally equivalent.
+///
+/// This considers `NaN` values to be equivalent, handles self-referential
+/// structures, and considers [YamlScalar]s to be equal to their values.
+bool deepEquals(Object? obj1, Object? obj2) => _DeepEquals().equals(obj1, obj2);
+
+/// A class that provides access to the list of parent objects used for loop
+/// detection.
+class _DeepEquals {
+  final _parents1 = <Object?>[];
+  final _parents2 = <Object?>[];
+
+  /// Returns whether [obj1] and [obj2] are structurally equivalent.
+  bool equals(Object? obj1, Object? obj2) {
+    if (obj1 is YamlScalar) obj1 = obj1.value;
+    if (obj2 is YamlScalar) obj2 = obj2.value;
+
+    // _parents1 and _parents2 are guaranteed to be the same size.
+    for (var i = 0; i < _parents1.length; i++) {
+      var loop1 = identical(obj1, _parents1[i]);
+      var loop2 = identical(obj2, _parents2[i]);
+      // If both structures loop in the same place, they're equal at that point
+      // in the structure. If one loops and the other doesn't, they're not
+      // equal.
+      if (loop1 && loop2) return true;
+      if (loop1 || loop2) return false;
+    }
+
+    _parents1.add(obj1);
+    _parents2.add(obj2);
+    try {
+      if (obj1 is List && obj2 is List) {
+        return _listEquals(obj1, obj2);
+      } else if (obj1 is Map && obj2 is Map) {
+        return _mapEquals(obj1, obj2);
+      } else if (obj1 is num && obj2 is num) {
+        return _numEquals(obj1, obj2);
+      } else {
+        return obj1 == obj2;
+      }
+    } finally {
+      _parents1.removeLast();
+      _parents2.removeLast();
+    }
+  }
+
+  /// Returns whether [list1] and [list2] are structurally equal.
+  bool _listEquals(List list1, List list2) {
+    if (list1.length != list2.length) return false;
+
+    for (var i = 0; i < list1.length; i++) {
+      if (!equals(list1[i], list2[i])) return false;
+    }
+
+    return true;
+  }
+
+  /// Returns whether [map1] and [map2] are structurally equal.
+  bool _mapEquals(Map map1, Map map2) {
+    if (map1.length != map2.length) return false;
+
+    for (var key in map1.keys) {
+      if (!map2.containsKey(key)) return false;
+      if (!equals(map1[key], map2[key])) return false;
+    }
+
+    return true;
+  }
+
+  /// Returns whether two numbers are equivalent.
+  ///
+  /// This differs from `n1 == n2` in that it considers `NaN` to be equal to
+  /// itself.
+  bool _numEquals(num n1, num n2) {
+    if (n1.isNaN && n2.isNaN) return true;
+    return n1 == n2;
+  }
+}
+
+/// Returns a hash code for [obj] such that structurally equivalent objects
+/// will have the same hash code.
+///
+/// This supports deep equality for maps and lists, including those with
+/// self-referential structures, and returns the same hash code for
+/// [YamlScalar]s and their values.
+int deepHashCode(Object? obj) {
+  var parents = <Object?>[];
+
+  int deepHashCodeInner(Object? value) {
+    if (parents.any((parent) => identical(parent, value))) return -1;
+
+    parents.add(value);
+    try {
+      if (value is Map) {
+        var equality = const UnorderedIterableEquality<Object?>();
+        return equality.hash(value.keys.map(deepHashCodeInner)) ^
+            equality.hash(value.values.map(deepHashCodeInner));
+      } else if (value is Iterable) {
+        return const IterableEquality<Object?>().hash(value.map(deepHashCode));
+      } else if (value is YamlScalar) {
+        return (value.value as Object?).hashCode;
+      } else {
+        return value.hashCode;
+      }
+    } finally {
+      parents.removeLast();
+    }
+  }
+
+  return deepHashCodeInner(obj);
+}
diff --git a/pkgs/yaml/lib/src/error_listener.dart b/pkgs/yaml/lib/src/error_listener.dart
new file mode 100644
index 0000000..0498d68
--- /dev/null
+++ b/pkgs/yaml/lib/src/error_listener.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2021, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'yaml_exception.dart';
+
+/// A listener that is notified of [YamlException]s during scanning/parsing.
+abstract class ErrorListener {
+  /// This method is invoked when an [error] has been found in the YAML.
+  void onError(YamlException error);
+}
+
+/// An [ErrorListener] that collects all errors into [errors].
+class ErrorCollector extends ErrorListener {
+  final List<YamlException> errors = [];
+
+  @override
+  void onError(YamlException error) => errors.add(error);
+}
diff --git a/pkgs/yaml/lib/src/event.dart b/pkgs/yaml/lib/src/event.dart
new file mode 100644
index 0000000..1476311
--- /dev/null
+++ b/pkgs/yaml/lib/src/event.dart
@@ -0,0 +1,171 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+import 'parser.dart';
+import 'style.dart';
+import 'yaml_document.dart';
+
+/// An event emitted by a [Parser].
+class Event {
+  final EventType type;
+  final FileSpan span;
+
+  Event(this.type, this.span);
+
+  @override
+  String toString() => type.toString();
+}
+
+/// An event indicating the beginning of a YAML document.
+class DocumentStartEvent implements Event {
+  @override
+  EventType get type => EventType.documentStart;
+  @override
+  final FileSpan span;
+
+  /// The document's `%YAML` directive, or `null` if there was none.
+  final VersionDirective? versionDirective;
+
+  /// The document's `%TAG` directives, if any.
+  final List<TagDirective> tagDirectives;
+
+  /// Whether the document started implicitly (that is, without an explicit
+  /// `===` sequence).
+  final bool isImplicit;
+
+  DocumentStartEvent(this.span,
+      {this.versionDirective,
+      List<TagDirective>? tagDirectives,
+      this.isImplicit = true})
+      : tagDirectives = tagDirectives ?? [];
+
+  @override
+  String toString() => 'DOCUMENT_START';
+}
+
+/// An event indicating the end of a YAML document.
+class DocumentEndEvent implements Event {
+  @override
+  EventType get type => EventType.documentEnd;
+  @override
+  final FileSpan span;
+
+  /// Whether the document ended implicitly (that is, without an explicit
+  /// `...` sequence).
+  final bool isImplicit;
+
+  DocumentEndEvent(this.span, {this.isImplicit = true});
+
+  @override
+  String toString() => 'DOCUMENT_END';
+}
+
+/// An event indicating that an alias was referenced.
+class AliasEvent implements Event {
+  @override
+  EventType get type => EventType.alias;
+  @override
+  final FileSpan span;
+
+  /// The alias name.
+  final String name;
+
+  AliasEvent(this.span, this.name);
+
+  @override
+  String toString() => 'ALIAS $name';
+}
+
+/// An event that can have associated anchor and tag properties.
+abstract class _ValueEvent implements Event {
+  /// The name of the value's anchor, or `null` if it wasn't anchored.
+  String? get anchor;
+
+  /// The text of the value's tag, or `null` if it wasn't tagged.
+  String? get tag;
+
+  @override
+  String toString() {
+    var buffer = StringBuffer('$type');
+    if (anchor != null) buffer.write(' &$anchor');
+    if (tag != null) buffer.write(' $tag');
+    return buffer.toString();
+  }
+}
+
+/// An event indicating a single scalar value.
+class ScalarEvent extends _ValueEvent {
+  @override
+  EventType get type => EventType.scalar;
+  @override
+  final FileSpan span;
+  @override
+  final String? anchor;
+  @override
+  final String? tag;
+
+  /// The contents of the scalar.
+  final String value;
+
+  /// The style of the scalar in the original source.
+  final ScalarStyle style;
+
+  ScalarEvent(this.span, this.value, this.style, {this.anchor, this.tag});
+
+  @override
+  String toString() => '${super.toString()} "$value"';
+}
+
+/// An event indicating the beginning of a sequence.
+class SequenceStartEvent extends _ValueEvent {
+  @override
+  EventType get type => EventType.sequenceStart;
+  @override
+  final FileSpan span;
+  @override
+  final String? anchor;
+  @override
+  final String? tag;
+
+  /// The style of the collection in the original source.
+  final CollectionStyle style;
+
+  SequenceStartEvent(this.span, this.style, {this.anchor, this.tag});
+}
+
+/// An event indicating the beginning of a mapping.
+class MappingStartEvent extends _ValueEvent {
+  @override
+  EventType get type => EventType.mappingStart;
+  @override
+  final FileSpan span;
+  @override
+  final String? anchor;
+  @override
+  final String? tag;
+
+  /// The style of the collection in the original source.
+  final CollectionStyle style;
+
+  MappingStartEvent(this.span, this.style, {this.anchor, this.tag});
+}
+
+/// The types of [Event] objects.
+enum EventType {
+  streamStart,
+  streamEnd,
+  documentStart,
+  documentEnd,
+  alias,
+  scalar,
+  sequenceStart,
+  sequenceEnd,
+  mappingStart,
+  mappingEnd
+}
diff --git a/pkgs/yaml/lib/src/loader.dart b/pkgs/yaml/lib/src/loader.dart
new file mode 100644
index 0000000..7cdf45a
--- /dev/null
+++ b/pkgs/yaml/lib/src/loader.dart
@@ -0,0 +1,343 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+import 'charcodes.dart';
+import 'equality.dart';
+import 'error_listener.dart';
+import 'event.dart';
+import 'parser.dart';
+import 'yaml_document.dart';
+import 'yaml_exception.dart';
+import 'yaml_node.dart';
+
+/// A loader that reads [Event]s emitted by a [Parser] and emits
+/// [YamlDocument]s.
+///
+/// This is based on the libyaml loader, available at
+/// https://github.com/yaml/libyaml/blob/master/src/loader.c. The license for
+/// that is available in ../../libyaml-license.txt.
+class Loader {
+  /// The underlying [Parser] that generates [Event]s.
+  final Parser _parser;
+
+  /// Aliases by the alias name.
+  final _aliases = <String, YamlNode>{};
+
+  /// The span of the entire stream emitted so far.
+  FileSpan get span => _span;
+  FileSpan _span;
+
+  /// Creates a loader that loads [source].
+  factory Loader(String source,
+      {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) {
+    var parser = Parser(source,
+        sourceUrl: sourceUrl, recover: recover, errorListener: errorListener);
+    var event = parser.parse();
+    assert(event.type == EventType.streamStart);
+    return Loader._(parser, event.span);
+  }
+
+  Loader._(this._parser, this._span);
+
+  /// Loads the next document from the stream.
+  ///
+  /// If there are no more documents, returns `null`.
+  YamlDocument? load() {
+    if (_parser.isDone) return null;
+
+    var event = _parser.parse();
+    if (event.type == EventType.streamEnd) {
+      _span = _span.expand(event.span);
+      return null;
+    }
+
+    var document = _loadDocument(event as DocumentStartEvent);
+    _span = _span.expand(document.span as FileSpan);
+    _aliases.clear();
+    return document;
+  }
+
+  /// Composes a document object.
+  YamlDocument _loadDocument(DocumentStartEvent firstEvent) {
+    var contents = _loadNode(_parser.parse());
+
+    var lastEvent = _parser.parse() as DocumentEndEvent;
+    assert(lastEvent.type == EventType.documentEnd);
+
+    return YamlDocument.internal(
+        contents,
+        firstEvent.span.expand(lastEvent.span),
+        firstEvent.versionDirective,
+        firstEvent.tagDirectives,
+        startImplicit: firstEvent.isImplicit,
+        endImplicit: lastEvent.isImplicit);
+  }
+
+  /// Composes a node.
+  YamlNode _loadNode(Event firstEvent) => switch (firstEvent.type) {
+        EventType.alias => _loadAlias(firstEvent as AliasEvent),
+        EventType.scalar => _loadScalar(firstEvent as ScalarEvent),
+        EventType.sequenceStart =>
+          _loadSequence(firstEvent as SequenceStartEvent),
+        EventType.mappingStart => _loadMapping(firstEvent as MappingStartEvent),
+        _ => throw StateError('Unreachable')
+      };
+
+  /// Registers an anchor.
+  void _registerAnchor(String? anchor, YamlNode node) {
+    if (anchor == null) return;
+
+    // libyaml throws an error for duplicate anchors, but example 7.1 makes it
+    // clear that they should be overridden:
+    // http://yaml.org/spec/1.2/spec.html#id2786448.
+
+    _aliases[anchor] = node;
+  }
+
+  /// Composes a node corresponding to an alias.
+  YamlNode _loadAlias(AliasEvent event) {
+    var alias = _aliases[event.name];
+    if (alias != null) return alias;
+
+    throw YamlException('Undefined alias.', event.span);
+  }
+
+  /// Composes a scalar node.
+  YamlNode _loadScalar(ScalarEvent scalar) {
+    YamlNode node;
+    if (scalar.tag == '!') {
+      node = YamlScalar.internal(scalar.value, scalar);
+    } else if (scalar.tag != null) {
+      node = _parseByTag(scalar);
+    } else {
+      node = _parseScalar(scalar);
+    }
+
+    _registerAnchor(scalar.anchor, node);
+    return node;
+  }
+
+  /// Composes a sequence node.
+  YamlNode _loadSequence(SequenceStartEvent firstEvent) {
+    if (firstEvent.tag != '!' &&
+        firstEvent.tag != null &&
+        firstEvent.tag != 'tag:yaml.org,2002:seq') {
+      throw YamlException('Invalid tag for sequence.', firstEvent.span);
+    }
+
+    var children = <YamlNode>[];
+    var node = YamlList.internal(children, firstEvent.span, firstEvent.style);
+    _registerAnchor(firstEvent.anchor, node);
+
+    var event = _parser.parse();
+    while (event.type != EventType.sequenceEnd) {
+      children.add(_loadNode(event));
+      event = _parser.parse();
+    }
+
+    setSpan(node, firstEvent.span.expand(event.span));
+    return node;
+  }
+
+  /// Composes a mapping node.
+  YamlNode _loadMapping(MappingStartEvent firstEvent) {
+    if (firstEvent.tag != '!' &&
+        firstEvent.tag != null &&
+        firstEvent.tag != 'tag:yaml.org,2002:map') {
+      throw YamlException('Invalid tag for mapping.', firstEvent.span);
+    }
+
+    var children = deepEqualsMap<dynamic, YamlNode>();
+    var node = YamlMap.internal(children, firstEvent.span, firstEvent.style);
+    _registerAnchor(firstEvent.anchor, node);
+
+    var event = _parser.parse();
+    while (event.type != EventType.mappingEnd) {
+      var key = _loadNode(event);
+      var value = _loadNode(_parser.parse());
+      if (children.containsKey(key)) {
+        throw YamlException('Duplicate mapping key.', key.span);
+      }
+
+      children[key] = value;
+      event = _parser.parse();
+    }
+
+    setSpan(node, firstEvent.span.expand(event.span));
+    return node;
+  }
+
+  /// Parses a scalar according to its tag name.
+  YamlScalar _parseByTag(ScalarEvent scalar) {
+    switch (scalar.tag) {
+      case 'tag:yaml.org,2002:null':
+        var result = _parseNull(scalar);
+        if (result != null) return result;
+        throw YamlException('Invalid null scalar.', scalar.span);
+      case 'tag:yaml.org,2002:bool':
+        var result = _parseBool(scalar);
+        if (result != null) return result;
+        throw YamlException('Invalid bool scalar.', scalar.span);
+      case 'tag:yaml.org,2002:int':
+        var result = _parseNumber(scalar, allowFloat: false);
+        if (result != null) return result;
+        throw YamlException('Invalid int scalar.', scalar.span);
+      case 'tag:yaml.org,2002:float':
+        var result = _parseNumber(scalar, allowInt: false);
+        if (result != null) return result;
+        throw YamlException('Invalid float scalar.', scalar.span);
+      case 'tag:yaml.org,2002:str':
+        return YamlScalar.internal(scalar.value, scalar);
+      default:
+        throw YamlException('Undefined tag: ${scalar.tag}.', scalar.span);
+    }
+  }
+
+  /// Parses [scalar], which may be one of several types.
+  YamlScalar _parseScalar(ScalarEvent scalar) =>
+      _tryParseScalar(scalar) ?? YamlScalar.internal(scalar.value, scalar);
+
+  /// Tries to parse [scalar].
+  ///
+  /// If parsing fails, this returns `null`, indicating that the scalar should
+  /// be parsed as a string.
+  YamlScalar? _tryParseScalar(ScalarEvent scalar) {
+    // Quickly check for the empty string, which means null.
+    var length = scalar.value.length;
+    if (length == 0) return YamlScalar.internal(null, scalar);
+
+    // Dispatch on the first character.
+    var firstChar = scalar.value.codeUnitAt(0);
+    return switch (firstChar) {
+      $dot || $plus || $minus => _parseNumber(scalar),
+      $n || $N => length == 4 ? _parseNull(scalar) : null,
+      $t || $T => length == 4 ? _parseBool(scalar) : null,
+      $f || $F => length == 5 ? _parseBool(scalar) : null,
+      $tilde => length == 1 ? YamlScalar.internal(null, scalar) : null,
+      _ => (firstChar >= $0 && firstChar <= $9) ? _parseNumber(scalar) : null
+    };
+  }
+
+  /// Parse a null scalar.
+  ///
+  /// Returns a Dart `null` if parsing fails.
+  YamlScalar? _parseNull(ScalarEvent scalar) => switch (scalar.value) {
+        '' ||
+        'null' ||
+        'Null' ||
+        'NULL' ||
+        '~' =>
+          YamlScalar.internal(null, scalar),
+        _ => null
+      };
+
+  /// Parse a boolean scalar.
+  ///
+  /// Returns `null` if parsing fails.
+  YamlScalar? _parseBool(ScalarEvent scalar) => switch (scalar.value) {
+        'true' || 'True' || 'TRUE' => YamlScalar.internal(true, scalar),
+        'false' || 'False' || 'FALSE' => YamlScalar.internal(false, scalar),
+        _ => null
+      };
+
+  /// Parses a numeric scalar.
+  ///
+  /// Returns `null` if parsing fails.
+  YamlScalar? _parseNumber(ScalarEvent scalar,
+      {bool allowInt = true, bool allowFloat = true}) {
+    var value = _parseNumberValue(scalar.value,
+        allowInt: allowInt, allowFloat: allowFloat);
+    return value == null ? null : YamlScalar.internal(value, scalar);
+  }
+
+  /// Parses the value of a number.
+  ///
+  /// Returns the number if it's parsed successfully, or `null` if it's not.
+  num? _parseNumberValue(String contents,
+      {bool allowInt = true, bool allowFloat = true}) {
+    assert(allowInt || allowFloat);
+
+    var firstChar = contents.codeUnitAt(0);
+    var length = contents.length;
+
+    // Quick check for single digit integers.
+    if (allowInt && length == 1) {
+      var value = firstChar - $0;
+      return value >= 0 && value <= 9 ? value : null;
+    }
+
+    var secondChar = contents.codeUnitAt(1);
+
+    // Hexadecimal or octal integers.
+    if (allowInt && firstChar == $0) {
+      // int.tryParse supports 0x natively.
+      if (secondChar == $x) return int.tryParse(contents);
+
+      if (secondChar == $o) {
+        var afterRadix = contents.substring(2);
+        return int.tryParse(afterRadix, radix: 8);
+      }
+    }
+
+    // Int or float starting with a digit or a +/- sign.
+    if ((firstChar >= $0 && firstChar <= $9) ||
+        ((firstChar == $plus || firstChar == $minus) &&
+            secondChar >= $0 &&
+            secondChar <= $9)) {
+      // Try to parse an int or, failing that, a double.
+      num? result;
+      if (allowInt) {
+        // Pass "radix: 10" explicitly to ensure that "-0x10", which is valid
+        // Dart but invalid YAML, doesn't get parsed.
+        result = int.tryParse(contents, radix: 10);
+      }
+
+      if (allowFloat) result ??= double.tryParse(contents);
+      return result;
+    }
+
+    if (!allowFloat) return null;
+
+    // Now the only possibility is to parse a float starting with a dot or a
+    // sign and a dot, or the signed/unsigned infinity values and not-a-numbers.
+    if ((firstChar == $dot && secondChar >= $0 && secondChar <= $9) ||
+        (firstChar == $minus || firstChar == $plus) && secondChar == $dot) {
+      // Starting with a . and a number or a sign followed by a dot.
+      if (length == 5) {
+        switch (contents) {
+          case '+.inf':
+          case '+.Inf':
+          case '+.INF':
+            return double.infinity;
+          case '-.inf':
+          case '-.Inf':
+          case '-.INF':
+            return -double.infinity;
+        }
+      }
+
+      return double.tryParse(contents);
+    }
+
+    if (length == 4 && firstChar == $dot) {
+      switch (contents) {
+        case '.inf':
+        case '.Inf':
+        case '.INF':
+          return double.infinity;
+        case '.nan':
+        case '.NaN':
+        case '.NAN':
+          return double.nan;
+      }
+    }
+
+    return null;
+  }
+}
diff --git a/pkgs/yaml/lib/src/null_span.dart b/pkgs/yaml/lib/src/null_span.dart
new file mode 100644
index 0000000..49e1a1c
--- /dev/null
+++ b/pkgs/yaml/lib/src/null_span.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+import 'yaml_node.dart';
+
+/// A [SourceSpan] with no location information.
+///
+/// This is used with [YamlMap.wrap] and [YamlList.wrap] to provide means of
+/// accessing a non-YAML map that behaves transparently like a map parsed from
+/// YAML.
+class NullSpan extends SourceSpanMixin {
+  @override
+  final SourceLocation start;
+  @override
+  SourceLocation get end => start;
+  @override
+  final text = '';
+
+  NullSpan(Object? sourceUrl) : start = SourceLocation(0, sourceUrl: sourceUrl);
+}
diff --git a/pkgs/yaml/lib/src/parser.dart b/pkgs/yaml/lib/src/parser.dart
new file mode 100644
index 0000000..e924e40
--- /dev/null
+++ b/pkgs/yaml/lib/src/parser.dart
@@ -0,0 +1,805 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: constant_identifier_names
+
+import 'package:source_span/source_span.dart';
+import 'package:string_scanner/string_scanner.dart';
+
+import 'error_listener.dart';
+import 'event.dart';
+import 'scanner.dart';
+import 'style.dart';
+import 'token.dart';
+import 'utils.dart';
+import 'yaml_document.dart';
+import 'yaml_exception.dart';
+
+/// A parser that reads [Token]s emitted by a [Scanner] and emits [Event]s.
+///
+/// This is based on the libyaml parser, available at
+/// https://github.com/yaml/libyaml/blob/master/src/parser.c. The license for
+/// that is available in ../../libyaml-license.txt.
+class Parser {
+  /// The underlying [Scanner] that generates [Token]s.
+  final Scanner _scanner;
+
+  /// The stack of parse states for nested contexts.
+  final _states = <_State>[];
+
+  /// The current parse state.
+  var _state = _State.STREAM_START;
+
+  /// The custom tag directives, by tag handle.
+  final _tagDirectives = <String, TagDirective>{};
+
+  /// Whether the parser has finished parsing.
+  bool get isDone => _state == _State.END;
+
+  /// Creates a parser that parses [source].
+  ///
+  /// If [recover] is true, will attempt to recover from parse errors and may
+  /// return invalid or synthetic nodes. If [errorListener] is also supplied,
+  /// its onError method will be called for each error recovered from. It is not
+  /// valid to provide [errorListener] if [recover] is false.
+  Parser(String source,
+      {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener})
+      : assert(recover || errorListener == null),
+        _scanner = Scanner(source,
+            sourceUrl: sourceUrl,
+            recover: recover,
+            errorListener: errorListener);
+
+  /// Consumes and returns the next event.
+  Event parse() {
+    try {
+      if (isDone) throw StateError('No more events.');
+      var event = _stateMachine();
+      return event;
+    } on StringScannerException catch (error) {
+      throw YamlException(error.message, error.span);
+    }
+  }
+
+  /// Dispatches parsing based on the current state.
+  Event _stateMachine() {
+    switch (_state) {
+      case _State.STREAM_START:
+        return _parseStreamStart();
+      case _State.DOCUMENT_START:
+        return _parseDocumentStart();
+      case _State.DOCUMENT_CONTENT:
+        return _parseDocumentContent();
+      case _State.DOCUMENT_END:
+        return _parseDocumentEnd();
+      case _State.BLOCK_NODE:
+        return _parseNode(block: true);
+      case _State.BLOCK_NODE_OR_INDENTLESS_SEQUENCE:
+        return _parseNode(block: true, indentlessSequence: true);
+      case _State.FLOW_NODE:
+        return _parseNode();
+      case _State.BLOCK_SEQUENCE_FIRST_ENTRY:
+        // Scan past the `BLOCK-SEQUENCE-FIRST-ENTRY` token to the
+        // `BLOCK-SEQUENCE-ENTRY` token.
+        _scanner.scan();
+        return _parseBlockSequenceEntry();
+      case _State.BLOCK_SEQUENCE_ENTRY:
+        return _parseBlockSequenceEntry();
+      case _State.INDENTLESS_SEQUENCE_ENTRY:
+        return _parseIndentlessSequenceEntry();
+      case _State.BLOCK_MAPPING_FIRST_KEY:
+        // Scan past the `BLOCK-MAPPING-FIRST-KEY` token to the
+        // `BLOCK-MAPPING-KEY` token.
+        _scanner.scan();
+        return _parseBlockMappingKey();
+      case _State.BLOCK_MAPPING_KEY:
+        return _parseBlockMappingKey();
+      case _State.BLOCK_MAPPING_VALUE:
+        return _parseBlockMappingValue();
+      case _State.FLOW_SEQUENCE_FIRST_ENTRY:
+        return _parseFlowSequenceEntry(first: true);
+      case _State.FLOW_SEQUENCE_ENTRY:
+        return _parseFlowSequenceEntry();
+      case _State.FLOW_SEQUENCE_ENTRY_MAPPING_KEY:
+        return _parseFlowSequenceEntryMappingKey();
+      case _State.FLOW_SEQUENCE_ENTRY_MAPPING_VALUE:
+        return _parseFlowSequenceEntryMappingValue();
+      case _State.FLOW_SEQUENCE_ENTRY_MAPPING_END:
+        return _parseFlowSequenceEntryMappingEnd();
+      case _State.FLOW_MAPPING_FIRST_KEY:
+        return _parseFlowMappingKey(first: true);
+      case _State.FLOW_MAPPING_KEY:
+        return _parseFlowMappingKey();
+      case _State.FLOW_MAPPING_VALUE:
+        return _parseFlowMappingValue();
+      case _State.FLOW_MAPPING_EMPTY_VALUE:
+        return _parseFlowMappingValue(empty: true);
+      default:
+        throw StateError('Unreachable');
+    }
+  }
+
+  /// Parses the production:
+  ///
+  ///     stream ::=
+  ///       STREAM-START implicit_document? explicit_document* STREAM-END
+  ///       ************
+  Event _parseStreamStart() {
+    var token = _scanner.scan();
+    assert(token.type == TokenType.streamStart);
+
+    _state = _State.DOCUMENT_START;
+    return Event(EventType.streamStart, token.span);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     implicit_document    ::= block_node DOCUMENT-END*
+  ///                              *
+  ///     explicit_document    ::=
+  ///       DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+  ///       *************************
+  Event _parseDocumentStart() {
+    var token = _scanner.peek()!;
+
+    // libyaml requires any document beyond the first in the stream to have an
+    // explicit document start indicator, but the spec allows it to be omitted
+    // as long as there was an end indicator.
+
+    // Parse extra document end indicators.
+    while (token.type == TokenType.documentEnd) {
+      token = _scanner.advance()!;
+    }
+
+    if (token.type != TokenType.versionDirective &&
+        token.type != TokenType.tagDirective &&
+        token.type != TokenType.documentStart &&
+        token.type != TokenType.streamEnd) {
+      // Parse an implicit document.
+      _processDirectives();
+      _states.add(_State.DOCUMENT_END);
+      _state = _State.BLOCK_NODE;
+      return DocumentStartEvent(token.span.start.pointSpan());
+    }
+
+    if (token.type == TokenType.streamEnd) {
+      _state = _State.END;
+      _scanner.scan();
+      return Event(EventType.streamEnd, token.span);
+    }
+
+    // Parse an explicit document.
+    var start = token.span;
+    var (versionDirective, tagDirectives) = _processDirectives();
+    token = _scanner.peek()!;
+    if (token.type != TokenType.documentStart) {
+      throw YamlException('Expected document start.', token.span);
+    }
+
+    _states.add(_State.DOCUMENT_END);
+    _state = _State.DOCUMENT_CONTENT;
+    _scanner.scan();
+    return DocumentStartEvent(start.expand(token.span),
+        versionDirective: versionDirective,
+        tagDirectives: tagDirectives,
+        isImplicit: false);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     explicit_document    ::=
+  ///       DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+  ///                                 ***********
+  Event _parseDocumentContent() {
+    var token = _scanner.peek()!;
+
+    switch (token.type) {
+      case TokenType.versionDirective:
+      case TokenType.tagDirective:
+      case TokenType.documentStart:
+      case TokenType.documentEnd:
+      case TokenType.streamEnd:
+        _state = _states.removeLast();
+        return _processEmptyScalar(token.span.start);
+      default:
+        return _parseNode(block: true);
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     implicit_document    ::= block_node DOCUMENT-END*
+  ///                                         *************
+  ///     explicit_document    ::=
+  ///       DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+  ///                                             *************
+  Event _parseDocumentEnd() {
+    _tagDirectives.clear();
+    _state = _State.DOCUMENT_START;
+
+    var token = _scanner.peek()!;
+    if (token.type == TokenType.documentEnd) {
+      _scanner.scan();
+      return DocumentEndEvent(token.span, isImplicit: false);
+    } else {
+      return DocumentEndEvent(token.span.start.pointSpan());
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     block_node_or_indentless_sequence    ::=
+  ///       ALIAS
+  ///       *****
+  ///       | properties (block_content | indentless_block_sequence)?
+  ///         **********  *
+  ///       | block_content | indentless_block_sequence
+  ///         *
+  ///     block_node           ::= ALIAS
+  ///                              *****
+  ///                              | properties block_content?
+  ///                                ********** *
+  ///                              | block_content
+  ///                                *
+  ///     flow_node            ::= ALIAS
+  ///                              *****
+  ///                              | properties flow_content?
+  ///                                ********** *
+  ///                              | flow_content
+  ///                                *
+  ///     properties           ::= TAG ANCHOR? | ANCHOR TAG?
+  ///                              *************************
+  ///     block_content        ::= block_collection | flow_collection | SCALAR
+  ///                                                                   ******
+  ///     flow_content         ::= flow_collection | SCALAR
+  ///                                                ******
+  Event _parseNode({bool block = false, bool indentlessSequence = false}) {
+    var token = _scanner.peek()!;
+
+    if (token is AliasToken) {
+      _scanner.scan();
+      _state = _states.removeLast();
+      return AliasEvent(token.span, token.name);
+    }
+
+    String? anchor;
+    TagToken? tagToken;
+    var span = token.span.start.pointSpan();
+    Token parseAnchor(AnchorToken token) {
+      anchor = token.name;
+      span = span.expand(token.span);
+      return _scanner.advance()!;
+    }
+
+    Token parseTag(TagToken token) {
+      tagToken = token;
+      span = span.expand(token.span);
+      return _scanner.advance()!;
+    }
+
+    if (token is AnchorToken) {
+      token = parseAnchor(token);
+      if (token is TagToken) token = parseTag(token);
+    } else if (token is TagToken) {
+      token = parseTag(token);
+      if (token is AnchorToken) token = parseAnchor(token);
+    }
+
+    String? tag;
+    if (tagToken != null) {
+      if (tagToken!.handle == null) {
+        tag = tagToken!.suffix;
+      } else {
+        var tagDirective = _tagDirectives[tagToken!.handle];
+        if (tagDirective == null) {
+          throw YamlException('Undefined tag handle.', tagToken!.span);
+        }
+
+        tag = tagDirective.prefix + (tagToken?.suffix ?? '');
+      }
+    }
+
+    if (indentlessSequence && token.type == TokenType.blockEntry) {
+      _state = _State.INDENTLESS_SEQUENCE_ENTRY;
+      return SequenceStartEvent(span.expand(token.span), CollectionStyle.BLOCK,
+          anchor: anchor, tag: tag);
+    }
+
+    if (token is ScalarToken) {
+      // All non-plain scalars have the "!" tag by default.
+      if (tag == null && token.style != ScalarStyle.PLAIN) tag = '!';
+
+      _state = _states.removeLast();
+      _scanner.scan();
+      return ScalarEvent(span.expand(token.span), token.value, token.style,
+          anchor: anchor, tag: tag);
+    }
+
+    if (token.type == TokenType.flowSequenceStart) {
+      _state = _State.FLOW_SEQUENCE_FIRST_ENTRY;
+      return SequenceStartEvent(span.expand(token.span), CollectionStyle.FLOW,
+          anchor: anchor, tag: tag);
+    }
+
+    if (token.type == TokenType.flowMappingStart) {
+      _state = _State.FLOW_MAPPING_FIRST_KEY;
+      return MappingStartEvent(span.expand(token.span), CollectionStyle.FLOW,
+          anchor: anchor, tag: tag);
+    }
+
+    if (block && token.type == TokenType.blockSequenceStart) {
+      _state = _State.BLOCK_SEQUENCE_FIRST_ENTRY;
+      return SequenceStartEvent(span.expand(token.span), CollectionStyle.BLOCK,
+          anchor: anchor, tag: tag);
+    }
+
+    if (block && token.type == TokenType.blockMappingStart) {
+      _state = _State.BLOCK_MAPPING_FIRST_KEY;
+      return MappingStartEvent(span.expand(token.span), CollectionStyle.BLOCK,
+          anchor: anchor, tag: tag);
+    }
+
+    if (anchor != null || tag != null) {
+      _state = _states.removeLast();
+      return ScalarEvent(span, '', ScalarStyle.PLAIN, anchor: anchor, tag: tag);
+    }
+
+    throw YamlException('Expected node content.', span);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     block_sequence ::=
+  ///       BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
+  ///       ********************  *********** *             *********
+  Event _parseBlockSequenceEntry() {
+    var token = _scanner.peek()!;
+
+    if (token.type == TokenType.blockEntry) {
+      var start = token.span.start;
+      token = _scanner.advance()!;
+
+      if (token.type == TokenType.blockEntry ||
+          token.type == TokenType.blockEnd) {
+        _state = _State.BLOCK_SEQUENCE_ENTRY;
+        return _processEmptyScalar(start);
+      } else {
+        _states.add(_State.BLOCK_SEQUENCE_ENTRY);
+        return _parseNode(block: true);
+      }
+    }
+
+    if (token.type == TokenType.blockEnd) {
+      _scanner.scan();
+      _state = _states.removeLast();
+      return Event(EventType.sequenceEnd, token.span);
+    }
+
+    throw YamlException("While parsing a block collection, expected '-'.",
+        token.span.start.pointSpan());
+  }
+
+  /// Parses the productions:
+  ///
+  ///     indentless_sequence  ::= (BLOCK-ENTRY block_node?)+
+  ///                               *********** *
+  Event _parseIndentlessSequenceEntry() {
+    var token = _scanner.peek()!;
+
+    if (token.type != TokenType.blockEntry) {
+      _state = _states.removeLast();
+      return Event(EventType.sequenceEnd, token.span.start.pointSpan());
+    }
+
+    var start = token.span.start;
+    token = _scanner.advance()!;
+
+    if (token.type == TokenType.blockEntry ||
+        token.type == TokenType.key ||
+        token.type == TokenType.value ||
+        token.type == TokenType.blockEnd) {
+      _state = _State.INDENTLESS_SEQUENCE_ENTRY;
+      return _processEmptyScalar(start);
+    } else {
+      _states.add(_State.INDENTLESS_SEQUENCE_ENTRY);
+      return _parseNode(block: true);
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     block_mapping        ::= BLOCK-MAPPING_START
+  ///                              *******************
+  ///                              ((KEY block_node_or_indentless_sequence?)?
+  ///                                *** *
+  ///                              (VALUE block_node_or_indentless_sequence?)?)*
+  ///
+  ///                              BLOCK-END
+  ///                              *********
+  Event _parseBlockMappingKey() {
+    var token = _scanner.peek()!;
+    if (token.type == TokenType.key) {
+      var start = token.span.start;
+      token = _scanner.advance()!;
+
+      if (token.type == TokenType.key ||
+          token.type == TokenType.value ||
+          token.type == TokenType.blockEnd) {
+        _state = _State.BLOCK_MAPPING_VALUE;
+        return _processEmptyScalar(start);
+      } else {
+        _states.add(_State.BLOCK_MAPPING_VALUE);
+        return _parseNode(block: true, indentlessSequence: true);
+      }
+    }
+
+    // libyaml doesn't allow empty keys without an explicit key indicator, but
+    // the spec does. See example 8.18:
+    // http://yaml.org/spec/1.2/spec.html#id2798896.
+    if (token.type == TokenType.value) {
+      _state = _State.BLOCK_MAPPING_VALUE;
+      return _processEmptyScalar(token.span.start);
+    }
+
+    if (token.type == TokenType.blockEnd) {
+      _scanner.scan();
+      _state = _states.removeLast();
+      return Event(EventType.mappingEnd, token.span);
+    }
+
+    throw YamlException('Expected a key while parsing a block mapping.',
+        token.span.start.pointSpan());
+  }
+
+  /// Parses the productions:
+  ///
+  ///     block_mapping        ::= BLOCK-MAPPING_START
+  ///
+  ///                              ((KEY block_node_or_indentless_sequence?)?
+  ///
+  ///                              (VALUE block_node_or_indentless_sequence?)?)*
+  ///                               ***** *
+  ///                              BLOCK-END
+  ///
+  Event _parseBlockMappingValue() {
+    var token = _scanner.peek()!;
+
+    if (token.type != TokenType.value) {
+      _state = _State.BLOCK_MAPPING_KEY;
+      return _processEmptyScalar(token.span.start);
+    }
+
+    var start = token.span.start;
+    token = _scanner.advance()!;
+    if (token.type == TokenType.key ||
+        token.type == TokenType.value ||
+        token.type == TokenType.blockEnd) {
+      _state = _State.BLOCK_MAPPING_KEY;
+      return _processEmptyScalar(start);
+    } else {
+      _states.add(_State.BLOCK_MAPPING_KEY);
+      return _parseNode(block: true, indentlessSequence: true);
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_sequence        ::= FLOW-SEQUENCE-START
+  ///                              *******************
+  ///                              (flow_sequence_entry FLOW-ENTRY)*
+  ///                               *                   **********
+  ///                              flow_sequence_entry?
+  ///                              *
+  ///                              FLOW-SEQUENCE-END
+  ///                              *****************
+  ///     flow_sequence_entry  ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///       *
+  Event _parseFlowSequenceEntry({bool first = false}) {
+    if (first) _scanner.scan();
+    var token = _scanner.peek()!;
+
+    if (token.type != TokenType.flowSequenceEnd) {
+      if (!first) {
+        if (token.type != TokenType.flowEntry) {
+          throw YamlException(
+              "While parsing a flow sequence, expected ',' or ']'.",
+              token.span.start.pointSpan());
+        }
+
+        token = _scanner.advance()!;
+      }
+
+      if (token.type == TokenType.key) {
+        _state = _State.FLOW_SEQUENCE_ENTRY_MAPPING_KEY;
+        _scanner.scan();
+        return MappingStartEvent(token.span, CollectionStyle.FLOW);
+      } else if (token.type != TokenType.flowSequenceEnd) {
+        _states.add(_State.FLOW_SEQUENCE_ENTRY);
+        return _parseNode();
+      }
+    }
+
+    _scanner.scan();
+    _state = _states.removeLast();
+    return Event(EventType.sequenceEnd, token.span);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_sequence_entry  ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///                   *** *
+  Event _parseFlowSequenceEntryMappingKey() {
+    var token = _scanner.peek()!;
+
+    if (token.type == TokenType.value ||
+        token.type == TokenType.flowEntry ||
+        token.type == TokenType.flowSequenceEnd) {
+      // libyaml consumes the token here, but that seems like a bug, since it
+      // always causes [_parseFlowSequenceEntryMappingValue] to emit an empty
+      // scalar.
+
+      var start = token.span.start;
+      _state = _State.FLOW_SEQUENCE_ENTRY_MAPPING_VALUE;
+      return _processEmptyScalar(start);
+    } else {
+      _states.add(_State.FLOW_SEQUENCE_ENTRY_MAPPING_VALUE);
+      return _parseNode();
+    }
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_sequence_entry  ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///                                   ***** *
+  Event _parseFlowSequenceEntryMappingValue() {
+    var token = _scanner.peek()!;
+
+    if (token.type == TokenType.value) {
+      token = _scanner.advance()!;
+      if (token.type != TokenType.flowEntry &&
+          token.type != TokenType.flowSequenceEnd) {
+        _states.add(_State.FLOW_SEQUENCE_ENTRY_MAPPING_END);
+        return _parseNode();
+      }
+    }
+
+    _state = _State.FLOW_SEQUENCE_ENTRY_MAPPING_END;
+    return _processEmptyScalar(token.span.start);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_sequence_entry  ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///                                                   *
+  Event _parseFlowSequenceEntryMappingEnd() {
+    _state = _State.FLOW_SEQUENCE_ENTRY;
+    return Event(EventType.mappingEnd, _scanner.peek()!.span.start.pointSpan());
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_mapping         ::= FLOW-MAPPING-START
+  ///                              ******************
+  ///                              (flow_mapping_entry FLOW-ENTRY)*
+  ///                               *                  **********
+  ///                              flow_mapping_entry?
+  ///                              ******************
+  ///                              FLOW-MAPPING-END
+  ///                              ****************
+  ///     flow_mapping_entry   ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///       *           *** *
+  Event _parseFlowMappingKey({bool first = false}) {
+    if (first) _scanner.scan();
+    var token = _scanner.peek()!;
+
+    if (token.type != TokenType.flowMappingEnd) {
+      if (!first) {
+        if (token.type != TokenType.flowEntry) {
+          throw YamlException(
+              "While parsing a flow mapping, expected ',' or '}'.",
+              token.span.start.pointSpan());
+        }
+
+        token = _scanner.advance()!;
+      }
+
+      if (token.type == TokenType.key) {
+        token = _scanner.advance()!;
+        if (token.type != TokenType.value &&
+            token.type != TokenType.flowEntry &&
+            token.type != TokenType.flowMappingEnd) {
+          _states.add(_State.FLOW_MAPPING_VALUE);
+          return _parseNode();
+        } else {
+          _state = _State.FLOW_MAPPING_VALUE;
+          return _processEmptyScalar(token.span.start);
+        }
+      } else if (token.type != TokenType.flowMappingEnd) {
+        _states.add(_State.FLOW_MAPPING_EMPTY_VALUE);
+        return _parseNode();
+      }
+    }
+
+    _scanner.scan();
+    _state = _states.removeLast();
+    return Event(EventType.mappingEnd, token.span);
+  }
+
+  /// Parses the productions:
+  ///
+  ///     flow_mapping_entry   ::=
+  ///       flow_node | KEY flow_node? (VALUE flow_node?)?
+  ///                *                  ***** *
+  Event _parseFlowMappingValue({bool empty = false}) {
+    var token = _scanner.peek()!;
+
+    if (empty) {
+      _state = _State.FLOW_MAPPING_KEY;
+      return _processEmptyScalar(token.span.start);
+    }
+
+    if (token.type == TokenType.value) {
+      token = _scanner.advance()!;
+      if (token.type != TokenType.flowEntry &&
+          token.type != TokenType.flowMappingEnd) {
+        _states.add(_State.FLOW_MAPPING_KEY);
+        return _parseNode();
+      }
+    }
+
+    _state = _State.FLOW_MAPPING_KEY;
+    return _processEmptyScalar(token.span.start);
+  }
+
+  /// Generate an empty scalar event.
+  Event _processEmptyScalar(SourceLocation location) =>
+      ScalarEvent(location.pointSpan() as FileSpan, '', ScalarStyle.PLAIN);
+
+  /// Parses directives.
+  (VersionDirective?, List<TagDirective>) _processDirectives() {
+    var token = _scanner.peek()!;
+
+    VersionDirective? versionDirective;
+    var tagDirectives = <TagDirective>[];
+    while (token.type == TokenType.versionDirective ||
+        token.type == TokenType.tagDirective) {
+      if (token is VersionDirectiveToken) {
+        if (versionDirective != null) {
+          throw YamlException('Duplicate %YAML directive.', token.span);
+        }
+
+        if (token.major != 1 || token.minor == 0) {
+          throw YamlException(
+              'Incompatible YAML document. This parser only supports YAML 1.1 '
+              'and 1.2.',
+              token.span);
+        } else if (token.minor > 2) {
+          // TODO(nweiz): Print to stderr when issue 6943 is fixed and dart:io
+          // is available.
+          warn('Warning: this parser only supports YAML 1.1 and 1.2.',
+              token.span);
+        }
+
+        versionDirective = VersionDirective(token.major, token.minor);
+      } else if (token is TagDirectiveToken) {
+        var tagDirective = TagDirective(token.handle, token.prefix);
+        _appendTagDirective(tagDirective, token.span);
+        tagDirectives.add(tagDirective);
+      }
+
+      token = _scanner.advance()!;
+    }
+
+    _appendTagDirective(TagDirective('!', '!'), token.span.start.pointSpan(),
+        allowDuplicates: true);
+    _appendTagDirective(
+        TagDirective('!!', 'tag:yaml.org,2002:'), token.span.start.pointSpan(),
+        allowDuplicates: true);
+
+    return (versionDirective, tagDirectives);
+  }
+
+  /// Adds a tag directive to the directives stack.
+  void _appendTagDirective(TagDirective newDirective, FileSpan span,
+      {bool allowDuplicates = false}) {
+    if (_tagDirectives.containsKey(newDirective.handle)) {
+      if (allowDuplicates) return;
+      throw YamlException('Duplicate %TAG directive.', span);
+    }
+
+    _tagDirectives[newDirective.handle] = newDirective;
+  }
+}
+
+/// The possible states for the parser.
+class _State {
+  /// Expect [TokenType.streamStart].
+  static const STREAM_START = _State('STREAM_START');
+
+  /// Expect [TokenType.documentStart].
+  static const DOCUMENT_START = _State('DOCUMENT_START');
+
+  /// Expect the content of a document.
+  static const DOCUMENT_CONTENT = _State('DOCUMENT_CONTENT');
+
+  /// Expect [TokenType.documentEnd].
+  static const DOCUMENT_END = _State('DOCUMENT_END');
+
+  /// Expect a block node.
+  static const BLOCK_NODE = _State('BLOCK_NODE');
+
+  /// Expect a block node or indentless sequence.
+  static const BLOCK_NODE_OR_INDENTLESS_SEQUENCE =
+      _State('BLOCK_NODE_OR_INDENTLESS_SEQUENCE');
+
+  /// Expect a flow node.
+  static const FLOW_NODE = _State('FLOW_NODE');
+
+  /// Expect the first entry of a block sequence.
+  static const BLOCK_SEQUENCE_FIRST_ENTRY =
+      _State('BLOCK_SEQUENCE_FIRST_ENTRY');
+
+  /// Expect an entry of a block sequence.
+  static const BLOCK_SEQUENCE_ENTRY = _State('BLOCK_SEQUENCE_ENTRY');
+
+  /// Expect an entry of an indentless sequence.
+  static const INDENTLESS_SEQUENCE_ENTRY = _State('INDENTLESS_SEQUENCE_ENTRY');
+
+  /// Expect the first key of a block mapping.
+  static const BLOCK_MAPPING_FIRST_KEY = _State('BLOCK_MAPPING_FIRST_KEY');
+
+  /// Expect a block mapping key.
+  static const BLOCK_MAPPING_KEY = _State('BLOCK_MAPPING_KEY');
+
+  /// Expect a block mapping value.
+  static const BLOCK_MAPPING_VALUE = _State('BLOCK_MAPPING_VALUE');
+
+  /// Expect the first entry of a flow sequence.
+  static const FLOW_SEQUENCE_FIRST_ENTRY = _State('FLOW_SEQUENCE_FIRST_ENTRY');
+
+  /// Expect an entry of a flow sequence.
+  static const FLOW_SEQUENCE_ENTRY = _State('FLOW_SEQUENCE_ENTRY');
+
+  /// Expect a key of an ordered mapping.
+  static const FLOW_SEQUENCE_ENTRY_MAPPING_KEY =
+      _State('FLOW_SEQUENCE_ENTRY_MAPPING_KEY');
+
+  /// Expect a value of an ordered mapping.
+  static const FLOW_SEQUENCE_ENTRY_MAPPING_VALUE =
+      _State('FLOW_SEQUENCE_ENTRY_MAPPING_VALUE');
+
+  /// Expect the and of an ordered mapping entry.
+  static const FLOW_SEQUENCE_ENTRY_MAPPING_END =
+      _State('FLOW_SEQUENCE_ENTRY_MAPPING_END');
+
+  /// Expect the first key of a flow mapping.
+  static const FLOW_MAPPING_FIRST_KEY = _State('FLOW_MAPPING_FIRST_KEY');
+
+  /// Expect a key of a flow mapping.
+  static const FLOW_MAPPING_KEY = _State('FLOW_MAPPING_KEY');
+
+  /// Expect a value of a flow mapping.
+  static const FLOW_MAPPING_VALUE = _State('FLOW_MAPPING_VALUE');
+
+  /// Expect an empty value of a flow mapping.
+  static const FLOW_MAPPING_EMPTY_VALUE = _State('FLOW_MAPPING_EMPTY_VALUE');
+
+  /// Expect nothing.
+  static const END = _State('END');
+
+  final String name;
+
+  const _State(this.name);
+
+  @override
+  String toString() => name;
+}
diff --git a/pkgs/yaml/lib/src/scanner.dart b/pkgs/yaml/lib/src/scanner.dart
new file mode 100644
index 0000000..1cfd3af
--- /dev/null
+++ b/pkgs/yaml/lib/src/scanner.dart
@@ -0,0 +1,1695 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: constant_identifier_names
+
+import 'package:collection/collection.dart';
+import 'package:source_span/source_span.dart';
+import 'package:string_scanner/string_scanner.dart';
+
+import 'error_listener.dart';
+import 'style.dart';
+import 'token.dart';
+import 'utils.dart';
+import 'yaml_exception.dart';
+
+/// A scanner that reads a string of Unicode characters and emits [Token]s.
+///
+/// This is based on the libyaml scanner, available at
+/// https://github.com/yaml/libyaml/blob/master/src/scanner.c. The license for
+/// that is available in ../../libyaml-license.txt.
+class Scanner {
+  static const TAB = 0x9;
+  static const LF = 0xA;
+  static const CR = 0xD;
+  static const SP = 0x20;
+  static const DOLLAR = 0x24;
+  static const LEFT_PAREN = 0x28;
+  static const RIGHT_PAREN = 0x29;
+  static const PLUS = 0x2B;
+  static const COMMA = 0x2C;
+  static const HYPHEN = 0x2D;
+  static const PERIOD = 0x2E;
+  static const QUESTION = 0x3F;
+  static const COLON = 0x3A;
+  static const SEMICOLON = 0x3B;
+  static const EQUALS = 0x3D;
+  static const LEFT_SQUARE = 0x5B;
+  static const RIGHT_SQUARE = 0x5D;
+  static const LEFT_CURLY = 0x7B;
+  static const RIGHT_CURLY = 0x7D;
+  static const HASH = 0x23;
+  static const AMPERSAND = 0x26;
+  static const ASTERISK = 0x2A;
+  static const EXCLAMATION = 0x21;
+  static const VERTICAL_BAR = 0x7C;
+  static const LEFT_ANGLE = 0x3C;
+  static const RIGHT_ANGLE = 0x3E;
+  static const SINGLE_QUOTE = 0x27;
+  static const DOUBLE_QUOTE = 0x22;
+  static const PERCENT = 0x25;
+  static const AT = 0x40;
+  static const GRAVE_ACCENT = 0x60;
+  static const TILDE = 0x7E;
+
+  static const NULL = 0x0;
+  static const BELL = 0x7;
+  static const BACKSPACE = 0x8;
+  static const VERTICAL_TAB = 0xB;
+  static const FORM_FEED = 0xC;
+  static const ESCAPE = 0x1B;
+  static const SLASH = 0x2F;
+  static const BACKSLASH = 0x5C;
+  static const UNDERSCORE = 0x5F;
+  static const NEL = 0x85;
+  static const NBSP = 0xA0;
+  static const LINE_SEPARATOR = 0x2028;
+  static const PARAGRAPH_SEPARATOR = 0x2029;
+  static const BOM = 0xFEFF;
+
+  static const NUMBER_0 = 0x30;
+  static const NUMBER_9 = 0x39;
+
+  static const LETTER_A = 0x61;
+  static const LETTER_B = 0x62;
+  static const LETTER_E = 0x65;
+  static const LETTER_F = 0x66;
+  static const LETTER_N = 0x6E;
+  static const LETTER_R = 0x72;
+  static const LETTER_T = 0x74;
+  static const LETTER_U = 0x75;
+  static const LETTER_V = 0x76;
+  static const LETTER_X = 0x78;
+  static const LETTER_Z = 0x7A;
+
+  static const LETTER_CAP_A = 0x41;
+  static const LETTER_CAP_F = 0x46;
+  static const LETTER_CAP_L = 0x4C;
+  static const LETTER_CAP_N = 0x4E;
+  static const LETTER_CAP_P = 0x50;
+  static const LETTER_CAP_U = 0x55;
+  static const LETTER_CAP_X = 0x58;
+  static const LETTER_CAP_Z = 0x5A;
+
+  /// Whether this scanner should attempt to recover when parsing invalid YAML.
+  final bool _recover;
+
+  /// A listener to report YAML errors to.
+  final ErrorListener? _errorListener;
+
+  /// The underlying [SpanScanner] used to read characters from the source text.
+  ///
+  /// This is also used to track line and column information and to generate
+  /// [SourceSpan]s.
+  final SpanScanner _scanner;
+
+  /// Whether this scanner has produced a [TokenType.streamStart] token
+  /// indicating the beginning of the YAML stream.
+  var _streamStartProduced = false;
+
+  /// Whether this scanner has produced a [TokenType.streamEnd] token
+  /// indicating the end of the YAML stream.
+  var _streamEndProduced = false;
+
+  /// The queue of tokens yet to be emitted.
+  ///
+  /// These are queued up in advance so that [TokenType.key] tokens can be
+  /// inserted once the scanner determines that a series of tokens represents a
+  /// mapping key.
+  final _tokens = QueueList<Token>();
+
+  /// The number of tokens that have been emitted.
+  ///
+  /// This doesn't count tokens in [_tokens].
+  var _tokensParsed = 0;
+
+  /// Whether the next token in [_tokens] is ready to be returned.
+  ///
+  /// It might not be ready if there may still be a [TokenType.key] inserted
+  /// before it.
+  var _tokenAvailable = false;
+
+  /// The stack of indent levels for the current nested block contexts.
+  ///
+  /// The YAML spec specifies that the initial indentation level is -1 spaces.
+  final _indents = <int>[-1];
+
+  /// Whether a simple key is allowed in this context.
+  ///
+  /// A simple key refers to any mapping key that doesn't have an explicit "?".
+  var _simpleKeyAllowed = true;
+
+  /// The stack of potential simple keys for each level of flow nesting.
+  ///
+  /// Entries in this list may be `null`, indicating that there is no valid
+  /// simple key for the associated level of nesting.
+  ///
+  /// When a ":" is parsed and there's a simple key available, a [TokenType.key]
+  /// token is inserted in [_tokens] before that key's token. This allows the
+  /// parser to tell that the key is intended to be a mapping key.
+  final _simpleKeys = <_SimpleKey?>[null];
+
+  /// The current indentation level.
+  int get _indent => _indents.last;
+
+  /// Whether the scanner's currently positioned in a block-level structure (as
+  /// opposed to flow-level).
+  bool get _inBlockContext => _simpleKeys.length == 1;
+
+  /// Whether the current character is a line break or the end of the source.
+  bool get _isBreakOrEnd => _scanner.isDone || _isBreak;
+
+  /// Whether the current character is a line break.
+  bool get _isBreak => _isBreakAt(0);
+
+  /// Whether the current character is whitespace or the end of the source.
+  bool get _isBlankOrEnd => _isBlankOrEndAt(0);
+
+  /// Whether the current character is whitespace.
+  bool get _isBlank => _isBlankAt(0);
+
+  /// Whether the current character is a valid tag name character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-tag-name.
+  bool get _isTagChar {
+    var char = _scanner.peekChar();
+    if (char == null) return false;
+    switch (char) {
+      case HYPHEN:
+      case SEMICOLON:
+      case SLASH:
+      case COLON:
+      case AT:
+      case AMPERSAND:
+      case EQUALS:
+      case PLUS:
+      case DOLLAR:
+      case PERIOD:
+      case TILDE:
+      case QUESTION:
+      case ASTERISK:
+      case SINGLE_QUOTE:
+      case LEFT_PAREN:
+      case RIGHT_PAREN:
+      case PERCENT:
+        return true;
+      default:
+        return (char >= NUMBER_0 && char <= NUMBER_9) ||
+            (char >= LETTER_A && char <= LETTER_Z) ||
+            (char >= LETTER_CAP_A && char <= LETTER_CAP_Z);
+    }
+  }
+
+  /// Whether the current character is a valid anchor name character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-anchor-name.
+  bool get _isAnchorChar {
+    if (!_isNonSpace) return false;
+
+    switch (_scanner.peekChar()) {
+      case COMMA:
+      case LEFT_SQUARE:
+      case RIGHT_SQUARE:
+      case LEFT_CURLY:
+      case RIGHT_CURLY:
+        return false;
+      default:
+        return true;
+    }
+  }
+
+  /// Whether the character at the current position is a decimal digit.
+  bool get _isDigit {
+    var char = _scanner.peekChar();
+    return char != null && (char >= NUMBER_0 && char <= NUMBER_9);
+  }
+
+  /// Whether the character at the current position is a hexidecimal
+  /// digit.
+  bool get _isHex {
+    var char = _scanner.peekChar();
+    if (char == null) return false;
+    return (char >= NUMBER_0 && char <= NUMBER_9) ||
+        (char >= LETTER_A && char <= LETTER_F) ||
+        (char >= LETTER_CAP_A && char <= LETTER_CAP_F);
+  }
+
+  /// Whether the character at the current position is a plain character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-plain-char(c).
+  bool get _isPlainChar => _isPlainCharAt(0);
+
+  /// Whether the character at the current position is a printable character
+  /// other than a line break or byte-order mark.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#nb-char.
+  bool get _isNonBreak {
+    var char = _scanner.peekChar();
+    return switch (char) {
+      null => false,
+      LF || CR || BOM => false,
+      TAB || NEL => true,
+      _ => _isStandardCharacterAt(0),
+    };
+  }
+
+  /// Whether the character at the current position is a printable character
+  /// other than whitespace.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#nb-char.
+  bool get _isNonSpace {
+    var char = _scanner.peekChar();
+    return switch (char) {
+      null => false,
+      LF || CR || BOM || SP => false,
+      NEL => true,
+      _ => _isStandardCharacterAt(0),
+    };
+  }
+
+  /// Returns Whether or not the current character begins a documentation
+  /// indicator.
+  ///
+  /// If so, this sets the scanner's last match to that indicator.
+  bool get _isDocumentIndicator =>
+      _scanner.column == 0 &&
+      _isBlankOrEndAt(3) &&
+      (_scanner.matches('---') || _scanner.matches('...'));
+
+  /// Creates a scanner that scans [source].
+  Scanner(String source,
+      {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener})
+      : _recover = recover,
+        _errorListener = errorListener,
+        _scanner = SpanScanner.eager(source, sourceUrl: sourceUrl);
+
+  /// Consumes and returns the next token.
+  Token scan() {
+    if (_streamEndProduced) throw StateError('Out of tokens.');
+    if (!_tokenAvailable) _fetchMoreTokens();
+
+    var token = _tokens.removeFirst();
+    _tokenAvailable = false;
+    _tokensParsed++;
+    _streamEndProduced = token.type == TokenType.streamEnd;
+    return token;
+  }
+
+  /// Consumes the next token and returns the one after that.
+  Token? advance() {
+    scan();
+    return peek();
+  }
+
+  /// Returns the next token without consuming it.
+  Token? peek() {
+    if (_streamEndProduced) return null;
+    if (!_tokenAvailable) _fetchMoreTokens();
+    return _tokens.first;
+  }
+
+  /// Ensures that [_tokens] contains at least one token which can be returned.
+  void _fetchMoreTokens() {
+    while (true) {
+      if (_tokens.isNotEmpty) {
+        _staleSimpleKeys();
+
+        // If there are no more tokens to fetch, break.
+        if (_tokens.last.type == TokenType.streamEnd) break;
+
+        // If the current token could be a simple key, we need to scan more
+        // tokens until we determine whether it is or not. Otherwise we might
+        // not emit the `KEY` token before we emit the value of the key.
+        if (!_simpleKeys
+            .any((key) => key != null && key.tokenNumber == _tokensParsed)) {
+          break;
+        }
+      }
+
+      _fetchNextToken();
+    }
+    _tokenAvailable = true;
+  }
+
+  /// The dispatcher for token fetchers.
+  void _fetchNextToken() {
+    if (!_streamStartProduced) {
+      _fetchStreamStart();
+      return;
+    }
+
+    _scanToNextToken();
+    _staleSimpleKeys();
+    _unrollIndent(_scanner.column);
+
+    if (_scanner.isDone) {
+      _fetchStreamEnd();
+      return;
+    }
+
+    if (_scanner.column == 0) {
+      if (_scanner.peekChar() == PERCENT) {
+        _fetchDirective();
+        return;
+      }
+
+      if (_isBlankOrEndAt(3)) {
+        if (_scanner.matches('---')) {
+          _fetchDocumentIndicator(TokenType.documentStart);
+          return;
+        }
+
+        if (_scanner.matches('...')) {
+          _fetchDocumentIndicator(TokenType.documentEnd);
+          return;
+        }
+      }
+    }
+
+    switch (_scanner.peekChar()) {
+      case LEFT_SQUARE:
+        _fetchFlowCollectionStart(TokenType.flowSequenceStart);
+        return;
+      case LEFT_CURLY:
+        _fetchFlowCollectionStart(TokenType.flowMappingStart);
+        return;
+      case RIGHT_SQUARE:
+        _fetchFlowCollectionEnd(TokenType.flowSequenceEnd);
+        return;
+      case RIGHT_CURLY:
+        _fetchFlowCollectionEnd(TokenType.flowMappingEnd);
+        return;
+      case COMMA:
+        _fetchFlowEntry();
+        return;
+      case ASTERISK:
+        _fetchAnchor(anchor: false);
+        return;
+      case AMPERSAND:
+        _fetchAnchor();
+        return;
+      case EXCLAMATION:
+        _fetchTag();
+        return;
+      case SINGLE_QUOTE:
+        _fetchFlowScalar(singleQuote: true);
+        return;
+      case DOUBLE_QUOTE:
+        _fetchFlowScalar();
+        return;
+      case VERTICAL_BAR:
+        if (!_inBlockContext) _invalidScalarCharacter();
+        _fetchBlockScalar(literal: true);
+        return;
+      case RIGHT_ANGLE:
+        if (!_inBlockContext) _invalidScalarCharacter();
+        _fetchBlockScalar();
+        return;
+      case PERCENT:
+      case AT:
+      case GRAVE_ACCENT:
+        _invalidScalarCharacter();
+        return;
+
+      // These characters may sometimes begin plain scalars.
+      case HYPHEN:
+        if (_isPlainCharAt(1)) {
+          _fetchPlainScalar();
+        } else {
+          _fetchBlockEntry();
+        }
+        return;
+      case QUESTION:
+        if (_isPlainCharAt(1)) {
+          _fetchPlainScalar();
+        } else {
+          _fetchKey();
+        }
+        return;
+      case COLON:
+        if (!_inBlockContext && _tokens.isNotEmpty) {
+          // If a colon follows a "JSON-like" value (an explicit map or list, or
+          // a quoted string) it isn't required to have whitespace after it
+          // since it unambiguously describes a map.
+          var token = _tokens.last;
+          if (token.type == TokenType.flowSequenceEnd ||
+              token.type == TokenType.flowMappingEnd ||
+              (token.type == TokenType.scalar &&
+                  (token as ScalarToken).style.isQuoted)) {
+            _fetchValue();
+            return;
+          }
+        }
+
+        if (_isPlainCharAt(1)) {
+          _fetchPlainScalar();
+        } else {
+          _fetchValue();
+        }
+        return;
+      default:
+        if (!_isNonBreak) _invalidScalarCharacter();
+
+        _fetchPlainScalar();
+        return;
+    }
+  }
+
+  /// Throws an error about a disallowed character.
+  void _invalidScalarCharacter() =>
+      _scanner.error('Unexpected character.', length: 1);
+
+  /// Checks the list of potential simple keys and remove the positions that
+  /// cannot contain simple keys anymore.
+  void _staleSimpleKeys() {
+    for (var i = 0; i < _simpleKeys.length; i++) {
+      var key = _simpleKeys[i];
+      if (key == null) continue;
+
+      // libyaml requires that all simple keys be a single line and no longer
+      // than 1024 characters. However, in section 7.4.2 of the spec
+      // (http://yaml.org/spec/1.2/spec.html#id2790832), these restrictions are
+      // only applied when the curly braces are omitted. It's difficult to
+      // retain enough context to know which keys need to have the restriction
+      // placed on them, so for now we go the other direction and allow
+      // everything but multiline simple keys in a block context.
+      if (!_inBlockContext) continue;
+
+      if (key.line == _scanner.line) continue;
+
+      if (key.required) {
+        _reportError(YamlException("Expected ':'.", _scanner.emptySpan));
+        _tokens.insert(key.tokenNumber - _tokensParsed,
+            Token(TokenType.key, key.location.pointSpan() as FileSpan));
+      }
+
+      _simpleKeys[i] = null;
+    }
+  }
+
+  /// Checks if a simple key may start at the current position and saves it if
+  /// so.
+  void _saveSimpleKey() {
+    // A simple key is required at the current position if the scanner is in the
+    // block context and the current column coincides with the indentation
+    // level.
+    var required = _inBlockContext && _indent == _scanner.column;
+
+    // A simple key is required only when it is the first token in the current
+    // line. Therefore it is always allowed. But we add a check anyway.
+    assert(_simpleKeyAllowed || !required);
+
+    if (!_simpleKeyAllowed) return;
+
+    // If the current position may start a simple key, save it.
+    _removeSimpleKey();
+    _simpleKeys[_simpleKeys.length - 1] = _SimpleKey(
+        _tokensParsed + _tokens.length,
+        _scanner.line,
+        _scanner.column,
+        _scanner.location,
+        required: required);
+  }
+
+  /// Removes a potential simple key at the current flow level.
+  void _removeSimpleKey() {
+    var key = _simpleKeys.last;
+    if (key != null && key.required) {
+      throw YamlException("Could not find expected ':' for simple key.",
+          key.location.pointSpan());
+    }
+
+    _simpleKeys[_simpleKeys.length - 1] = null;
+  }
+
+  /// Increases the flow level and resizes the simple key list.
+  void _increaseFlowLevel() {
+    _simpleKeys.add(null);
+  }
+
+  /// Decreases the flow level.
+  void _decreaseFlowLevel() {
+    if (_inBlockContext) return;
+    _simpleKeys.removeLast();
+  }
+
+  /// Pushes the current indentation level to the stack and sets the new level
+  /// if [column] is greater than [_indent].
+  ///
+  /// If it is, appends or inserts the specified token into [_tokens]. If
+  /// [tokenNumber] is provided, the corresponding token will be replaced;
+  /// otherwise, the token will be added at the end.
+  void _rollIndent(int column, TokenType type, SourceLocation location,
+      {int? tokenNumber}) {
+    if (!_inBlockContext) return;
+    if (_indent != -1 && _indent >= column) return;
+
+    // Push the current indentation level to the stack and set the new
+    // indentation level.
+    _indents.add(column);
+
+    // Create a token and insert it into the queue.
+    var token = Token(type, location.pointSpan() as FileSpan);
+    if (tokenNumber == null) {
+      _tokens.add(token);
+    } else {
+      _tokens.insert(tokenNumber - _tokensParsed, token);
+    }
+  }
+
+  /// Pops indentation levels from [_indents] until the current level becomes
+  /// less than or equal to [column].
+  ///
+  /// For each indentation level, appends a [TokenType.blockEnd] token.
+  void _unrollIndent(int column) {
+    if (!_inBlockContext) return;
+
+    while (_indent > column) {
+      _tokens.add(Token(TokenType.blockEnd, _scanner.emptySpan));
+      _indents.removeLast();
+    }
+  }
+
+  /// Pops indentation levels from [_indents] until the current level resets to
+  /// -1.
+  ///
+  /// For each indentation level, appends a [TokenType.blockEnd] token.
+  void _resetIndent() => _unrollIndent(-1);
+
+  /// Produces a [TokenType.streamStart] token.
+  void _fetchStreamStart() {
+    // Much of libyaml's initialization logic here is done in variable
+    // initializers instead.
+    _streamStartProduced = true;
+    _tokens.add(Token(TokenType.streamStart, _scanner.emptySpan));
+  }
+
+  /// Produces a [TokenType.streamEnd] token.
+  void _fetchStreamEnd() {
+    _resetIndent();
+    _removeSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(Token(TokenType.streamEnd, _scanner.emptySpan));
+  }
+
+  /// Produces a [TokenType.versionDirective] or [TokenType.tagDirective]
+  /// token.
+  void _fetchDirective() {
+    _resetIndent();
+    _removeSimpleKey();
+    _simpleKeyAllowed = false;
+    var directive = _scanDirective();
+    if (directive != null) _tokens.add(directive);
+  }
+
+  /// Produces a [TokenType.documentStart] or [TokenType.documentEnd] token.
+  void _fetchDocumentIndicator(TokenType type) {
+    _resetIndent();
+    _removeSimpleKey();
+    _simpleKeyAllowed = false;
+
+    // Consume the indicator token.
+    var start = _scanner.state;
+    _scanner.readCodePoint();
+    _scanner.readCodePoint();
+    _scanner.readCodePoint();
+
+    _tokens.add(Token(type, _scanner.spanFrom(start)));
+  }
+
+  /// Produces a [TokenType.flowSequenceStart] or
+  /// [TokenType.flowMappingStart] token.
+  void _fetchFlowCollectionStart(TokenType type) {
+    _saveSimpleKey();
+    _increaseFlowLevel();
+    _simpleKeyAllowed = true;
+    _addCharToken(type);
+  }
+
+  /// Produces a [TokenType.flowSequenceEnd] or [TokenType.flowMappingEnd]
+  /// token.
+  void _fetchFlowCollectionEnd(TokenType type) {
+    _removeSimpleKey();
+    _decreaseFlowLevel();
+    _simpleKeyAllowed = false;
+    _addCharToken(type);
+  }
+
+  /// Produces a [TokenType.flowEntry] token.
+  void _fetchFlowEntry() {
+    _removeSimpleKey();
+    _simpleKeyAllowed = true;
+    _addCharToken(TokenType.flowEntry);
+  }
+
+  /// Produces a [TokenType.blockEntry] token.
+  void _fetchBlockEntry() {
+    if (_inBlockContext) {
+      if (!_simpleKeyAllowed) {
+        throw YamlException(
+            'Block sequence entries are not allowed here.', _scanner.emptySpan);
+      }
+
+      _rollIndent(
+          _scanner.column, TokenType.blockSequenceStart, _scanner.location);
+    } else {
+      // It is an error for the '-' indicator to occur in the flow context, but
+      // we let the Parser detect and report it because it's able to point to
+      // the context.
+    }
+
+    _removeSimpleKey();
+    _simpleKeyAllowed = true;
+    _addCharToken(TokenType.blockEntry);
+  }
+
+  /// Produces the [TokenType.key] token.
+  void _fetchKey() {
+    if (_inBlockContext) {
+      if (!_simpleKeyAllowed) {
+        throw YamlException(
+            'Mapping keys are not allowed here.', _scanner.emptySpan);
+      }
+
+      _rollIndent(
+          _scanner.column, TokenType.blockMappingStart, _scanner.location);
+    }
+
+    // Simple keys are allowed after `?` in a block context.
+    _simpleKeyAllowed = _inBlockContext;
+    _addCharToken(TokenType.key);
+  }
+
+  /// Produces the [TokenType.value] token.
+  void _fetchValue() {
+    var simpleKey = _simpleKeys.last;
+    if (simpleKey != null) {
+      // Add a [TokenType.KEY] directive before the first token of the simple
+      // key so the parser knows that it's part of a key/value pair.
+      _tokens.insert(simpleKey.tokenNumber - _tokensParsed,
+          Token(TokenType.key, simpleKey.location.pointSpan() as FileSpan));
+
+      // In the block context, we may need to add the
+      // [TokenType.BLOCK_MAPPING_START] token.
+      _rollIndent(
+          simpleKey.column, TokenType.blockMappingStart, simpleKey.location,
+          tokenNumber: simpleKey.tokenNumber);
+
+      // Remove the simple key.
+      _simpleKeys[_simpleKeys.length - 1] = null;
+
+      // A simple key cannot follow another simple key.
+      _simpleKeyAllowed = false;
+    } else if (_inBlockContext) {
+      if (!_simpleKeyAllowed) {
+        throw YamlException(
+            'Mapping values are not allowed here. Did you miss a colon '
+            'earlier?',
+            _scanner.emptySpan);
+      }
+
+      // If we're here, we've found the ':' indicator following a complex key.
+
+      _rollIndent(
+          _scanner.column, TokenType.blockMappingStart, _scanner.location);
+      _simpleKeyAllowed = true;
+    } else if (_simpleKeyAllowed) {
+      // If we're here, we've found the ':' indicator with an empty key. This
+      // behavior differs from libyaml, which disallows empty implicit keys.
+      _simpleKeyAllowed = false;
+      _addCharToken(TokenType.key);
+    }
+
+    _addCharToken(TokenType.value);
+  }
+
+  /// Adds a token with [type] to [_tokens].
+  ///
+  /// The span of the new token is the current character.
+  void _addCharToken(TokenType type) {
+    var start = _scanner.state;
+    _scanner.readCodePoint();
+    _tokens.add(Token(type, _scanner.spanFrom(start)));
+  }
+
+  /// Produces a [TokenType.alias] or [TokenType.anchor] token.
+  void _fetchAnchor({bool anchor = true}) {
+    _saveSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(_scanAnchor(anchor: anchor));
+  }
+
+  /// Produces a [TokenType.tag] token.
+  void _fetchTag() {
+    _saveSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(_scanTag());
+  }
+
+  /// Produces a [TokenType.scalar] token with style [ScalarStyle.LITERAL] or
+  /// [ScalarStyle.FOLDED].
+  void _fetchBlockScalar({bool literal = false}) {
+    _removeSimpleKey();
+    _simpleKeyAllowed = true;
+    _tokens.add(_scanBlockScalar(literal: literal));
+  }
+
+  /// Produces a [TokenType.scalar] token with style [ScalarStyle.SINGLE_QUOTED]
+  /// or [ScalarStyle.DOUBLE_QUOTED].
+  void _fetchFlowScalar({bool singleQuote = false}) {
+    _saveSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(_scanFlowScalar(singleQuote: singleQuote));
+  }
+
+  /// Produces a [TokenType.scalar] token with style [ScalarStyle.PLAIN].
+  void _fetchPlainScalar() {
+    _saveSimpleKey();
+    _simpleKeyAllowed = false;
+    _tokens.add(_scanPlainScalar());
+  }
+
+  /// Eats whitespace and comments until the next token is found.
+  void _scanToNextToken() {
+    var afterLineBreak = false;
+    while (true) {
+      // Allow the BOM to start a line.
+      if (_scanner.column == 0) _scanner.scan('\uFEFF');
+
+      // Eat whitespace.
+      //
+      // libyaml disallows tabs after "-", "?", or ":", but the spec allows
+      // them. See section 6.2: http://yaml.org/spec/1.2/spec.html#id2778241.
+      while (_scanner.peekChar() == SP ||
+          ((!_inBlockContext || !afterLineBreak) &&
+              _scanner.peekChar() == TAB)) {
+        _scanner.readChar();
+      }
+
+      if (_scanner.peekChar() == TAB) {
+        _scanner.error('Tab characters are not allowed as indentation.',
+            length: 1);
+      }
+
+      // Eat a comment until a line break.
+      _skipComment();
+
+      // If we're at a line break, eat it.
+      if (_isBreak) {
+        _skipLine();
+
+        // In the block context, a new line may start a simple key.
+        if (_inBlockContext) _simpleKeyAllowed = true;
+        afterLineBreak = true;
+      } else {
+        // Otherwise we've found a token.
+        break;
+      }
+    }
+  }
+
+  /// Scans a [TokenType.versionDirective] or [TokenType.tagDirective] token.
+  ///
+  ///     %YAML    1.2    # a comment \n
+  ///     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  ///     %TAG    !yaml!  tag:yaml.org,2002:  \n
+  ///     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  Token? _scanDirective() {
+    var start = _scanner.state;
+
+    // Eat '%'.
+    _scanner.readChar();
+
+    Token token;
+    var name = _scanDirectiveName();
+    if (name == 'YAML') {
+      token = _scanVersionDirectiveValue(start);
+    } else if (name == 'TAG') {
+      token = _scanTagDirectiveValue(start);
+    } else {
+      warn('Warning: unknown directive.', _scanner.spanFrom(start));
+
+      // libyaml doesn't support unknown directives, but the spec says to ignore
+      // them and warn: http://yaml.org/spec/1.2/spec.html#id2781147.
+      while (!_isBreakOrEnd) {
+        _scanner.readCodePoint();
+      }
+
+      return null;
+    }
+
+    // Eat the rest of the line, including any comments.
+    _skipBlanks();
+    _skipComment();
+
+    if (!_isBreakOrEnd) {
+      throw YamlException('Expected comment or line break after directive.',
+          _scanner.spanFrom(start));
+    }
+
+    _skipLine();
+    return token;
+  }
+
+  /// Scans a directive name.
+  ///
+  ///      %YAML   1.2     # a comment \n
+  ///       ^^^^
+  ///      %TAG    !yaml!  tag:yaml.org,2002:  \n
+  ///       ^^^
+  String _scanDirectiveName() {
+    // libyaml only allows word characters in directive names, but the spec
+    // disagrees: http://yaml.org/spec/1.2/spec.html#ns-directive-name.
+    var start = _scanner.position;
+    while (_isNonSpace) {
+      _scanner.readCodePoint();
+    }
+
+    var name = _scanner.substring(start);
+    if (name.isEmpty) {
+      throw YamlException('Expected directive name.', _scanner.emptySpan);
+    } else if (!_isBlankOrEnd) {
+      throw YamlException(
+          'Unexpected character in directive name.', _scanner.emptySpan);
+    }
+
+    return name;
+  }
+
+  /// Scans the value of a version directive.
+  ///
+  ///      %YAML   1.2     # a comment \n
+  ///           ^^^^^^
+  Token _scanVersionDirectiveValue(LineScannerState start) {
+    _skipBlanks();
+
+    var major = _scanVersionDirectiveNumber();
+    _scanner.expect('.');
+    var minor = _scanVersionDirectiveNumber();
+
+    return VersionDirectiveToken(_scanner.spanFrom(start), major, minor);
+  }
+
+  /// Scans the version number of a version directive.
+  ///
+  ///      %YAML   1.2     # a comment \n
+  ///              ^
+  ///      %YAML   1.2     # a comment \n
+  ///                ^
+  int _scanVersionDirectiveNumber() {
+    var start = _scanner.position;
+    while (_isDigit) {
+      _scanner.readChar();
+    }
+
+    var number = _scanner.substring(start);
+    if (number.isEmpty) {
+      throw YamlException('Expected version number.', _scanner.emptySpan);
+    }
+
+    return int.parse(number);
+  }
+
+  /// Scans the value of a tag directive.
+  ///
+  ///      %TAG    !yaml!  tag:yaml.org,2002:  \n
+  ///          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  Token _scanTagDirectiveValue(LineScannerState start) {
+    _skipBlanks();
+
+    var handle = _scanTagHandle(directive: true);
+    if (!_isBlank) {
+      throw YamlException('Expected whitespace.', _scanner.emptySpan);
+    }
+
+    _skipBlanks();
+
+    var prefix = _scanTagUri();
+    if (!_isBlankOrEnd) {
+      throw YamlException('Expected whitespace.', _scanner.emptySpan);
+    }
+
+    return TagDirectiveToken(_scanner.spanFrom(start), handle, prefix);
+  }
+
+  /// Scans a [TokenType.anchor] token.
+  Token _scanAnchor({bool anchor = true}) {
+    var start = _scanner.state;
+
+    // Eat the indicator character.
+    _scanner.readCodePoint();
+
+    // libyaml only allows word characters in anchor names, but the spec
+    // disagrees: http://yaml.org/spec/1.2/spec.html#ns-anchor-char.
+    var startPosition = _scanner.position;
+    while (_isAnchorChar) {
+      _scanner.readCodePoint();
+    }
+    var name = _scanner.substring(startPosition);
+
+    var next = _scanner.peekChar();
+    if (name.isEmpty ||
+        (!_isBlankOrEnd &&
+            next != QUESTION &&
+            next != COLON &&
+            next != COMMA &&
+            next != RIGHT_SQUARE &&
+            next != RIGHT_CURLY &&
+            next != PERCENT &&
+            next != AT &&
+            next != GRAVE_ACCENT)) {
+      throw YamlException(
+          'Expected alphanumeric character.', _scanner.emptySpan);
+    }
+
+    if (anchor) {
+      return AnchorToken(_scanner.spanFrom(start), name);
+    } else {
+      return AliasToken(_scanner.spanFrom(start), name);
+    }
+  }
+
+  /// Scans a [TokenType.tag] token.
+  Token _scanTag() {
+    String? handle;
+    String suffix;
+    var start = _scanner.state;
+
+    // Check if the tag is in the canonical form.
+    if (_scanner.peekChar(1) == LEFT_ANGLE) {
+      // Eat '!<'.
+      _scanner.readChar();
+      _scanner.readChar();
+
+      handle = '';
+      suffix = _scanTagUri();
+
+      _scanner.expect('>');
+    } else {
+      // The tag has either the '!suffix' or the '!handle!suffix' form.
+
+      // First, try to scan a handle.
+      handle = _scanTagHandle();
+
+      if (handle.length > 1 && handle.startsWith('!') && handle.endsWith('!')) {
+        suffix = _scanTagUri(flowSeparators: false);
+      } else {
+        suffix = _scanTagUri(head: handle, flowSeparators: false);
+
+        // There was no explicit handle.
+        if (suffix.isEmpty) {
+          // This is the special '!' tag.
+          handle = null;
+          suffix = '!';
+        } else {
+          handle = '!';
+        }
+      }
+    }
+
+    // libyaml insists on whitespace after a tag, but example 7.2 indicates
+    // that it's not required: http://yaml.org/spec/1.2/spec.html#id2786720.
+
+    return TagToken(_scanner.spanFrom(start), handle, suffix);
+  }
+
+  /// Scans a tag handle.
+  String _scanTagHandle({bool directive = false}) {
+    _scanner.expect('!');
+
+    var buffer = StringBuffer('!');
+
+    // libyaml only allows word characters in tags, but the spec disagrees:
+    // http://yaml.org/spec/1.2/spec.html#ns-tag-char.
+    var start = _scanner.position;
+    while (_isTagChar) {
+      _scanner.readChar();
+    }
+    buffer.write(_scanner.substring(start));
+
+    if (_scanner.peekChar() == EXCLAMATION) {
+      buffer.writeCharCode(_scanner.readCodePoint());
+    } else {
+      // It's either the '!' tag or not really a tag handle. If it's a %TAG
+      // directive, it's an error. If it's a tag token, it must be part of a
+      // URI.
+      if (directive && buffer.toString() != '!') _scanner.expect('!');
+    }
+
+    return buffer.toString();
+  }
+
+  /// Scans a tag URI.
+  ///
+  /// [head] is the initial portion of the tag that's already been scanned.
+  /// [flowSeparators] indicates whether the tag URI can contain flow
+  /// separators.
+  String _scanTagUri({String? head, bool flowSeparators = true}) {
+    var length = head == null ? 0 : head.length;
+    var buffer = StringBuffer();
+
+    // Copy the head if needed.
+    //
+    // Note that we don't copy the leading '!' character.
+    if (length > 1) buffer.write(head!.substring(1));
+
+    // The set of characters that may appear in URI is as follows:
+    //
+    //      '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&',
+    //      '=', '+', '$', ',', '.', '!', '~', '*', '\'', '(', ')', '[', ']',
+    //      '%'.
+    //
+    // In a shorthand tag annotation, the flow separators ',', '[', and ']' are
+    // disallowed.
+    var start = _scanner.position;
+    var char = _scanner.peekChar();
+    while (_isTagChar ||
+        (flowSeparators &&
+            (char == COMMA || char == LEFT_SQUARE || char == RIGHT_SQUARE))) {
+      _scanner.readChar();
+      char = _scanner.peekChar();
+    }
+
+    // libyaml manually decodes the URL, but we don't have to do that.
+    return Uri.decodeFull(_scanner.substring(start));
+  }
+
+  /// Scans a block scalar.
+  Token _scanBlockScalar({bool literal = false}) {
+    var start = _scanner.state;
+
+    // Eat the indicator '|' or '>'.
+    _scanner.readCodePoint();
+
+    // Check for a chomping indicator.
+    var chomping = _Chomping.clip;
+    var increment = 0;
+    var char = _scanner.peekChar();
+    if (char == PLUS || char == HYPHEN) {
+      chomping = char == PLUS ? _Chomping.keep : _Chomping.strip;
+      _scanner.readCodePoint();
+
+      // Check for an indentation indicator.
+      if (_isDigit) {
+        // Check that the indentation is greater than 0.
+        if (_scanner.peekChar() == NUMBER_0) {
+          throw YamlException('0 may not be used as an indentation indicator.',
+              _scanner.spanFrom(start));
+        }
+
+        increment = _scanner.readCodePoint() - NUMBER_0;
+      }
+    } else if (_isDigit) {
+      // Do the same as above, but in the opposite order.
+      if (_scanner.peekChar() == NUMBER_0) {
+        throw YamlException('0 may not be used as an indentation indicator.',
+            _scanner.spanFrom(start));
+      }
+
+      increment = _scanner.readCodePoint() - NUMBER_0;
+
+      char = _scanner.peekChar();
+      if (char == PLUS || char == HYPHEN) {
+        chomping = char == PLUS ? _Chomping.keep : _Chomping.strip;
+        _scanner.readCodePoint();
+      }
+    }
+
+    // Eat whitespace and comments to the end of the line.
+    _skipBlanks();
+    _skipComment();
+
+    // Check if we're at the end of the line.
+    if (!_isBreakOrEnd) {
+      throw YamlException(
+          'Expected comment or line break.', _scanner.emptySpan);
+    }
+
+    _skipLine();
+
+    // If the block scalar has an explicit indentation indicator, add that to
+    // the current indentation to get the indentation level for the scalar's
+    // contents.
+    var indent = 0;
+    if (increment != 0) {
+      indent = _indent >= 0 ? _indent + increment : increment;
+    }
+
+    // Scan the leading line breaks to determine the indentation level if
+    // needed.
+    var pair = _scanBlockScalarBreaks(indent);
+    indent = pair.indent;
+    var trailingBreaks = pair.trailingBreaks;
+
+    // Scan the block scalar contents.
+    var buffer = StringBuffer();
+    var leadingBreak = '';
+    var leadingBlank = false;
+    var trailingBlank = false;
+    var end = _scanner.state;
+    while (_scanner.column == indent && !_scanner.isDone) {
+      // Check for a document indicator. libyaml doesn't do this, but the spec
+      // mandates it. See example 9.5:
+      // http://yaml.org/spec/1.2/spec.html#id2801606.
+      if (_isDocumentIndicator) break;
+
+      // We are at the beginning of a non-empty line.
+
+      // Is there trailing whitespace?
+      trailingBlank = _isBlank;
+
+      // Check if we need to fold the leading line break.
+      if (!literal &&
+          leadingBreak.isNotEmpty &&
+          !leadingBlank &&
+          !trailingBlank) {
+        // Do we need to join the lines with a space?
+        if (trailingBreaks.isEmpty) buffer.writeCharCode(SP);
+      } else {
+        buffer.write(leadingBreak);
+      }
+      leadingBreak = '';
+
+      // Append the remaining line breaks.
+      buffer.write(trailingBreaks);
+
+      // Is there leading whitespace?
+      leadingBlank = _isBlank;
+
+      var startPosition = _scanner.position;
+      while (!_isBreakOrEnd) {
+        _scanner.readCodePoint();
+      }
+      buffer.write(_scanner.substring(startPosition));
+      end = _scanner.state;
+
+      // libyaml always reads a line here, but this breaks on block scalars at
+      // the end of the document that end without newlines. See example 8.1:
+      // http://yaml.org/spec/1.2/spec.html#id2793888.
+      if (!_scanner.isDone) leadingBreak = _readLine();
+
+      // Eat the following indentation and spaces.
+      var pair = _scanBlockScalarBreaks(indent);
+      indent = pair.indent;
+      trailingBreaks = pair.trailingBreaks;
+    }
+
+    // Chomp the tail.
+    if (chomping != _Chomping.strip) buffer.write(leadingBreak);
+    if (chomping == _Chomping.keep) buffer.write(trailingBreaks);
+
+    return ScalarToken(_scanner.spanFrom(start, end), buffer.toString(),
+        literal ? ScalarStyle.LITERAL : ScalarStyle.FOLDED);
+  }
+
+  /// Scans indentation spaces and line breaks for a block scalar.
+  ///
+  /// Determines the intendation level if needed. Returns the new indentation
+  /// level and the text of the line breaks.
+  ({int indent, String trailingBreaks}) _scanBlockScalarBreaks(int indent) {
+    var maxIndent = 0;
+    var breaks = StringBuffer();
+
+    while (true) {
+      while ((indent == 0 || _scanner.column < indent) &&
+          _scanner.peekChar() == SP) {
+        _scanner.readChar();
+      }
+
+      if (_scanner.column > maxIndent) maxIndent = _scanner.column;
+
+      // libyaml throws an error here if a tab character is detected, but the
+      // spec treats tabs like any other non-space character. See example 8.2:
+      // http://yaml.org/spec/1.2/spec.html#id2794311.
+
+      if (!_isBreak) break;
+      breaks.write(_readLine());
+    }
+
+    if (indent == 0) {
+      indent = maxIndent;
+      if (indent < _indent + 1) indent = _indent + 1;
+
+      // libyaml forces indent to be at least 1 here, but that doesn't seem to
+      // be supported by the spec.
+    }
+
+    return (indent: indent, trailingBreaks: breaks.toString());
+  }
+
+  // Scans a quoted scalar.
+  Token _scanFlowScalar({bool singleQuote = false}) {
+    var start = _scanner.state;
+    var buffer = StringBuffer();
+
+    // Eat the left quote.
+    _scanner.readChar();
+
+    while (true) {
+      // Check that there are no document indicators at the beginning of the
+      // line.
+      if (_isDocumentIndicator) {
+        _scanner.error('Unexpected document indicator.');
+      }
+
+      if (_scanner.isDone) {
+        throw YamlException('Unexpected end of file.', _scanner.emptySpan);
+      }
+
+      var leadingBlanks = false;
+      while (!_isBlankOrEnd) {
+        var char = _scanner.peekChar();
+        if (singleQuote &&
+            char == SINGLE_QUOTE &&
+            _scanner.peekChar(1) == SINGLE_QUOTE) {
+          // An escaped single quote.
+          _scanner.readChar();
+          _scanner.readChar();
+          buffer.writeCharCode(SINGLE_QUOTE);
+        } else if (char == (singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE)) {
+          // The closing quote.
+          break;
+        } else if (!singleQuote && char == BACKSLASH && _isBreakAt(1)) {
+          // An escaped newline.
+          _scanner.readChar();
+          _skipLine();
+          leadingBlanks = true;
+          break;
+        } else if (!singleQuote && char == BACKSLASH) {
+          var escapeStart = _scanner.state;
+
+          // An escape sequence.
+          int? codeLength;
+          switch (_scanner.peekChar(1)) {
+            case NUMBER_0:
+              buffer.writeCharCode(NULL);
+              break;
+            case LETTER_A:
+              buffer.writeCharCode(BELL);
+              break;
+            case LETTER_B:
+              buffer.writeCharCode(BACKSPACE);
+              break;
+            case LETTER_T:
+            case TAB:
+              buffer.writeCharCode(TAB);
+              break;
+            case LETTER_N:
+              buffer.writeCharCode(LF);
+              break;
+            case LETTER_V:
+              buffer.writeCharCode(VERTICAL_TAB);
+              break;
+            case LETTER_F:
+              buffer.writeCharCode(FORM_FEED);
+              break;
+            case LETTER_R:
+              buffer.writeCharCode(CR);
+              break;
+            case LETTER_E:
+              buffer.writeCharCode(ESCAPE);
+              break;
+            case SP:
+            case DOUBLE_QUOTE:
+            case SLASH:
+            case BACKSLASH:
+              // libyaml doesn't support an escaped forward slash, but it was
+              // added in YAML 1.2. See section 5.7:
+              // http://yaml.org/spec/1.2/spec.html#id2776092
+              buffer.writeCharCode(_scanner.peekChar(1)!);
+              break;
+            case LETTER_CAP_N:
+              buffer.writeCharCode(NEL);
+              break;
+            case UNDERSCORE:
+              buffer.writeCharCode(NBSP);
+              break;
+            case LETTER_CAP_L:
+              buffer.writeCharCode(LINE_SEPARATOR);
+              break;
+            case LETTER_CAP_P:
+              buffer.writeCharCode(PARAGRAPH_SEPARATOR);
+              break;
+            case LETTER_X:
+              codeLength = 2;
+              break;
+            case LETTER_U:
+              codeLength = 4;
+              break;
+            case LETTER_CAP_U:
+              codeLength = 8;
+              break;
+            default:
+              throw YamlException(
+                  'Unknown escape character.', _scanner.spanFrom(escapeStart));
+          }
+
+          _scanner.readChar();
+          _scanner.readChar();
+
+          if (codeLength != null) {
+            var value = 0;
+            for (var i = 0; i < codeLength; i++) {
+              if (!_isHex) {
+                _scanner.readChar();
+                throw YamlException(
+                    'Expected $codeLength-digit hexidecimal number.',
+                    _scanner.spanFrom(escapeStart));
+              }
+
+              value = (value << 4) + _asHex(_scanner.readChar());
+            }
+
+            // Check the value and write the character.
+            if ((value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF) {
+              throw YamlException('Invalid Unicode character escape code.',
+                  _scanner.spanFrom(escapeStart));
+            }
+
+            buffer.writeCharCode(value);
+          }
+        } else {
+          buffer.writeCharCode(_scanner.readCodePoint());
+        }
+      }
+
+      // Check if we're at the end of a scalar.
+      if (_scanner.peekChar() == (singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE)) {
+        break;
+      }
+
+      var whitespace = StringBuffer();
+      var leadingBreak = '';
+      var trailingBreaks = StringBuffer();
+      while (_isBlank || _isBreak) {
+        if (_isBlank) {
+          // Consume a space or a tab.
+          if (!leadingBlanks) {
+            whitespace.writeCharCode(_scanner.readChar());
+          } else {
+            _scanner.readChar();
+          }
+        } else {
+          // Check if it's a first line break.
+          if (!leadingBlanks) {
+            whitespace.clear();
+            leadingBreak = _readLine();
+            leadingBlanks = true;
+          } else {
+            trailingBreaks.write(_readLine());
+          }
+        }
+      }
+
+      // Join the whitespace or fold line breaks.
+      if (leadingBlanks) {
+        if (leadingBreak.isNotEmpty && trailingBreaks.isEmpty) {
+          buffer.writeCharCode(SP);
+        } else {
+          buffer.write(trailingBreaks);
+        }
+      } else {
+        buffer.write(whitespace);
+        whitespace.clear();
+      }
+    }
+
+    // Eat the right quote.
+    _scanner.readChar();
+
+    return ScalarToken(_scanner.spanFrom(start), buffer.toString(),
+        singleQuote ? ScalarStyle.SINGLE_QUOTED : ScalarStyle.DOUBLE_QUOTED);
+  }
+
+  /// Scans a plain scalar.
+  Token _scanPlainScalar() {
+    var start = _scanner.state;
+    var end = _scanner.state;
+    var buffer = StringBuffer();
+    var leadingBreak = '';
+    var trailingBreaks = '';
+    var whitespace = StringBuffer();
+    var indent = _indent + 1;
+
+    while (true) {
+      // Check for a document indicator.
+      if (_isDocumentIndicator) break;
+
+      // Check for a comment.
+      if (_scanner.peekChar() == HASH) break;
+
+      if (_isPlainChar) {
+        // Join the whitespace or fold line breaks.
+        if (leadingBreak.isNotEmpty) {
+          if (trailingBreaks.isEmpty) {
+            buffer.writeCharCode(SP);
+          } else {
+            buffer.write(trailingBreaks);
+          }
+          leadingBreak = '';
+          trailingBreaks = '';
+        } else {
+          buffer.write(whitespace);
+          whitespace.clear();
+        }
+      }
+
+      // libyaml's notion of valid identifiers differs substantially from YAML
+      // 1.2's. We use [_isPlainChar] instead of libyaml's character here.
+      var startPosition = _scanner.position;
+      while (_isPlainChar) {
+        _scanner.readCodePoint();
+      }
+      buffer.write(_scanner.substring(startPosition));
+      end = _scanner.state;
+
+      // Is it the end?
+      if (!_isBlank && !_isBreak) break;
+
+      while (_isBlank || _isBreak) {
+        if (_isBlank) {
+          // Check for a tab character messing up the intendation.
+          if (leadingBreak.isNotEmpty &&
+              _scanner.column < indent &&
+              _scanner.peekChar() == TAB) {
+            _scanner.error('Expected a space but found a tab.', length: 1);
+          }
+
+          if (leadingBreak.isEmpty) {
+            whitespace.writeCharCode(_scanner.readChar());
+          } else {
+            _scanner.readChar();
+          }
+        } else {
+          // Check if it's a first line break.
+          if (leadingBreak.isEmpty) {
+            leadingBreak = _readLine();
+            whitespace.clear();
+          } else {
+            trailingBreaks = _readLine();
+          }
+        }
+      }
+
+      // Check the indentation level.
+      if (_inBlockContext && _scanner.column < indent) break;
+    }
+
+    // Allow a simple key after a plain scalar with leading blanks.
+    if (leadingBreak.isNotEmpty) _simpleKeyAllowed = true;
+
+    return ScalarToken(
+        _scanner.spanFrom(start, end), buffer.toString(), ScalarStyle.PLAIN);
+  }
+
+  /// Moves past the current line break, if there is one.
+  void _skipLine() {
+    var char = _scanner.peekChar();
+    if (char != CR && char != LF) return;
+    _scanner.readChar();
+    if (char == CR && _scanner.peekChar() == LF) _scanner.readChar();
+  }
+
+  // Moves past the current line break and returns a newline.
+  String _readLine() {
+    var char = _scanner.peekChar();
+
+    // libyaml supports NEL, PS, and LS characters as line separators, but this
+    // is explicitly forbidden in section 5.4 of the YAML spec.
+    if (char != CR && char != LF) {
+      throw YamlException('Expected newline.', _scanner.emptySpan);
+    }
+
+    _scanner.readChar();
+    // CR LF | CR | LF -> LF
+    if (char == CR && _scanner.peekChar() == LF) _scanner.readChar();
+    return '\n';
+  }
+
+  // Returns whether the character at [offset] is whitespace.
+  bool _isBlankAt(int offset) {
+    var char = _scanner.peekChar(offset);
+    return char == SP || char == TAB;
+  }
+
+  // Returns whether the character at [offset] is a line break.
+  bool _isBreakAt(int offset) {
+    // Libyaml considers NEL, LS, and PS to be line breaks as well, but that's
+    // contrary to the spec.
+    var char = _scanner.peekChar(offset);
+    return char == CR || char == LF;
+  }
+
+  // Returns whether the character at [offset] is whitespace or past the end of
+  // the source.
+  bool _isBlankOrEndAt(int offset) {
+    var char = _scanner.peekChar(offset);
+    return char == null ||
+        char == SP ||
+        char == TAB ||
+        char == CR ||
+        char == LF;
+  }
+
+  /// Returns whether the character at [offset] is a plain character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-plain-char(c).
+  bool _isPlainCharAt(int offset) {
+    switch (_scanner.peekChar(offset)) {
+      case COLON:
+        return _isPlainSafeAt(offset + 1);
+      case HASH:
+        var previous = _scanner.peekChar(offset - 1);
+        return previous != SP && previous != TAB;
+      default:
+        return _isPlainSafeAt(offset);
+    }
+  }
+
+  /// Returns whether the character at [offset] is a plain-safe character.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#ns-plain-safe(c).
+  bool _isPlainSafeAt(int offset) {
+    var char = _scanner.peekChar(offset);
+    return switch (char) {
+      null => false,
+      COMMA ||
+      LEFT_SQUARE ||
+      RIGHT_SQUARE ||
+      LEFT_CURLY ||
+      RIGHT_CURLY =>
+        // These characters are delimiters in a flow context and thus are only
+        // safe in a block context.
+        _inBlockContext,
+      SP || TAB || LF || CR || BOM => false,
+      NEL => true,
+      _ => _isStandardCharacterAt(offset)
+    };
+  }
+
+  bool _isStandardCharacterAt(int offset) {
+    var first = _scanner.peekChar(offset);
+    if (first == null) return false;
+
+    if (isHighSurrogate(first)) {
+      var next = _scanner.peekChar(offset + 1);
+      // A surrogate pair encodes code points from U+010000 to U+10FFFF, so it
+      // must be a standard character.
+      return next != null && isLowSurrogate(next);
+    }
+
+    return _isStandardCharacter(first);
+  }
+
+  bool _isStandardCharacter(int char) =>
+      (char >= 0x0020 && char <= 0x007E) ||
+      (char >= 0x00A0 && char <= 0xD7FF) ||
+      (char >= 0xE000 && char <= 0xFFFD);
+
+  /// Returns the hexidecimal value of [char].
+  int _asHex(int char) {
+    if (char <= NUMBER_9) return char - NUMBER_0;
+    if (char <= LETTER_CAP_F) return 10 + char - LETTER_CAP_A;
+    return 10 + char - LETTER_A;
+  }
+
+  /// Moves the scanner past any blank characters.
+  void _skipBlanks() {
+    while (_isBlank) {
+      _scanner.readChar();
+    }
+  }
+
+  /// Moves the scanner past a comment, if one starts at the current position.
+  void _skipComment() {
+    if (_scanner.peekChar() != HASH) return;
+    while (!_isBreakOrEnd) {
+      _scanner.readChar();
+    }
+  }
+
+  /// Reports a [YamlException] to [_errorListener] if [_recover] is true,
+  /// otherwise throws the exception.
+  void _reportError(YamlException exception) {
+    if (!_recover) {
+      throw exception;
+    }
+    _errorListener?.onError(exception);
+  }
+}
+
+/// A record of the location of a potential simple key.
+class _SimpleKey {
+  /// The index of the token that begins the simple key.
+  ///
+  /// This is the index relative to all tokens emitted, rather than relative to
+  /// [location].
+  final int tokenNumber;
+
+  /// The source location of the beginning of the simple key.
+  ///
+  /// This is used for error reporting and for determining when a simple key is
+  /// no longer on the current line.
+  final SourceLocation location;
+
+  /// The line on which the key appears.
+  ///
+  /// We could get this from [location], but that requires a binary search
+  /// whereas this is O(1).
+  final int line;
+
+  /// The column on which the key appears.
+  ///
+  /// We could get this from [location], but that requires a binary search
+  /// whereas this is O(1).
+  final int column;
+
+  /// Whether this key must exist for the document to be scanned.
+  final bool required;
+
+  _SimpleKey(
+    this.tokenNumber,
+    this.line,
+    this.column,
+    this.location, {
+    required this.required,
+  });
+}
+
+/// The ways to handle trailing whitespace for a block scalar.
+///
+/// See http://yaml.org/spec/1.2/spec.html#id2794534.
+enum _Chomping {
+  /// All trailing whitespace is discarded.
+  strip,
+
+  /// A single trailing newline is retained.
+  clip,
+
+  /// All trailing whitespace is preserved.
+  keep
+}
diff --git a/pkgs/yaml/lib/src/style.dart b/pkgs/yaml/lib/src/style.dart
new file mode 100644
index 0000000..96c3b94
--- /dev/null
+++ b/pkgs/yaml/lib/src/style.dart
@@ -0,0 +1,79 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: constant_identifier_names
+
+import 'yaml_node.dart';
+
+/// An enum of source scalar styles.
+class ScalarStyle {
+  /// No source style was specified.
+  ///
+  /// This usually indicates a scalar constructed with [YamlScalar.wrap].
+  static const ANY = ScalarStyle._('ANY');
+
+  /// The plain scalar style, unquoted and without a prefix.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#style/flow/plain.
+  static const PLAIN = ScalarStyle._('PLAIN');
+
+  /// The literal scalar style, with a `|` prefix.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#id2795688.
+  static const LITERAL = ScalarStyle._('LITERAL');
+
+  /// The folded scalar style, with a `>` prefix.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#id2796251.
+  static const FOLDED = ScalarStyle._('FOLDED');
+
+  /// The single-quoted scalar style.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#style/flow/single-quoted.
+  static const SINGLE_QUOTED = ScalarStyle._('SINGLE_QUOTED');
+
+  /// The double-quoted scalar style.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#style/flow/double-quoted.
+  static const DOUBLE_QUOTED = ScalarStyle._('DOUBLE_QUOTED');
+
+  final String name;
+
+  /// Whether this is a quoted style ([SINGLE_QUOTED] or [DOUBLE_QUOTED]).
+  bool get isQuoted => this == SINGLE_QUOTED || this == DOUBLE_QUOTED;
+
+  const ScalarStyle._(this.name);
+
+  @override
+  String toString() => name;
+}
+
+/// An enum of collection styles.
+class CollectionStyle {
+  /// No source style was specified.
+  ///
+  /// This usually indicates a collection constructed with [YamlList.wrap] or
+  /// [YamlMap.wrap].
+  static const ANY = CollectionStyle._('ANY');
+
+  /// The indentation-based block style.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#id2797293.
+  static const BLOCK = CollectionStyle._('BLOCK');
+
+  /// The delimiter-based block style.
+  ///
+  /// See http://yaml.org/spec/1.2/spec.html#id2790088.
+  static const FLOW = CollectionStyle._('FLOW');
+
+  final String name;
+
+  const CollectionStyle._(this.name);
+
+  @override
+  String toString() => name;
+}
diff --git a/pkgs/yaml/lib/src/token.dart b/pkgs/yaml/lib/src/token.dart
new file mode 100644
index 0000000..7d5d6bc
--- /dev/null
+++ b/pkgs/yaml/lib/src/token.dart
@@ -0,0 +1,158 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+import 'scanner.dart';
+import 'style.dart';
+
+/// A token emitted by a [Scanner].
+class Token {
+  final TokenType type;
+  final FileSpan span;
+
+  Token(this.type, this.span);
+
+  @override
+  String toString() => type.toString();
+}
+
+/// A token representing a `%YAML` directive.
+class VersionDirectiveToken implements Token {
+  @override
+  TokenType get type => TokenType.versionDirective;
+  @override
+  final FileSpan span;
+
+  /// The declared major version of the document.
+  final int major;
+
+  /// The declared minor version of the document.
+  final int minor;
+
+  VersionDirectiveToken(this.span, this.major, this.minor);
+
+  @override
+  String toString() => 'VERSION_DIRECTIVE $major.$minor';
+}
+
+/// A token representing a `%TAG` directive.
+class TagDirectiveToken implements Token {
+  @override
+  TokenType get type => TokenType.tagDirective;
+  @override
+  final FileSpan span;
+
+  /// The tag handle used in the document.
+  final String handle;
+
+  /// The tag prefix that the handle maps to.
+  final String prefix;
+
+  TagDirectiveToken(this.span, this.handle, this.prefix);
+
+  @override
+  String toString() => 'TAG_DIRECTIVE $handle $prefix';
+}
+
+/// A token representing an anchor (`&foo`).
+class AnchorToken implements Token {
+  @override
+  TokenType get type => TokenType.anchor;
+  @override
+  final FileSpan span;
+
+  final String name;
+
+  AnchorToken(this.span, this.name);
+
+  @override
+  String toString() => 'ANCHOR $name';
+}
+
+/// A token representing an alias (`*foo`).
+class AliasToken implements Token {
+  @override
+  TokenType get type => TokenType.alias;
+  @override
+  final FileSpan span;
+
+  final String name;
+
+  AliasToken(this.span, this.name);
+
+  @override
+  String toString() => 'ALIAS $name';
+}
+
+/// A token representing a tag (`!foo`).
+class TagToken implements Token {
+  @override
+  TokenType get type => TokenType.tag;
+  @override
+  final FileSpan span;
+
+  /// The tag handle for named tags.
+  final String? handle;
+
+  /// The tag suffix.
+  final String suffix;
+
+  TagToken(this.span, this.handle, this.suffix);
+
+  @override
+  String toString() => 'TAG $handle $suffix';
+}
+
+/// A scalar value.
+class ScalarToken implements Token {
+  @override
+  TokenType get type => TokenType.scalar;
+  @override
+  final FileSpan span;
+
+  /// The unparsed contents of the value..
+  final String value;
+
+  /// The style of the scalar in the original source.
+  final ScalarStyle style;
+
+  ScalarToken(this.span, this.value, this.style);
+
+  @override
+  String toString() => 'SCALAR $style "$value"';
+}
+
+/// The types of [Token] objects.
+enum TokenType {
+  streamStart,
+  streamEnd,
+
+  versionDirective,
+  tagDirective,
+  documentStart,
+  documentEnd,
+
+  blockSequenceStart,
+  blockMappingStart,
+  blockEnd,
+
+  flowSequenceStart,
+  flowSequenceEnd,
+  flowMappingStart,
+  flowMappingEnd,
+
+  blockEntry,
+  flowEntry,
+  key,
+  value,
+
+  alias,
+  anchor,
+  tag,
+  scalar
+}
diff --git a/pkgs/yaml/lib/src/utils.dart b/pkgs/yaml/lib/src/utils.dart
new file mode 100644
index 0000000..0dc132f
--- /dev/null
+++ b/pkgs/yaml/lib/src/utils.dart
@@ -0,0 +1,40 @@
+// Copyright (c) 2013, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+/// Print a warning.
+///
+/// If [span] is passed, associates the warning with that span.
+void warn(String message, [SourceSpan? span]) =>
+    yamlWarningCallback(message, span);
+
+/// A callback for emitting a warning.
+///
+/// [message] is the text of the warning. If [span] is passed, it's the portion
+/// of the document that the warning is associated with and should be included
+/// in the printed warning.
+typedef YamlWarningCallback = void Function(String message, [SourceSpan? span]);
+
+/// A callback for emitting a warning.
+///
+/// In a very few cases, the YAML spec indicates that an implementation should
+/// emit a warning. To do so, it calls this callback. The default implementation
+/// prints a message using [print].
+// ignore: prefer_function_declarations_over_variables
+YamlWarningCallback yamlWarningCallback = (message, [SourceSpan? span]) {
+  // TODO(nweiz): Print to stderr with color when issue 6943 is fixed and
+  // dart:io is available.
+  if (span != null) message = span.message(message);
+  print(message);
+};
+
+/// Whether [codeUnit] is a UTF-16 high surrogate.
+bool isHighSurrogate(int codeUnit) => codeUnit >>> 10 == 0x36;
+
+/// Whether [codeUnit] is a UTF-16 low surrogate.
+bool isLowSurrogate(int codeUnit) => codeUnit >>> 10 == 0x37;
diff --git a/pkgs/yaml/lib/src/yaml_document.dart b/pkgs/yaml/lib/src/yaml_document.dart
new file mode 100644
index 0000000..da6aa1e
--- /dev/null
+++ b/pkgs/yaml/lib/src/yaml_document.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:collection';
+
+import 'package:source_span/source_span.dart';
+
+import 'yaml_node.dart';
+
+/// A YAML document, complete with metadata.
+class YamlDocument {
+  /// The contents of the document.
+  final YamlNode contents;
+
+  /// The span covering the entire document.
+  final SourceSpan span;
+
+  /// The version directive for the document, if any.
+  final VersionDirective? versionDirective;
+
+  /// The tag directives for the document.
+  final List<TagDirective> tagDirectives;
+
+  /// Whether the beginning of the document was implicit (versus explicit via
+  /// `===`).
+  final bool startImplicit;
+
+  /// Whether the end of the document was implicit (versus explicit via `...`).
+  final bool endImplicit;
+
+  /// Users of the library should not use this constructor.
+  YamlDocument.internal(this.contents, this.span, this.versionDirective,
+      List<TagDirective> tagDirectives,
+      {this.startImplicit = false, this.endImplicit = false})
+      : tagDirectives = UnmodifiableListView(tagDirectives);
+
+  @override
+  String toString() => contents.toString();
+}
+
+/// A directive indicating which version of YAML a document was written to.
+class VersionDirective {
+  /// The major version number.
+  final int major;
+
+  /// The minor version number.
+  final int minor;
+
+  VersionDirective(this.major, this.minor);
+
+  @override
+  String toString() => '%YAML $major.$minor';
+}
+
+/// A directive describing a custom tag handle.
+class TagDirective {
+  /// The handle for use in the document.
+  final String handle;
+
+  /// The prefix that the handle maps to.
+  final String prefix;
+
+  TagDirective(this.handle, this.prefix);
+
+  @override
+  String toString() => '%TAG $handle $prefix';
+}
diff --git a/pkgs/yaml/lib/src/yaml_exception.dart b/pkgs/yaml/lib/src/yaml_exception.dart
new file mode 100644
index 0000000..7aa5389
--- /dev/null
+++ b/pkgs/yaml/lib/src/yaml_exception.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2013, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:source_span/source_span.dart';
+
+/// An error thrown by the YAML processor.
+class YamlException extends SourceSpanFormatException {
+  YamlException(super.message, super.span);
+}
diff --git a/pkgs/yaml/lib/src/yaml_node.dart b/pkgs/yaml/lib/src/yaml_node.dart
new file mode 100644
index 0000000..bd17b6c
--- /dev/null
+++ b/pkgs/yaml/lib/src/yaml_node.dart
@@ -0,0 +1,191 @@
+// Copyright (c) 2012, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:collection' as collection;
+
+import 'package:collection/collection.dart';
+import 'package:source_span/source_span.dart';
+
+import 'event.dart';
+import 'null_span.dart';
+import 'style.dart';
+import 'yaml_node_wrapper.dart';
+
+/// An interface for parsed nodes from a YAML source tree.
+///
+/// [YamlMap]s and [YamlList]s implement this interface in addition to the
+/// normal [Map] and [List] interfaces, so any maps and lists will be
+/// [YamlNode]s regardless of how they're accessed.
+///
+/// Scalars values like strings and numbers, on the other hand, don't have this
+/// interface by default. Instead, they can be accessed as [YamlScalar]s via
+/// [YamlMap.nodes] or [YamlList.nodes].
+abstract class YamlNode {
+  /// The source span for this node.
+  ///
+  /// [SourceSpan.message] can be used to produce a human-friendly message about
+  /// this node.
+  SourceSpan get span => _span;
+  SourceSpan _span;
+
+  YamlNode._(this._span);
+
+  /// The inner value of this node.
+  ///
+  /// For [YamlScalar]s, this will return the wrapped value. For [YamlMap] and
+  /// [YamlList], it will return `this`, since they already implement [Map] and
+  /// [List], respectively.
+  dynamic get value;
+}
+
+/// A read-only [Map] parsed from YAML.
+class YamlMap extends YamlNode with collection.MapMixin, UnmodifiableMapMixin {
+  /// A view of `this` where the keys and values are guaranteed to be
+  /// [YamlNode]s.
+  ///
+  /// The key type is `dynamic` to allow values to be accessed using
+  /// non-[YamlNode] keys, but [Map.keys] and [Map.forEach] will always expose
+  /// them as [YamlNode]s. For example, for `{"foo": [1, 2, 3]}` [nodes] will be
+  /// a map from a [YamlScalar] to a [YamlList], but since the key type is
+  /// `dynamic` `map.nodes["foo"]` will still work.
+  final Map<dynamic, YamlNode> nodes;
+
+  /// The style used for the map in the original document.
+  final CollectionStyle style;
+
+  @override
+  Map get value => this;
+
+  @override
+  Iterable get keys => nodes.keys.map((node) => (node as YamlNode).value);
+
+  /// Creates an empty YamlMap.
+  ///
+  /// This map's [span] won't have useful location information. However, it will
+  /// have a reasonable implementation of [SourceSpan.message]. If [sourceUrl]
+  /// is passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  factory YamlMap({Object? sourceUrl}) => YamlMapWrapper(const {}, sourceUrl);
+
+  /// Wraps a Dart map so that it can be accessed (recursively) like a
+  /// [YamlMap].
+  ///
+  /// Any [SourceSpan]s returned by this map or its children will be dummies
+  /// without useful location information. However, they will have a reasonable
+  /// implementation of [SourceSpan.message]. If [sourceUrl] is
+  /// passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  factory YamlMap.wrap(Map dartMap,
+          {Object? sourceUrl, CollectionStyle style = CollectionStyle.ANY}) =>
+      YamlMapWrapper(dartMap, sourceUrl, style: style);
+
+  /// Users of the library should not use this constructor.
+  YamlMap.internal(Map<dynamic, YamlNode> nodes, super.span, this.style)
+      : nodes = UnmodifiableMapView<dynamic, YamlNode>(nodes),
+        super._();
+
+  @override
+  dynamic operator [](Object? key) => nodes[key]?.value;
+}
+
+// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
+/// A read-only [List] parsed from YAML.
+class YamlList extends YamlNode with collection.ListMixin {
+  final List<YamlNode> nodes;
+
+  /// The style used for the list in the original document.
+  final CollectionStyle style;
+
+  @override
+  List get value => this;
+
+  @override
+  int get length => nodes.length;
+
+  @override
+  set length(int index) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+
+  /// Creates an empty YamlList.
+  ///
+  /// This list's [span] won't have useful location information. However, it
+  /// will have a reasonable implementation of [SourceSpan.message]. If
+  /// [sourceUrl] is passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  factory YamlList({Object? sourceUrl}) => YamlListWrapper(const [], sourceUrl);
+
+  /// Wraps a Dart list so that it can be accessed (recursively) like a
+  /// [YamlList].
+  ///
+  /// Any [SourceSpan]s returned by this list or its children will be dummies
+  /// without useful location information. However, they will have a reasonable
+  /// implementation of [SourceSpan.message]. If [sourceUrl] is
+  /// passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  factory YamlList.wrap(List dartList,
+          {Object? sourceUrl, CollectionStyle style = CollectionStyle.ANY}) =>
+      YamlListWrapper(dartList, sourceUrl, style: style);
+
+  /// Users of the library should not use this constructor.
+  YamlList.internal(List<YamlNode> nodes, super.span, this.style)
+      : nodes = UnmodifiableListView<YamlNode>(nodes),
+        super._();
+
+  @override
+  dynamic operator [](int index) => nodes[index].value;
+
+  @override
+  void operator []=(int index, Object? value) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+}
+
+/// A wrapped scalar value parsed from YAML.
+class YamlScalar extends YamlNode {
+  @override
+  final dynamic value;
+
+  /// The style used for the scalar in the original document.
+  final ScalarStyle style;
+
+  /// Wraps a Dart value in a [YamlScalar].
+  ///
+  /// This scalar's [span] won't have useful location information. However, it
+  /// will have a reasonable implementation of [SourceSpan.message]. If
+  /// [sourceUrl] is passed, it's used as the [SourceSpan.sourceUrl].
+  ///
+  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+  YamlScalar.wrap(this.value, {Object? sourceUrl, this.style = ScalarStyle.ANY})
+      : super._(NullSpan(sourceUrl)) {
+    ArgumentError.checkNotNull(style, 'style');
+  }
+
+  /// Users of the library should not use this constructor.
+  YamlScalar.internal(this.value, ScalarEvent scalar)
+      : style = scalar.style,
+        super._(scalar.span);
+
+  /// Users of the library should not use this constructor.
+  YamlScalar.internalWithSpan(this.value, SourceSpan span)
+      : style = ScalarStyle.ANY,
+        super._(span);
+
+  @override
+  String toString() => value.toString();
+}
+
+/// Sets the source span of a [YamlNode].
+///
+/// This method is not exposed publicly.
+void setSpan(YamlNode node, SourceSpan span) {
+  node._span = span;
+}
diff --git a/pkgs/yaml/lib/src/yaml_node_wrapper.dart b/pkgs/yaml/lib/src/yaml_node_wrapper.dart
new file mode 100644
index 0000000..5250844
--- /dev/null
+++ b/pkgs/yaml/lib/src/yaml_node_wrapper.dart
@@ -0,0 +1,189 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart' as pkg_collection;
+import 'package:source_span/source_span.dart';
+
+import 'null_span.dart';
+import 'style.dart';
+import 'yaml_node.dart';
+
+/// A wrapper that makes a normal Dart map behave like a [YamlMap].
+class YamlMapWrapper extends MapBase
+    with pkg_collection.UnmodifiableMapMixin
+    implements YamlMap {
+  @override
+  final CollectionStyle style;
+
+  final Map _dartMap;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  final Map<dynamic, YamlNode> nodes;
+
+  @override
+  Map get value => this;
+
+  @override
+  Iterable get keys => _dartMap.keys;
+
+  YamlMapWrapper(Map dartMap, Object? sourceUrl,
+      {CollectionStyle style = CollectionStyle.ANY})
+      : this._(dartMap, NullSpan(sourceUrl), style: style);
+
+  YamlMapWrapper._(Map dartMap, this.span, {this.style = CollectionStyle.ANY})
+      : _dartMap = dartMap,
+        nodes = _YamlMapNodes(dartMap, span) {
+    ArgumentError.checkNotNull(style, 'style');
+  }
+
+  @override
+  dynamic operator [](Object? key) {
+    var value = _dartMap[key];
+    if (value is Map) return YamlMapWrapper._(value, span);
+    if (value is List) return YamlListWrapper._(value, span);
+    return value;
+  }
+
+  @override
+  int get hashCode => _dartMap.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      other is YamlMapWrapper && other._dartMap == _dartMap;
+}
+
+/// The implementation of [YamlMapWrapper.nodes] as a wrapper around the Dart
+/// map.
+class _YamlMapNodes extends MapBase<dynamic, YamlNode>
+    with pkg_collection.UnmodifiableMapMixin<dynamic, YamlNode> {
+  final Map _dartMap;
+
+  final SourceSpan _span;
+
+  @override
+  Iterable get keys =>
+      _dartMap.keys.map((key) => YamlScalar.internalWithSpan(key, _span));
+
+  _YamlMapNodes(this._dartMap, this._span);
+
+  @override
+  YamlNode? operator [](Object? key) {
+    // Use "as" here because key being assigned to invalidates type propagation.
+    if (key is YamlScalar) key = key.value;
+    if (!_dartMap.containsKey(key)) return null;
+    return _nodeForValue(_dartMap[key], _span);
+  }
+
+  @override
+  int get hashCode => _dartMap.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      other is _YamlMapNodes && other._dartMap == _dartMap;
+}
+
+// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
+/// A wrapper that makes a normal Dart list behave like a [YamlList].
+class YamlListWrapper extends ListBase implements YamlList {
+  @override
+  final CollectionStyle style;
+
+  final List _dartList;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  final List<YamlNode> nodes;
+
+  @override
+  List get value => this;
+
+  @override
+  int get length => _dartList.length;
+
+  @override
+  set length(int index) {
+    throw UnsupportedError('Cannot modify an unmodifiable List.');
+  }
+
+  YamlListWrapper(List dartList, Object? sourceUrl,
+      {CollectionStyle style = CollectionStyle.ANY})
+      : this._(dartList, NullSpan(sourceUrl), style: style);
+
+  YamlListWrapper._(List dartList, this.span,
+      {this.style = CollectionStyle.ANY})
+      : _dartList = dartList,
+        nodes = _YamlListNodes(dartList, span) {
+    ArgumentError.checkNotNull(style, 'style');
+  }
+
+  @override
+  dynamic operator [](int index) {
+    var value = _dartList[index];
+    if (value is Map) return YamlMapWrapper._(value, span);
+    if (value is List) return YamlListWrapper._(value, span);
+    return value;
+  }
+
+  @override
+  void operator []=(int index, Object? value) {
+    throw UnsupportedError('Cannot modify an unmodifiable List.');
+  }
+
+  @override
+  int get hashCode => _dartList.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      other is YamlListWrapper && other._dartList == _dartList;
+}
+
+// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
+/// The implementation of [YamlListWrapper.nodes] as a wrapper around the Dart
+/// list.
+class _YamlListNodes extends ListBase<YamlNode> {
+  final List _dartList;
+
+  final SourceSpan _span;
+
+  @override
+  int get length => _dartList.length;
+
+  @override
+  set length(int index) {
+    throw UnsupportedError('Cannot modify an unmodifiable List.');
+  }
+
+  _YamlListNodes(this._dartList, this._span);
+
+  @override
+  YamlNode operator [](int index) => _nodeForValue(_dartList[index], _span);
+
+  @override
+  void operator []=(int index, Object? value) {
+    throw UnsupportedError('Cannot modify an unmodifiable List.');
+  }
+
+  @override
+  int get hashCode => _dartList.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      other is _YamlListNodes && other._dartList == _dartList;
+}
+
+YamlNode _nodeForValue(Object? value, SourceSpan span) {
+  if (value is Map) return YamlMapWrapper._(value, span);
+  if (value is List) return YamlListWrapper._(value, span);
+  return YamlScalar.internalWithSpan(value, span);
+}
diff --git a/pkgs/yaml/lib/yaml.dart b/pkgs/yaml/lib/yaml.dart
new file mode 100644
index 0000000..26cc9b8
--- /dev/null
+++ b/pkgs/yaml/lib/yaml.dart
@@ -0,0 +1,126 @@
+// Copyright (c) 2012, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'src/error_listener.dart';
+import 'src/loader.dart';
+import 'src/style.dart';
+import 'src/yaml_document.dart';
+import 'src/yaml_exception.dart';
+import 'src/yaml_node.dart';
+
+export 'src/style.dart';
+export 'src/utils.dart' show YamlWarningCallback, yamlWarningCallback;
+export 'src/yaml_document.dart';
+export 'src/yaml_exception.dart';
+export 'src/yaml_node.dart' hide setSpan;
+
+/// Loads a single document from a YAML string.
+///
+/// If the string contains more than one document, this throws a
+/// [YamlException]. In future releases, this will become an [ArgumentError].
+///
+/// The return value is mostly normal Dart objects. However, since YAML mappings
+/// support some key types that the default Dart map implementation doesn't
+/// (NaN, lists, and maps), all maps in the returned document are [YamlMap]s.
+/// These have a few small behavioral differences from the default Map
+/// implementation; for details, see the [YamlMap] class.
+///
+/// If [sourceUrl] is passed, it's used as the URL from which the YAML
+/// originated for error reporting.
+///
+/// If [recover] is true, will attempt to recover from parse errors and may
+/// return invalid or synthetic nodes. If [errorListener] is also supplied, its
+/// onError method will be called for each error recovered from. It is not valid
+/// to provide [errorListener] if [recover] is false.
+dynamic loadYaml(String yaml,
+        {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) =>
+    loadYamlNode(yaml,
+            sourceUrl: sourceUrl,
+            recover: recover,
+            errorListener: errorListener)
+        .value;
+
+/// Loads a single document from a YAML string as a [YamlNode].
+///
+/// This is just like [loadYaml], except that where [loadYaml] would return a
+/// normal Dart value this returns a [YamlNode] instead. This allows the caller
+/// to be confident that the return value will always be a [YamlNode].
+YamlNode loadYamlNode(String yaml,
+        {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) =>
+    loadYamlDocument(yaml,
+            sourceUrl: sourceUrl,
+            recover: recover,
+            errorListener: errorListener)
+        .contents;
+
+/// Loads a single document from a YAML string as a [YamlDocument].
+///
+/// This is just like [loadYaml], except that where [loadYaml] would return a
+/// normal Dart value this returns a [YamlDocument] instead. This allows the
+/// caller to access document metadata.
+YamlDocument loadYamlDocument(String yaml,
+    {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) {
+  var loader = Loader(yaml,
+      sourceUrl: sourceUrl, recover: recover, errorListener: errorListener);
+  var document = loader.load();
+  if (document == null) {
+    return YamlDocument.internal(YamlScalar.internalWithSpan(null, loader.span),
+        loader.span, null, const []);
+  }
+
+  var nextDocument = loader.load();
+  if (nextDocument != null) {
+    throw YamlException('Only expected one document.', nextDocument.span);
+  }
+
+  return document;
+}
+
+/// Loads a stream of documents from a YAML string.
+///
+/// The return value is mostly normal Dart objects. However, since YAML mappings
+/// support some key types that the default Dart map implementation doesn't
+/// (NaN, lists, and maps), all maps in the returned document are [YamlMap]s.
+/// These have a few small behavioral differences from the default Map
+/// implementation; for details, see the [YamlMap] class.
+///
+/// If [sourceUrl] is passed, it's used as the URL from which the YAML
+/// originated for error reporting.
+YamlList loadYamlStream(String yaml, {Uri? sourceUrl}) {
+  var loader = Loader(yaml, sourceUrl: sourceUrl);
+
+  var documents = <YamlDocument>[];
+  var document = loader.load();
+  while (document != null) {
+    documents.add(document);
+    document = loader.load();
+  }
+
+  // TODO(jmesserly): the type on the `document` parameter is a workaround for:
+  // https://github.com/dart-lang/dev_compiler/issues/203
+  return YamlList.internal(
+      documents.map((YamlDocument document) => document.contents).toList(),
+      loader.span,
+      CollectionStyle.ANY);
+}
+
+/// Loads a stream of documents from a YAML string.
+///
+/// This is like [loadYamlStream], except that it returns [YamlDocument]s with
+/// metadata wrapping the document contents.
+List<YamlDocument> loadYamlDocuments(String yaml, {Uri? sourceUrl}) {
+  var loader = Loader(yaml, sourceUrl: sourceUrl);
+
+  var documents = <YamlDocument>[];
+  var document = loader.load();
+  while (document != null) {
+    documents.add(document);
+    document = loader.load();
+  }
+
+  return documents;
+}
diff --git a/pkgs/yaml/pubspec.yaml b/pkgs/yaml/pubspec.yaml
new file mode 100644
index 0000000..fb37436
--- /dev/null
+++ b/pkgs/yaml/pubspec.yaml
@@ -0,0 +1,21 @@
+name: yaml
+version: 3.1.3
+description: A parser for YAML, a human-friendly data serialization standard
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/yaml
+
+topics:
+ - yaml
+ - config-format
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  collection: ^1.15.0
+  source_span: ^1.8.0
+  string_scanner: ^1.2.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  path: ^1.8.0
+  test: ^1.16.6
diff --git a/pkgs/yaml/test/span_test.dart b/pkgs/yaml/test/span_test.dart
new file mode 100644
index 0000000..03b7f9c
--- /dev/null
+++ b/pkgs/yaml/test/span_test.dart
@@ -0,0 +1,173 @@
+// Copyright (c) 2019, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'dart:convert';
+
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+void _expectSpan(SourceSpan source, String expected) {
+  final result = source.message('message');
+  printOnFailure("r'''\n$result'''");
+
+  expect(result, expected);
+}
+
+void main() {
+  late YamlMap yaml;
+
+  setUpAll(() {
+    yaml = loadYaml(const JsonEncoder.withIndent(' ').convert({
+      'num': 42,
+      'nested': {
+        'null': null,
+        'num': 42,
+      },
+      'null': null,
+    })) as YamlMap;
+  });
+
+  test('first root key', () {
+    _expectSpan(
+      yaml.nodes['num']!.span,
+      r'''
+line 2, column 9: message
+  ╷
+2 │  "num": 42,
+  │         ^^
+  ╵''',
+    );
+  });
+
+  test('first root key', () {
+    _expectSpan(
+      yaml.nodes['null']!.span,
+      r'''
+line 7, column 10: message
+  ╷
+7 │  "null": null
+  │          ^^^^
+  ╵''',
+    );
+  });
+
+  group('nested', () {
+    late YamlMap nestedMap;
+
+    setUpAll(() {
+      nestedMap = yaml.nodes['nested'] as YamlMap;
+    });
+
+    test('first root key', () {
+      _expectSpan(
+        nestedMap.nodes['null']!.span,
+        r'''
+line 4, column 11: message
+  ╷
+4 │   "null": null,
+  │           ^^^^
+  ╵''',
+      );
+    });
+
+    test('first root key', () {
+      _expectSpan(
+        nestedMap.nodes['num']!.span,
+        r'''
+line 5, column 10: message
+  ╷
+5 │     "num": 42
+  │ ┌──────────^
+6 │ │  },
+  │ └─^
+  ╵''',
+      );
+    });
+  });
+
+  group('block', () {
+    late YamlList list, nestedList;
+
+    setUpAll(() {
+      const yamlStr = '''
+- foo
+- 
+  - one
+  - 
+  - three
+  - 
+  - five
+  -
+- 
+  a : b
+  c : d
+- bar
+''';
+
+      list = loadYaml(yamlStr) as YamlList;
+      nestedList = list.nodes[1] as YamlList;
+    });
+
+    test('root nodes span', () {
+      _expectSpan(list.nodes[0].span, r'''
+line 1, column 3: message
+  ╷
+1 │ - foo
+  │   ^^^
+  ╵''');
+
+      _expectSpan(list.nodes[1].span, r'''
+line 3, column 3: message
+  ╷
+3 │ ┌   - one
+4 │ │   - 
+5 │ │   - three
+6 │ │   - 
+7 │ │   - five
+8 │ └   -
+  ╵''');
+
+      _expectSpan(list.nodes[2].span, r'''
+line 10, column 3: message
+   ╷
+10 │ ┌   a : b
+11 │ └   c : d
+   ╵''');
+
+      _expectSpan(list.nodes[3].span, r'''
+line 12, column 3: message
+   ╷
+12 │ - bar
+   │   ^^^
+   ╵''');
+    });
+
+    test('null nodes span', () {
+      _expectSpan(nestedList.nodes[1].span, r'''
+line 4, column 3: message
+  ╷
+4 │   - 
+  │   ^
+  ╵''');
+
+      _expectSpan(nestedList.nodes[3].span, r'''
+line 6, column 3: message
+  ╷
+6 │   - 
+  │   ^
+  ╵''');
+
+      _expectSpan(nestedList.nodes[5].span, r'''
+line 8, column 3: message
+  ╷
+8 │   -
+  │   ^
+  ╵''');
+    });
+  });
+}
diff --git a/pkgs/yaml/test/utils.dart b/pkgs/yaml/test/utils.dart
new file mode 100644
index 0000000..372440a
--- /dev/null
+++ b/pkgs/yaml/test/utils.dart
@@ -0,0 +1,95 @@
+// Copyright (c) 2014, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+import 'package:test/test.dart';
+import 'package:yaml/src/equality.dart' as equality;
+import 'package:yaml/yaml.dart';
+
+/// A matcher that validates that a closure or Future throws a [YamlException].
+final Matcher throwsYamlException = throwsA(isA<YamlException>());
+
+/// Returns a matcher that asserts that the value equals [expected].
+///
+/// This handles recursive loops and considers `NaN` to equal itself.
+Matcher deepEquals(Object? expected) => predicate(
+    (actual) => equality.deepEquals(actual, expected), 'equals $expected');
+
+/// Constructs a new yaml.YamlMap, optionally from a normal Map.
+Map deepEqualsMap([Map? from]) {
+  var map = equality.deepEqualsMap<Object?, Object?>();
+  if (from != null) map.addAll(from);
+  return map;
+}
+
+/// Asserts that an error has the given message and starts at the given line/col.
+void expectErrorAtLineCol(
+    YamlException error, String message, int line, int col) {
+  expect(error.message, equals(message));
+  expect(error.span!.start.line, equals(line));
+  expect(error.span!.start.column, equals(col));
+}
+
+/// Asserts that a string containing a single YAML document produces a given
+/// value when loaded.
+void expectYamlLoads(Object? expected, String source) {
+  var actual = loadYaml(cleanUpLiteral(source));
+  expect(actual, deepEquals(expected));
+}
+
+/// Asserts that a string containing a stream of YAML documents produces a given
+/// list of values when loaded.
+void expectYamlStreamLoads(List expected, String source) {
+  var actual = loadYamlStream(cleanUpLiteral(source));
+  expect(actual, deepEquals(expected));
+}
+
+/// Asserts that a string containing a single YAML document throws a
+/// [YamlException].
+void expectYamlFails(String source) {
+  expect(() => loadYaml(cleanUpLiteral(source)), throwsYamlException);
+}
+
+/// Removes eight spaces of leading indentation from a multiline string.
+///
+/// Note that this is very sensitive to how the literals are styled. They should
+/// be:
+///     '''
+///     Text starts on own line. Lines up with subsequent lines.
+///     Lines are indented exactly 8 characters from the left margin.
+///     Close is on the same line.'''
+///
+/// This does nothing if text is only a single line.
+String cleanUpLiteral(String text) {
+  var lines = text.split('\n');
+  if (lines.length <= 1) return text;
+
+  for (var j = 0; j < lines.length; j++) {
+    if (lines[j].length > 8) {
+      lines[j] = lines[j].substring(8, lines[j].length);
+    } else {
+      lines[j] = '';
+    }
+  }
+
+  return lines.join('\n');
+}
+
+/// Indents each line of [text] so that, when passed to [cleanUpLiteral], it
+/// will produce output identical to [text].
+///
+/// This is useful for literals that need to include newlines but can't be
+/// conveniently represented as multi-line strings.
+String indentLiteral(String text) {
+  var lines = text.split('\n');
+  if (lines.length <= 1) return text;
+
+  for (var i = 0; i < lines.length; i++) {
+    lines[i] = '        ${lines[i]}';
+  }
+
+  return lines.join('\n');
+}
diff --git a/pkgs/yaml/test/yaml_node_wrapper_test.dart b/pkgs/yaml/test/yaml_node_wrapper_test.dart
new file mode 100644
index 0000000..637b778
--- /dev/null
+++ b/pkgs/yaml/test/yaml_node_wrapper_test.dart
@@ -0,0 +1,235 @@
+// Copyright (c) 2012, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: avoid_dynamic_calls
+
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+void main() {
+  test('YamlMap() with no sourceUrl', () {
+    var map = YamlMap();
+    expect(map, isEmpty);
+    expect(map.nodes, isEmpty);
+    expect(map.span, isNullSpan(isNull));
+  });
+
+  test('YamlMap() with a sourceUrl', () {
+    var map = YamlMap(sourceUrl: 'source');
+    expect(map.span, isNullSpan(Uri.parse('source')));
+  });
+
+  test('YamlList() with no sourceUrl', () {
+    var list = YamlList();
+    expect(list, isEmpty);
+    expect(list.nodes, isEmpty);
+    expect(list.span, isNullSpan(isNull));
+  });
+
+  test('YamlList() with a sourceUrl', () {
+    var list = YamlList(sourceUrl: 'source');
+    expect(list.span, isNullSpan(Uri.parse('source')));
+  });
+
+  test('YamlMap.wrap() with no sourceUrl', () {
+    var map = YamlMap.wrap({
+      'list': [1, 2, 3],
+      'map': {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'scalar': 'value'
+    });
+
+    expect(
+        map,
+        equals({
+          'list': [1, 2, 3],
+          'map': {
+            'foo': 'bar',
+            'nested': [4, 5, 6]
+          },
+          'scalar': 'value'
+        }));
+
+    expect(map.span, isNullSpan(isNull));
+    expect(map['list'], isA<YamlList>());
+    expect(map['list'].nodes[0], isA<YamlScalar>());
+    expect(map['list'].span, isNullSpan(isNull));
+    expect(map['map'], isA<YamlMap>());
+    expect(map['map'].nodes['foo'], isA<YamlScalar>());
+    expect(map['map']['nested'], isA<YamlList>());
+    expect(map['map'].span, isNullSpan(isNull));
+    expect(map.nodes['scalar'], isA<YamlScalar>());
+    expect(map.nodes['scalar']!.value, 'value');
+    expect(map.nodes['scalar']!.span, isNullSpan(isNull));
+    expect(map['scalar'], 'value');
+    expect(map.keys, unorderedEquals(['list', 'map', 'scalar']));
+    expect(map.nodes.keys, everyElement(isA<YamlScalar>()));
+    expect(map.nodes[YamlScalar.wrap('list')], equals([1, 2, 3]));
+    expect(map.style, equals(CollectionStyle.ANY));
+    expect((map.nodes['list'] as YamlList).style, equals(CollectionStyle.ANY));
+    expect((map.nodes['map'] as YamlMap).style, equals(CollectionStyle.ANY));
+    expect((map['map'].nodes['nested'] as YamlList).style,
+        equals(CollectionStyle.ANY));
+  });
+
+  test('YamlMap.wrap() with a sourceUrl', () {
+    var map = YamlMap.wrap({
+      'list': [1, 2, 3],
+      'map': {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'scalar': 'value'
+    }, sourceUrl: 'source');
+
+    var source = Uri.parse('source');
+    expect(map.span, isNullSpan(source));
+    expect(map['list'].span, isNullSpan(source));
+    expect(map['map'].span, isNullSpan(source));
+    expect(map.nodes['scalar']!.span, isNullSpan(source));
+  });
+
+  test('YamlMap.wrap() with a sourceUrl and style', () {
+    var map = YamlMap.wrap({
+      'list': [1, 2, 3],
+      'map': {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'scalar': 'value'
+    }, sourceUrl: 'source', style: CollectionStyle.BLOCK);
+
+    expect(map.style, equals(CollectionStyle.BLOCK));
+    expect((map.nodes['list'] as YamlList).style, equals(CollectionStyle.ANY));
+    expect((map.nodes['map'] as YamlMap).style, equals(CollectionStyle.ANY));
+    expect((map['map'].nodes['nested'] as YamlList).style,
+        equals(CollectionStyle.ANY));
+  });
+
+  test('YamlList.wrap() with no sourceUrl', () {
+    var list = YamlList.wrap([
+      [1, 2, 3],
+      {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'value'
+    ]);
+
+    expect(
+        list,
+        equals([
+          [1, 2, 3],
+          {
+            'foo': 'bar',
+            'nested': [4, 5, 6]
+          },
+          'value'
+        ]));
+
+    expect(list.span, isNullSpan(isNull));
+    expect(list[0], isA<YamlList>());
+    expect(list[0].nodes[0], isA<YamlScalar>());
+    expect(list[0].span, isNullSpan(isNull));
+    expect(list[1], isA<YamlMap>());
+    expect(list[1].nodes['foo'], isA<YamlScalar>());
+    expect(list[1]['nested'], isA<YamlList>());
+    expect(list[1].span, isNullSpan(isNull));
+    expect(list.nodes[2], isA<YamlScalar>());
+    expect(list.nodes[2].value, 'value');
+    expect(list.nodes[2].span, isNullSpan(isNull));
+    expect(list[2], 'value');
+    expect(list.style, equals(CollectionStyle.ANY));
+    expect((list[0] as YamlList).style, equals(CollectionStyle.ANY));
+    expect((list[1] as YamlMap).style, equals(CollectionStyle.ANY));
+    expect((list[1]['nested'] as YamlList).style, equals(CollectionStyle.ANY));
+  });
+
+  test('YamlList.wrap() with a sourceUrl', () {
+    var list = YamlList.wrap([
+      [1, 2, 3],
+      {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'value'
+    ], sourceUrl: 'source');
+
+    var source = Uri.parse('source');
+    expect(list.span, isNullSpan(source));
+    expect(list[0].span, isNullSpan(source));
+    expect(list[1].span, isNullSpan(source));
+    expect(list.nodes[2].span, isNullSpan(source));
+  });
+
+  test('YamlList.wrap() with a sourceUrl and style', () {
+    var list = YamlList.wrap([
+      [1, 2, 3],
+      {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+      'value'
+    ], sourceUrl: 'source', style: CollectionStyle.FLOW);
+
+    expect(list.style, equals(CollectionStyle.FLOW));
+    expect((list[0] as YamlList).style, equals(CollectionStyle.ANY));
+    expect((list[1] as YamlMap).style, equals(CollectionStyle.ANY));
+    expect((list[1]['nested'] as YamlList).style, equals(CollectionStyle.ANY));
+  });
+
+  test('re-wrapped objects equal one another', () {
+    var list = YamlList.wrap([
+      [1, 2, 3],
+      {'foo': 'bar'}
+    ]);
+
+    expect(list[0] == list[0], isTrue);
+    expect(list[0].nodes == list[0].nodes, isTrue);
+    expect(list[0] == YamlList.wrap([1, 2, 3]), isFalse);
+    expect(list[1] == list[1], isTrue);
+    expect(list[1].nodes == list[1].nodes, isTrue);
+    expect(list[1] == YamlMap.wrap({'foo': 'bar'}), isFalse);
+  });
+
+  test('YamlScalar.wrap() with no sourceUrl', () {
+    var scalar = YamlScalar.wrap('foo');
+
+    expect(scalar.span, isNullSpan(isNull));
+    expect(scalar.value, 'foo');
+    expect(scalar.style, equals(ScalarStyle.ANY));
+  });
+
+  test('YamlScalar.wrap() with sourceUrl', () {
+    var scalar = YamlScalar.wrap('foo', sourceUrl: 'source');
+
+    var source = Uri.parse('source');
+    expect(scalar.span, isNullSpan(source));
+  });
+
+  test('YamlScalar.wrap() with sourceUrl and style', () {
+    var scalar = YamlScalar.wrap('foo',
+        sourceUrl: 'source', style: ScalarStyle.DOUBLE_QUOTED);
+
+    expect(scalar.style, equals(ScalarStyle.DOUBLE_QUOTED));
+  });
+}
+
+Matcher isNullSpan(Object sourceUrl) => predicate((SourceSpan span) {
+      expect(span, isA<SourceSpan>());
+      expect(span.length, equals(0));
+      expect(span.text, isEmpty);
+      expect(span.start, equals(span.end));
+      expect(span.start.offset, equals(0));
+      expect(span.start.line, equals(0));
+      expect(span.start.column, equals(0));
+      expect(span.sourceUrl, sourceUrl);
+      return true;
+    });
diff --git a/pkgs/yaml/test/yaml_test.dart b/pkgs/yaml/test/yaml_test.dart
new file mode 100644
index 0000000..3b5b77d
--- /dev/null
+++ b/pkgs/yaml/test/yaml_test.dart
@@ -0,0 +1,1921 @@
+// Copyright (c) 2012, the Dart project authors.
+// Copyright (c) 2006, Kirill Simonov.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file or at
+// https://opensource.org/licenses/MIT.
+
+// ignore_for_file: avoid_dynamic_calls
+
+import 'package:test/test.dart';
+import 'package:yaml/src/error_listener.dart';
+import 'package:yaml/yaml.dart';
+
+import 'utils.dart';
+
+void main() {
+  var infinity = double.parse('Infinity');
+  var nan = double.parse('NaN');
+
+  group('has a friendly error message for', () {
+    var tabError = predicate((e) =>
+        e.toString().contains('Tab characters are not allowed as indentation'));
+
+    test('using a tab as indentation', () {
+      expect(() => loadYaml('foo:\n\tbar'), throwsA(tabError));
+    });
+
+    test('using a tab not as indentation', () {
+      expect(() => loadYaml('''
+          "foo
+          \tbar"
+          error'''), throwsA(isNot(tabError)));
+    });
+  });
+
+  group('refuses', () {
+    // Regression test for #19.
+    test('invalid contents', () {
+      expectYamlFails('{');
+    });
+
+    test('duplicate mapping keys', () {
+      expectYamlFails('{a: 1, a: 2}');
+    });
+
+    group('documents that declare version', () {
+      test('1.0', () {
+        expectYamlFails('''
+         %YAML 1.0
+         --- text
+         ''');
+      });
+
+      test('1.3', () {
+        expectYamlFails('''
+           %YAML 1.3
+           --- text
+           ''');
+      });
+
+      test('2.0', () {
+        expectYamlFails('''
+           %YAML 2.0
+           --- text
+           ''');
+      });
+    });
+  });
+
+  group('recovers', () {
+    var collector = ErrorCollector();
+    setUp(() {
+      collector = ErrorCollector();
+    });
+
+    test('from incomplete leading keys', () {
+      final yaml = cleanUpLiteral(r'''
+        dependencies:
+          zero
+          one: any
+          ''');
+      var result = loadYaml(yaml, recover: true, errorListener: collector);
+      expect(
+          result,
+          deepEquals({
+            'dependencies': {
+              'zero': null,
+              'one': 'any',
+            }
+          }));
+      expect(collector.errors.length, equals(1));
+      // These errors are reported at the start of the next token (after the
+      // whitespace/newlines).
+      expectErrorAtLineCol(collector.errors[0], "Expected ':'.", 2, 2);
+      // Skipped because this case is not currently handled. If it's the first
+      // package without the colon, because the value is indented from the line
+      // above, the whole `zero\n     one` is treated as a scalar value.
+    }, skip: true);
+    test('from incomplete keys', () {
+      final yaml = cleanUpLiteral(r'''
+        dependencies:
+          one: any
+          two
+          three:
+          four
+          five:
+            1.2.3
+          six: 5.4.3
+          ''');
+      var result = loadYaml(yaml, recover: true, errorListener: collector);
+      expect(
+          result,
+          deepEquals({
+            'dependencies': {
+              'one': 'any',
+              'two': null,
+              'three': null,
+              'four': null,
+              'five': '1.2.3',
+              'six': '5.4.3',
+            }
+          }));
+
+      expect(collector.errors.length, equals(2));
+      // These errors are reported at the start of the next token (after the
+      // whitespace/newlines).
+      expectErrorAtLineCol(collector.errors[0], "Expected ':'.", 3, 2);
+      expectErrorAtLineCol(collector.errors[1], "Expected ':'.", 5, 2);
+    });
+    test('from incomplete trailing keys', () {
+      final yaml = cleanUpLiteral(r'''
+        dependencies:
+          six: 5.4.3
+          seven
+          ''');
+      var result = loadYaml(yaml, recover: true);
+      expect(
+          result,
+          deepEquals({
+            'dependencies': {
+              'six': '5.4.3',
+              'seven': null,
+            }
+          }));
+    });
+  });
+
+  test('includes source span information', () {
+    var yaml = loadYamlNode(r'''
+- foo:
+    bar
+- 123
+''') as YamlList;
+
+    expect(yaml.span.start.line, equals(0));
+    expect(yaml.span.start.column, equals(0));
+    expect(yaml.span.end.line, equals(3));
+    expect(yaml.span.end.column, equals(0));
+
+    var map = yaml.nodes.first as YamlMap;
+    expect(map.span.start.line, equals(0));
+    expect(map.span.start.column, equals(2));
+    expect(map.span.end.line, equals(2));
+    expect(map.span.end.column, equals(0));
+
+    var key = map.nodes.keys.first;
+    expect(key.span.start.line, equals(0));
+    expect(key.span.start.column, equals(2));
+    expect(key.span.end.line, equals(0));
+    expect(key.span.end.column, equals(5));
+
+    var value = map.nodes.values.first;
+    expect(value.span.start.line, equals(1));
+    expect(value.span.start.column, equals(4));
+    expect(value.span.end.line, equals(1));
+    expect(value.span.end.column, equals(7));
+
+    var scalar = yaml.nodes.last;
+    expect(scalar.span.start.line, equals(2));
+    expect(scalar.span.start.column, equals(2));
+    expect(scalar.span.end.line, equals(2));
+    expect(scalar.span.end.column, equals(5));
+  });
+
+  // The following tests are all taken directly from the YAML spec
+  // (http://www.yaml.org/spec/1.2/spec.html). Most of them are code examples
+  // that are directly included in the spec, but additional tests are derived
+  // from the prose.
+
+  // A few examples from the spec are deliberately excluded, because they test
+  // features that this implementation doesn't intend to support (character
+  // encoding detection and user-defined tags). More tests are commented out,
+  // because they're intended to be supported but not yet implemented.
+
+  // Chapter 2 is just a preview of various Yaml documents. It's probably not
+  // necessary to test its examples, but it would be nice to test everything in
+  // the spec.
+  group('2.1: Collections', () {
+    test('[Example 2.1]', () {
+      expectYamlLoads(['Mark McGwire', 'Sammy Sosa', 'Ken Griffey'], '''
+        - Mark McGwire
+        - Sammy Sosa
+        - Ken Griffey''');
+    });
+
+    test('[Example 2.2]', () {
+      expectYamlLoads({'hr': 65, 'avg': 0.278, 'rbi': 147}, '''
+        hr:  65    # Home runs
+        avg: 0.278 # Batting average
+        rbi: 147   # Runs Batted In''');
+    });
+
+    test('[Example 2.3]', () {
+      expectYamlLoads({
+        'american': ['Boston Red Sox', 'Detroit Tigers', 'New York Yankees'],
+        'national': ['New York Mets', 'Chicago Cubs', 'Atlanta Braves'],
+      }, '''
+        american:
+          - Boston Red Sox
+          - Detroit Tigers
+          - New York Yankees
+        national:
+          - New York Mets
+          - Chicago Cubs
+          - Atlanta Braves''');
+    });
+
+    test('[Example 2.4]', () {
+      expectYamlLoads([
+        {'name': 'Mark McGwire', 'hr': 65, 'avg': 0.278},
+        {'name': 'Sammy Sosa', 'hr': 63, 'avg': 0.288},
+      ], '''
+        -
+          name: Mark McGwire
+          hr:   65
+          avg:  0.278
+        -
+          name: Sammy Sosa
+          hr:   63
+          avg:  0.288''');
+    });
+
+    test('[Example 2.5]', () {
+      expectYamlLoads([
+        ['name', 'hr', 'avg'],
+        ['Mark McGwire', 65, 0.278],
+        ['Sammy Sosa', 63, 0.288]
+      ], '''
+        - [name        , hr, avg  ]
+        - [Mark McGwire, 65, 0.278]
+        - [Sammy Sosa  , 63, 0.288]''');
+    });
+
+    test('[Example 2.6]', () {
+      expectYamlLoads({
+        'Mark McGwire': {'hr': 65, 'avg': 0.278},
+        'Sammy Sosa': {'hr': 63, 'avg': 0.288}
+      }, '''
+        Mark McGwire: {hr: 65, avg: 0.278}
+        Sammy Sosa: {
+            hr: 63,
+            avg: 0.288
+          }''');
+    });
+  });
+
+  group('2.2: Structures', () {
+    test('[Example 2.7]', () {
+      expectYamlStreamLoads([
+        ['Mark McGwire', 'Sammy Sosa', 'Ken Griffey'],
+        ['Chicago Cubs', 'St Louis Cardinals']
+      ], '''
+        # Ranking of 1998 home runs
+        ---
+        - Mark McGwire
+        - Sammy Sosa
+        - Ken Griffey
+
+        # Team ranking
+        ---
+        - Chicago Cubs
+        - St Louis Cardinals''');
+    });
+
+    test('[Example 2.8]', () {
+      expectYamlStreamLoads([
+        {'time': '20:03:20', 'player': 'Sammy Sosa', 'action': 'strike (miss)'},
+        {'time': '20:03:47', 'player': 'Sammy Sosa', 'action': 'grand slam'},
+      ], '''
+        ---
+        time: 20:03:20
+        player: Sammy Sosa
+        action: strike (miss)
+        ...
+        ---
+        time: 20:03:47
+        player: Sammy Sosa
+        action: grand slam
+        ...''');
+    });
+
+    test('[Example 2.9]', () {
+      expectYamlLoads({
+        'hr': ['Mark McGwire', 'Sammy Sosa'],
+        'rbi': ['Sammy Sosa', 'Ken Griffey']
+      }, '''
+        ---
+        hr: # 1998 hr ranking
+          - Mark McGwire
+          - Sammy Sosa
+        rbi:
+          # 1998 rbi ranking
+          - Sammy Sosa
+          - Ken Griffey''');
+    });
+
+    test('[Example 2.10]', () {
+      expectYamlLoads({
+        'hr': ['Mark McGwire', 'Sammy Sosa'],
+        'rbi': ['Sammy Sosa', 'Ken Griffey']
+      }, '''
+        ---
+        hr:
+          - Mark McGwire
+          # Following node labeled SS
+          - &SS Sammy Sosa
+        rbi:
+          - *SS # Subsequent occurrence
+          - Ken Griffey''');
+    });
+
+    test('[Example 2.11]', () {
+      var doc = deepEqualsMap();
+      doc[['Detroit Tigers', 'Chicago cubs']] = ['2001-07-23'];
+      doc[['New York Yankees', 'Atlanta Braves']] = [
+        '2001-07-02',
+        '2001-08-12',
+        '2001-08-14'
+      ];
+      expectYamlLoads(doc, '''
+        ? - Detroit Tigers
+          - Chicago cubs
+        :
+          - 2001-07-23
+
+        ? [ New York Yankees,
+            Atlanta Braves ]
+        : [ 2001-07-02, 2001-08-12,
+            2001-08-14 ]''');
+    });
+
+    test('[Example 2.12]', () {
+      expectYamlLoads([
+        {'item': 'Super Hoop', 'quantity': 1},
+        {'item': 'Basketball', 'quantity': 4},
+        {'item': 'Big Shoes', 'quantity': 1},
+      ], '''
+        ---
+        # Products purchased
+        - item    : Super Hoop
+          quantity: 1
+        - item    : Basketball
+          quantity: 4
+        - item    : Big Shoes
+          quantity: 1''');
+    });
+  });
+
+  group('2.3: Scalars', () {
+    test('[Example 2.13]', () {
+      expectYamlLoads(cleanUpLiteral('''
+        \\//||\\/||
+        // ||  ||__'''), '''
+        # ASCII Art
+        --- |
+          \\//||\\/||
+          // ||  ||__''');
+    });
+
+    test('[Example 2.14]', () {
+      expectYamlLoads("Mark McGwire's year was crippled by a knee injury.", '''
+        --- >
+          Mark McGwire's
+          year was crippled
+          by a knee injury.''');
+    });
+
+    test('[Example 2.15]', () {
+      expectYamlLoads(cleanUpLiteral('''
+        Sammy Sosa completed another fine season with great stats.
+
+          63 Home Runs
+          0.288 Batting Average
+
+        What a year!'''), '''
+        >
+         Sammy Sosa completed another
+         fine season with great stats.
+
+           63 Home Runs
+           0.288 Batting Average
+
+         What a year!''');
+    });
+
+    test('[Example 2.16]', () {
+      expectYamlLoads({
+        'name': 'Mark McGwire',
+        'accomplishment': 'Mark set a major league home run record in 1998.\n',
+        'stats': '65 Home Runs\n0.278 Batting Average'
+      }, '''
+        name: Mark McGwire
+        accomplishment: >
+          Mark set a major league
+          home run record in 1998.
+        stats: |
+          65 Home Runs
+          0.278 Batting Average''');
+    });
+
+    test('[Example 2.17]', () {
+      expectYamlLoads({
+        'unicode': 'Sosa did fine.\u263A \u{1F680}',
+        'control': '\b1998\t1999\t2000\n',
+        'hex esc': '\r\n is \r\n',
+        'single': '"Howdy!" he cried.',
+        'quoted': " # Not a 'comment'.",
+        'tie-fighter': '|\\-*-/|',
+        'surrogate-pair': 'I \u{D83D}\u{DE03}  ️Dart!',
+        'key-\u{D83D}\u{DD11}': 'Look\u{D83D}\u{DE03}\u{D83C}\u{DF89}surprise!',
+      }, """
+        unicode: "Sosa did fine.\\u263A \\U0001F680"
+        control: "\\b1998\\t1999\\t2000\\n"
+        hex esc: "\\x0d\\x0a is \\r\\n"
+
+        single: '"Howdy!" he cried.'
+        quoted: ' # Not a ''comment''.'
+        tie-fighter: '|\\-*-/|'
+        
+        surrogate-pair: I \u{D83D}\u{DE03}  ️Dart!
+        key-\u{D83D}\u{DD11}: Look\u{D83D}\u{DE03}\u{D83C}\u{DF89}surprise!""");
+    });
+
+    test('[Example 2.18]', () {
+      expectYamlLoads({
+        'plain': 'This unquoted scalar spans many lines.',
+        'quoted': 'So does this quoted scalar.\n'
+      }, '''
+        plain:
+          This unquoted scalar
+          spans many lines.
+
+        quoted: "So does this
+          quoted scalar.\\n"''');
+    });
+  });
+
+  group('2.4: Tags', () {
+    test('[Example 2.19]', () {
+      expectYamlLoads({
+        'canonical': 12345,
+        'decimal': 12345,
+        'octal': 12,
+        'hexadecimal': 12
+      }, '''
+        canonical: 12345
+        decimal: +12345
+        octal: 0o14
+        hexadecimal: 0xC''');
+    });
+
+    test('[Example 2.20]', () {
+      expectYamlLoads({
+        'canonical': 1230.15,
+        'exponential': 1230.15,
+        'fixed': 1230.15,
+        'negative infinity': -infinity,
+        'not a number': nan
+      }, '''
+        canonical: 1.23015e+3
+        exponential: 12.3015e+02
+        fixed: 1230.15
+        negative infinity: -.inf
+        not a number: .NaN''');
+    });
+
+    test('[Example 2.21]', () {
+      var doc = deepEqualsMap({
+        'booleans': [true, false],
+        'string': '012345'
+      });
+      doc[null] = null;
+      expectYamlLoads(doc, """
+        null:
+        booleans: [ true, false ]
+        string: '012345'""");
+    });
+
+    // Examples 2.22 through 2.26 test custom tag URIs, which this
+    // implementation currently doesn't plan to support.
+  });
+
+  group('2.5 Full Length Example', () {
+    // Example 2.27 tests custom tag URIs, which this implementation currently
+    // doesn't plan to support.
+
+    test('[Example 2.28]', () {
+      expectYamlStreamLoads([
+        {
+          'Time': '2001-11-23 15:01:42 -5',
+          'User': 'ed',
+          'Warning': 'This is an error message for the log file'
+        },
+        {
+          'Time': '2001-11-23 15:02:31 -5',
+          'User': 'ed',
+          'Warning': 'A slightly different error message.'
+        },
+        {
+          'DateTime': '2001-11-23 15:03:17 -5',
+          'User': 'ed',
+          'Fatal': 'Unknown variable "bar"',
+          'Stack': [
+            {
+              'file': 'TopClass.py',
+              'line': 23,
+              'code': 'x = MoreObject("345\\n")\n'
+            },
+            {'file': 'MoreClass.py', 'line': 58, 'code': 'foo = bar'}
+          ]
+        }
+      ], '''
+        ---
+        Time: 2001-11-23 15:01:42 -5
+        User: ed
+        Warning:
+          This is an error message
+          for the log file
+        ---
+        Time: 2001-11-23 15:02:31 -5
+        User: ed
+        Warning:
+          A slightly different error
+          message.
+        ---
+        DateTime: 2001-11-23 15:03:17 -5
+        User: ed
+        Fatal:
+          Unknown variable "bar"
+        Stack:
+          - file: TopClass.py
+            line: 23
+            code: |
+              x = MoreObject("345\\n")
+          - file: MoreClass.py
+            line: 58
+            code: |-
+              foo = bar''');
+    });
+  });
+
+  // Chapter 3 just talks about the structure of loading and dumping Yaml.
+  // Chapter 4 explains conventions used in the spec.
+
+  // Chapter 5: Characters
+  group('5.1: Character Set', () {
+    void expectAllowsCharacter(int charCode) {
+      var char = String.fromCharCodes([charCode]);
+      expectYamlLoads('The character "$char" is allowed',
+          'The character "$char" is allowed');
+    }
+
+    void expectAllowsQuotedCharacter(int charCode) {
+      var char = String.fromCharCodes([charCode]);
+      expectYamlLoads("The character '$char' is allowed",
+          '"The character \'$char\' is allowed"');
+    }
+
+    void expectDisallowsCharacter(int charCode) {
+      var char = String.fromCharCodes([charCode]);
+      expectYamlFails('The character "$char" is disallowed');
+    }
+
+    test("doesn't include C0 control characters", () {
+      expectDisallowsCharacter(0x0);
+      expectDisallowsCharacter(0x8);
+      expectDisallowsCharacter(0x1F);
+    });
+
+    test('includes TAB', () => expectAllowsCharacter(0x9));
+    test("doesn't include DEL", () => expectDisallowsCharacter(0x7F));
+
+    test("doesn't include C1 control characters", () {
+      expectDisallowsCharacter(0x80);
+      expectDisallowsCharacter(0x8A);
+      expectDisallowsCharacter(0x9F);
+    });
+
+    test('includes NEL', () => expectAllowsCharacter(0x85));
+
+    group('within quoted strings', () {
+      test('includes DEL', () => expectAllowsQuotedCharacter(0x7F));
+      test('includes C1 control characters', () {
+        expectAllowsQuotedCharacter(0x80);
+        expectAllowsQuotedCharacter(0x8A);
+        expectAllowsQuotedCharacter(0x9F);
+      });
+    });
+  });
+
+  // Skipping section 5.2 (Character Encodings), since at the moment the module
+  // assumes that the client code is providing it with a string of the proper
+  // encoding.
+
+  group('5.3: Indicator Characters', () {
+    test('[Example 5.3]', () {
+      expectYamlLoads({
+        'sequence': ['one', 'two'],
+        'mapping': {'sky': 'blue', 'sea': 'green'}
+      }, '''
+        sequence:
+        - one
+        - two
+        mapping:
+          ? sky
+          : blue
+          sea : green''');
+    });
+
+    test('[Example 5.4]', () {
+      expectYamlLoads({
+        'sequence': ['one', 'two'],
+        'mapping': {'sky': 'blue', 'sea': 'green'}
+      }, '''
+        sequence: [ one, two, ]
+        mapping: { sky: blue, sea: green }''');
+    });
+
+    test('[Example 5.5]', () => expectYamlLoads(null, '# Comment only.'));
+
+    // Skipping 5.6 because it uses an undefined tag.
+
+    test('[Example 5.7]', () {
+      expectYamlLoads({'literal': 'some\ntext\n', 'folded': 'some text\n'}, '''
+        literal: |
+          some
+          text
+        folded: >
+          some
+          text
+        ''');
+    });
+
+    test('[Example 5.8]', () {
+      expectYamlLoads({'single': 'text', 'double': 'text'}, '''
+        single: 'text'
+        double: "text"
+        ''');
+    });
+
+    test('[Example 5.9]', () {
+      expectYamlLoads('text', '''
+        %YAML 1.2
+        --- text''');
+    });
+
+    test('[Example 5.10]', () {
+      expectYamlFails('commercial-at: @text');
+      expectYamlFails('commercial-at: `text');
+    });
+  });
+
+  group('5.4: Line Break Characters', () {
+    group('include', () {
+      test('\\n', () => expectYamlLoads([1, 2], indentLiteral('- 1\n- 2')));
+      test('\\r', () => expectYamlLoads([1, 2], '- 1\r- 2'));
+    });
+
+    group('do not include', () {
+      test('form feed', () => expectYamlFails('- 1\x0C- 2'));
+      test('NEL', () => expectYamlLoads(['1\x85- 2'], '- 1\x85- 2'));
+      test('0x2028', () => expectYamlLoads(['1\u2028- 2'], '- 1\u2028- 2'));
+      test('0x2029', () => expectYamlLoads(['1\u2029- 2'], '- 1\u2029- 2'));
+    });
+
+    group('in a scalar context must be normalized', () {
+      test(
+          'from \\r to \\n',
+          () => expectYamlLoads(
+              ['foo\nbar'], indentLiteral('- |\n  foo\r  bar')));
+      test(
+          'from \\r\\n to \\n',
+          () => expectYamlLoads(
+              ['foo\nbar'], indentLiteral('- |\n  foo\r\n  bar')));
+    });
+
+    test('[Example 5.11]', () {
+      expectYamlLoads(cleanUpLiteral('''
+        Line break (no glyph)
+        Line break (glyphed)'''), '''
+        |
+          Line break (no glyph)
+          Line break (glyphed)''');
+    });
+  });
+
+  group('5.5: White Space Characters', () {
+    test('[Example 5.12]', () {
+      expectYamlLoads({
+        'quoted': 'Quoted \t',
+        'block': 'void main() {\n\tprintf("Hello, world!\\n");\n}\n'
+      }, '''
+        # Tabs and spaces
+        quoted: "Quoted \t"
+        block:\t|
+          void main() {
+          \tprintf("Hello, world!\\n");
+          }
+        ''');
+    });
+  });
+
+  group('5.7: Escaped Characters', () {
+    test('[Example 5.13]', () {
+      expectYamlLoads(
+          'Fun with \x5C '
+              '\x22 \x07 \x08 \x1B \x0C '
+              '\x0A \x0D \x09 \x0B \x00 '
+              '\x20 \xA0 \x85 \u2028 \u2029 '
+              'A A A',
+          '''
+        "Fun with \\\\
+        \\" \\a \\b \\e \\f \\
+        \\n \\r \\t \\v \\0 \\
+        \\  \\_ \\N \\L \\P \\
+        \\x41 \\u0041 \\U00000041"''');
+    });
+
+    test('[Example 5.14]', () {
+      expectYamlFails('Bad escape: "\\c"');
+      expectYamlFails('Bad escape: "\\xq-"');
+    });
+  });
+
+  // Chapter 6: Basic Structures
+  group('6.1: Indentation Spaces', () {
+    test('may not include TAB characters', () {
+      expectYamlFails('''
+        -
+        \t- foo
+        \t- bar''');
+    });
+
+    test('must be the same for all sibling nodes', () {
+      expectYamlFails('''
+        -
+          - foo
+         - bar''');
+    });
+
+    test('may be different for the children of sibling nodes', () {
+      expectYamlLoads([
+        ['foo'],
+        ['bar']
+      ], '''
+        -
+          - foo
+        -
+         - bar''');
+    });
+
+    test('[Example 6.1]', () {
+      expectYamlLoads({
+        'Not indented': {
+          'By one space': 'By four\n  spaces\n',
+          'Flow style': ['By two', 'Also by two', 'Still by two']
+        }
+      }, '''
+          # Leading comment line spaces are
+           # neither content nor indentation.
+            
+        Not indented:
+         By one space: |
+            By four
+              spaces
+         Flow style: [    # Leading spaces
+           By two,        # in flow style
+          Also by two,    # are neither
+          \tStill by two   # content nor
+            ]             # indentation.''');
+    });
+
+    test('[Example 6.2]', () {
+      expectYamlLoads({
+        'a': [
+          'b',
+          ['c', 'd']
+        ]
+      }, '''
+        ? a
+        : -\tb
+          -  -\tc
+             - d''');
+    });
+  });
+
+  group('6.2: Separation Spaces', () {
+    test('[Example 6.3]', () {
+      expectYamlLoads([
+        {'foo': 'bar'},
+        ['baz', 'baz']
+      ], '''
+        - foo:\t bar
+        - - baz
+          -\tbaz''');
+    });
+  });
+
+  group('6.3: Line Prefixes', () {
+    test('[Example 6.4]', () {
+      expectYamlLoads({
+        'plain': 'text lines',
+        'quoted': 'text lines',
+        'block': 'text\n \tlines\n'
+      }, '''
+        plain: text
+          lines
+        quoted: "text
+          \tlines"
+        block: |
+          text
+           \tlines
+        ''');
+    });
+  });
+
+  group('6.4: Empty Lines', () {
+    test('[Example 6.5]', () {
+      expectYamlLoads({
+        'Folding': 'Empty line\nas a line feed',
+        'Chomping': 'Clipped empty lines\n',
+      }, '''
+        Folding:
+          "Empty line
+           \t
+          as a line feed"
+        Chomping: |
+          Clipped empty lines
+         ''');
+    });
+  });
+
+  group('6.5: Line Folding', () {
+    test('[Example 6.6]', () {
+      expectYamlLoads('trimmed\n\n\nas space', '''
+        >-
+          trimmed
+          
+         
+
+          as
+          space
+        ''');
+    });
+
+    test('[Example 6.7]', () {
+      expectYamlLoads('foo \n\n\t bar\n\nbaz\n', '''
+        >
+          foo 
+         
+          \t bar
+
+          baz
+        ''');
+    });
+
+    test('[Example 6.8]', () {
+      expectYamlLoads(' foo\nbar\nbaz ', '''
+        "
+          foo 
+         
+          \t bar
+
+          baz
+        "''');
+    });
+  });
+
+  group('6.6: Comments', () {
+    test('must be separated from other tokens by white space characters', () {
+      expectYamlLoads('foo#bar', 'foo#bar');
+      expectYamlLoads('foo:#bar', 'foo:#bar');
+      expectYamlLoads('-#bar', '-#bar');
+    });
+
+    test('[Example 6.9]', () {
+      expectYamlLoads({'key': 'value'}, '''
+        key:    # Comment
+          value''');
+    });
+
+    group('outside of scalar content', () {
+      test('may appear on a line of their own', () {
+        expectYamlLoads([1, 2], '''
+        - 1
+        # Comment
+        - 2''');
+      });
+
+      test('are independent of indentation level', () {
+        expectYamlLoads([
+          [1, 2]
+        ], '''
+        -
+          - 1
+         # Comment
+          - 2''');
+      });
+
+      test('include lines containing only white space characters', () {
+        expectYamlLoads([1, 2], '''
+        - 1
+          \t  
+        - 2''');
+      });
+    });
+
+    group('within scalar content', () {
+      test('may not appear on a line of their own', () {
+        expectYamlLoads(['foo\n# not comment\nbar\n'], '''
+        - |
+          foo
+          # not comment
+          bar
+        ''');
+      });
+
+      test("don't include lines containing only white space characters", () {
+        expectYamlLoads(['foo\n  \t   \nbar\n'], '''
+        - |
+          foo
+            \t   
+          bar
+        ''');
+      });
+    });
+
+    test('[Example 6.10]', () {
+      expectYamlLoads(null, '''
+          # Comment
+           
+        ''');
+    });
+
+    test('[Example 6.11]', () {
+      expectYamlLoads({'key': 'value'}, '''
+        key:    # Comment
+                # lines
+          value
+        ''');
+    });
+
+    group('ending a block scalar header', () {
+      test('may not be followed by additional comment lines', () {
+        expectYamlLoads(['# not comment\nfoo\n'], '''
+        - | # comment
+            # not comment
+            foo
+        ''');
+      });
+    });
+  });
+
+  group('6.7: Separation Lines', () {
+    test('may not be used within implicit keys', () {
+      expectYamlFails('''
+        [1,
+         2]: 3''');
+    });
+
+    test('[Example 6.12]', () {
+      var doc = deepEqualsMap();
+      doc[{'first': 'Sammy', 'last': 'Sosa'}] = {'hr': 65, 'avg': 0.278};
+      expectYamlLoads(doc, '''
+        { first: Sammy, last: Sosa }:
+        # Statistics:
+          hr:  # Home runs
+             65
+          avg: # Average
+           0.278''');
+    });
+  });
+
+  group('6.8: Directives', () {
+    // TODO(nweiz): assert that this produces a warning
+    test('[Example 6.13]', () {
+      expectYamlLoads('foo', '''
+        %FOO  bar baz # Should be ignored
+                      # with a warning.
+        --- "foo"''');
+    });
+
+    // TODO(nweiz): assert that this produces a warning.
+    test('[Example 6.14]', () {
+      expectYamlLoads('foo', '''
+        %YAML 1.3 # Attempt parsing
+                   # with a warning
+        ---
+        "foo"''');
+    });
+
+    test('[Example 6.15]', () {
+      expectYamlFails('''
+        %YAML 1.2
+        %YAML 1.1
+        foo''');
+    });
+
+    test('[Example 6.16]', () {
+      expectYamlLoads('foo', '''
+        %TAG !yaml! tag:yaml.org,2002:
+        ---
+        !yaml!str "foo"''');
+    });
+
+    test('[Example 6.17]', () {
+      expectYamlFails('''
+        %TAG ! !foo
+        %TAG ! !foo
+        bar''');
+    });
+
+    // Examples 6.18 through 6.22 test custom tag URIs, which this
+    // implementation currently doesn't plan to support.
+  });
+
+  group('6.9: Node Properties', () {
+    test('may be specified in any order', () {
+      expectYamlLoads(['foo', 'bar'], '''
+        - !!str &a1 foo
+        - &a2 !!str bar''');
+    });
+
+    test('[Example 6.23]', () {
+      expectYamlLoads({'foo': 'bar', 'baz': 'foo'}, '''
+        !!str &a1 "foo":
+          !!str bar
+        &a2 baz : *a1''');
+    });
+
+    // Example 6.24 tests custom tag URIs, which this implementation currently
+    // doesn't plan to support.
+
+    test('[Example 6.25]', () {
+      expectYamlFails('- !<!> foo');
+      expectYamlFails('- !<\$:?> foo');
+    });
+
+    // Examples 6.26 and 6.27 test custom tag URIs, which this implementation
+    // currently doesn't plan to support.
+
+    test('[Example 6.28]', () {
+      expectYamlLoads(['12', 12, '12'], '''
+        # Assuming conventional resolution:
+        - "12"
+        - 12
+        - ! 12''');
+    });
+
+    test('[Example 6.29]', () {
+      expectYamlLoads(
+          {'First occurrence': 'Value', 'Second occurrence': 'Value'}, '''
+        First occurrence: &anchor Value
+        Second occurrence: *anchor''');
+    });
+  });
+
+  // Chapter 7: Flow Styles
+  group('7.1: Alias Nodes', () {
+    test("must not use an anchor that doesn't previously occur", () {
+      expectYamlFails('''
+        - *anchor
+        - &anchor foo''');
+    });
+
+    test("don't have to exist for a given anchor node", () {
+      expectYamlLoads(['foo'], '- &anchor foo');
+    });
+
+    group('must not specify', () {
+      test('tag properties', () => expectYamlFails('''
+        - &anchor foo
+        - !str *anchor'''));
+
+      test('anchor properties', () => expectYamlFails('''
+        - &anchor foo
+        - &anchor2 *anchor'''));
+
+      test('content', () => expectYamlFails('''
+        - &anchor foo
+        - *anchor bar'''));
+    });
+
+    test('must preserve structural equality', () {
+      var doc = loadYaml(cleanUpLiteral('''
+        anchor: &anchor [a, b, c]
+        alias: *anchor'''));
+      var anchorList = doc['anchor'];
+      var aliasList = doc['alias'];
+      expect(anchorList, same(aliasList));
+
+      doc = loadYaml(cleanUpLiteral('''
+        ? &anchor [a, b, c]
+        : ? *anchor
+          : bar'''));
+      anchorList = doc.keys.first;
+      aliasList = doc[['a', 'b', 'c']].keys.first;
+      expect(anchorList, same(aliasList));
+    });
+
+    test('[Example 7.1]', () {
+      expectYamlLoads({
+        'First occurrence': 'Foo',
+        'Second occurrence': 'Foo',
+        'Override anchor': 'Bar',
+        'Reuse anchor': 'Bar',
+      }, '''
+        First occurrence: &anchor Foo
+        Second occurrence: *anchor
+        Override anchor: &anchor Bar
+        Reuse anchor: *anchor''');
+    });
+  });
+
+  group('7.2: Empty Nodes', () {
+    test('[Example 7.2]', () {
+      expectYamlLoads({'foo': '', '': 'bar'}, '''
+        {
+          foo : !!str,
+          !!str : bar,
+        }''');
+    });
+
+    test('[Example 7.3]', () {
+      var doc = deepEqualsMap({'foo': null});
+      doc[null] = 'bar';
+      expectYamlLoads(doc, '''
+        {
+          ? foo :,
+          : bar,
+        }''');
+    });
+  });
+
+  group('7.3: Flow Scalar Styles', () {
+    test('[Example 7.4]', () {
+      expectYamlLoads({
+        'implicit block key': [
+          {'implicit flow key': 'value'}
+        ]
+      }, '''
+        "implicit block key" : [
+          "implicit flow key" : value,
+         ]''');
+    });
+
+    test('[Example 7.5]', () {
+      expectYamlLoads(
+          'folded to a space,\nto a line feed, or \t \tnon-content', '''
+        "folded 
+        to a space,\t
+         
+        to a line feed, or \t\\
+         \\ \tnon-content"''');
+    });
+
+    test('[Example 7.6]', () {
+      expectYamlLoads(' 1st non-empty\n2nd non-empty 3rd non-empty ', '''
+        " 1st non-empty
+
+         2nd non-empty 
+        \t3rd non-empty "''');
+    });
+
+    test('[Example 7.7]', () {
+      expectYamlLoads("here's to \"quotes\"", "'here''s to \"quotes\"'");
+    });
+
+    test('[Example 7.8]', () {
+      expectYamlLoads({
+        'implicit block key': [
+          {'implicit flow key': 'value'}
+        ]
+      }, """
+        'implicit block key' : [
+          'implicit flow key' : value,
+         ]""");
+    });
+
+    test('[Example 7.9]', () {
+      expectYamlLoads(' 1st non-empty\n2nd non-empty 3rd non-empty ', """
+        ' 1st non-empty
+
+         2nd non-empty 
+        \t3rd non-empty '""");
+    });
+
+    test('[Example 7.10]', () {
+      expectYamlLoads([
+        '::vector',
+        ': - ()',
+        'Up, up, and away!',
+        -123,
+        'http://example.com/foo#bar',
+        [
+          '::vector',
+          ': - ()',
+          'Up, up, and away!',
+          -123,
+          'http://example.com/foo#bar'
+        ]
+      ], '''
+        # Outside flow collection:
+        - ::vector
+        - ": - ()"
+        - Up, up, and away!
+        - -123
+        - http://example.com/foo#bar
+        # Inside flow collection:
+        - [ ::vector,
+          ": - ()",
+          "Up, up, and away!",
+          -123,
+          http://example.com/foo#bar ]''');
+    });
+
+    test('[Example 7.11]', () {
+      expectYamlLoads({
+        'implicit block key': [
+          {'implicit flow key': 'value'}
+        ]
+      }, '''
+        implicit block key : [
+          implicit flow key : value,
+         ]''');
+    });
+
+    test('[Example 7.12]', () {
+      expectYamlLoads('1st non-empty\n2nd non-empty 3rd non-empty', '''
+        1st non-empty
+
+         2nd non-empty 
+        \t3rd non-empty''');
+    });
+  });
+
+  group('7.4: Flow Collection Styles', () {
+    test('[Example 7.13]', () {
+      expectYamlLoads([
+        ['one', 'two'],
+        ['three', 'four']
+      ], '''
+        - [ one, two, ]
+        - [three ,four]''');
+    });
+
+    test('[Example 7.14]', () {
+      expectYamlLoads([
+        'double quoted',
+        'single quoted',
+        'plain text',
+        ['nested'],
+        {'single': 'pair'}
+      ], """
+        [
+        "double
+         quoted", 'single
+                   quoted',
+        plain
+         text, [ nested ],
+        single: pair,
+        ]""");
+    });
+
+    test('[Example 7.15]', () {
+      expectYamlLoads([
+        {'one': 'two', 'three': 'four'},
+        {'five': 'six', 'seven': 'eight'},
+      ], '''
+        - { one : two , three: four , }
+        - {five: six,seven : eight}''');
+    });
+
+    test('[Example 7.16]', () {
+      var doc = deepEqualsMap({'explicit': 'entry', 'implicit': 'entry'});
+      doc[null] = null;
+      expectYamlLoads(doc, '''
+        {
+        ? explicit: entry,
+        implicit: entry,
+        ?
+        }''');
+    });
+
+    test('[Example 7.17]', () {
+      var doc = deepEqualsMap({
+        'unquoted': 'separate',
+        'http://foo.com': null,
+        'omitted value': null
+      });
+      doc[null] = 'omitted key';
+      expectYamlLoads(doc, '''
+        {
+        unquoted : "separate",
+        http://foo.com,
+        omitted value:,
+        : omitted key,
+        }''');
+    });
+
+    test('[Example 7.18]', () {
+      expectYamlLoads(
+          {'adjacent': 'value', 'readable': 'value', 'empty': null}, '''
+        {
+        "adjacent":value,
+        "readable": value,
+        "empty":
+        }''');
+    });
+
+    test('[Example 7.19]', () {
+      expectYamlLoads([
+        {'foo': 'bar'}
+      ], '''
+        [
+        foo: bar
+        ]''');
+    });
+
+    test('[Example 7.20]', () {
+      expectYamlLoads([
+        {'foo bar': 'baz'}
+      ], '''
+        [
+        ? foo
+         bar : baz
+        ]''');
+    });
+
+    test('[Example 7.21]', () {
+      var el1 = deepEqualsMap();
+      el1[null] = 'empty key entry';
+
+      var el2 = deepEqualsMap();
+      el2[{'JSON': 'like'}] = 'adjacent';
+
+      expectYamlLoads([
+        [
+          {'YAML': 'separate'}
+        ],
+        [el1],
+        [el2]
+      ], '''
+        - [ YAML : separate ]
+        - [ : empty key entry ]
+        - [ {JSON: like}:adjacent ]''');
+    });
+
+    // TODO(nweiz): enable this when we throw an error for long or multiline
+    // keys.
+    // test('[Example 7.22]', () {
+    //   expectYamlFails(
+    //     """
+    //     [ foo
+    //      bar: invalid ]""");
+    //
+    //   var dotList = new List.filled(1024, ' ');
+    //   var dots = dotList.join();
+    //   expectYamlFails('[ "foo...$dots...bar": invalid ]');
+    // });
+  });
+
+  group('7.5: Flow Nodes', () {
+    test('[Example 7.23]', () {
+      expectYamlLoads([
+        ['a', 'b'],
+        {'a': 'b'},
+        'a',
+        'b',
+        'c'
+      ], '''
+        - [ a, b ]
+        - { a: b }
+        - 'a'
+        - 'b'
+        - c''');
+    });
+
+    test('[Example 7.24]', () {
+      expectYamlLoads(['a', 'b', 'c', 'c', ''], '''
+        - !!str "a"
+        - 'b'
+        - &anchor "c"
+        - *anchor
+        - !!str''');
+    });
+  });
+
+  // Chapter 8: Block Styles
+  group('8.1: Block Scalar Styles', () {
+    test('[Example 8.1]', () {
+      expectYamlLoads(['literal\n', ' folded\n', 'keep\n\n', ' strip'], '''
+        - | # Empty header
+         literal
+        - >1 # Indentation indicator
+          folded
+        - |+ # Chomping indicator
+         keep
+
+        - >1- # Both indicators
+          strip''');
+    });
+
+    test('[Example 8.2]', () {
+      // Note: in the spec, the fourth element in this array is listed as
+      // "\t detected\n", not "\t\ndetected\n". However, I'm reasonably
+      // confident that "\t\ndetected\n" is correct when parsed according to the
+      // rest of the spec.
+      expectYamlLoads(
+          ['detected\n', '\n\n# detected\n', ' explicit\n', '\t\ndetected\n'],
+          '''
+        - |
+         detected
+        - >
+
+
+          # detected
+        - |1
+          explicit
+        - >
+         \t
+         detected
+        ''');
+    });
+
+    test('[Example 8.3]', () {
+      expectYamlFails('''
+        - |
+          
+         text''');
+
+      expectYamlFails('''
+        - >
+          text
+         text''');
+
+      expectYamlFails('''
+        - |2
+         text''');
+    });
+
+    test('[Example 8.4]', () {
+      expectYamlLoads({'strip': 'text', 'clip': 'text\n', 'keep': 'text\n'}, '''
+        strip: |-
+          text
+        clip: |
+          text
+        keep: |+
+          text
+        ''');
+    });
+
+    test('[Example 8.5]', () {
+      // This example in the spec only includes a single newline in the "keep"
+      // value, but as far as I can tell that's not how it's supposed to be
+      // parsed according to the rest of the spec.
+      expectYamlLoads(
+          {'strip': '# text', 'clip': '# text\n', 'keep': '# text\n\n'}, '''
+         # Strip
+          # Comments:
+        strip: |-
+          # text
+          
+         # Clip
+          # comments:
+
+        clip: |
+          # text
+         
+         # Keep
+          # comments:
+
+        keep: |+
+          # text
+
+         # Trail
+          # comments.
+        ''');
+    });
+
+    test('[Example 8.6]', () {
+      expectYamlLoads({'strip': '', 'clip': '', 'keep': '\n'}, '''
+        strip: >-
+
+        clip: >
+
+        keep: |+
+
+        ''');
+    });
+
+    test('[Example 8.7]', () {
+      expectYamlLoads('literal\n\ttext\n', '''
+        |
+         literal
+         \ttext
+        ''');
+    });
+
+    test('[Example 8.8]', () {
+      expectYamlLoads('\n\nliteral\n \n\ntext\n', '''
+        |
+         
+          
+          literal
+           
+          
+          text
+
+         # Comment''');
+    });
+
+    test('[Example 8.9]', () {
+      expectYamlLoads('folded text\n', '''
+        >
+         folded
+         text
+        ''');
+    });
+
+    test('[Example 8.10]', () {
+      expectYamlLoads(cleanUpLiteral('''
+
+        folded line
+        next line
+          * bullet
+
+          * list
+          * lines
+
+        last line
+        '''), '''
+        >
+
+         folded
+         line
+
+         next
+         line
+           * bullet
+
+           * list
+           * lines
+
+         last
+         line
+
+        # Comment''');
+    });
+
+    // Examples 8.11 through 8.13 are duplicates of 8.10.
+  });
+
+  group('8.2: Block Collection Styles', () {
+    test('[Example 8.14]', () {
+      expectYamlLoads({
+        'block sequence': [
+          'one',
+          {'two': 'three'}
+        ]
+      }, '''
+        block sequence:
+          - one
+          - two : three''');
+    });
+
+    test('[Example 8.15]', () {
+      expectYamlLoads([
+        null,
+        'block node\n',
+        ['one', 'two'],
+        {'one': 'two'}
+      ], '''
+        - # Empty
+        - |
+         block node
+        - - one # Compact
+          - two # sequence
+        - one: two # Compact mapping''');
+    });
+
+    test('[Example 8.16]', () {
+      expectYamlLoads({
+        'block mapping': {'key': 'value'}
+      }, '''
+        block mapping:
+         key: value''');
+    });
+
+    test('[Example 8.17]', () {
+      expectYamlLoads({
+        'explicit key': null,
+        'block key\n': ['one', 'two']
+      }, '''
+        ? explicit key # Empty value
+        ? |
+          block key
+        : - one # Explicit compact
+          - two # block value''');
+    });
+
+    test('[Example 8.18]', () {
+      var doc = deepEqualsMap({
+        'plain key': 'in-line value',
+        'quoted key': ['entry']
+      });
+      doc[null] = null;
+      expectYamlLoads(doc, '''
+        plain key: in-line value
+        : # Both empty
+        "quoted key":
+        - entry''');
+    });
+
+    test('[Example 8.19]', () {
+      var el = deepEqualsMap();
+      el[{'earth': 'blue'}] = {'moon': 'white'};
+      expectYamlLoads([
+        {'sun': 'yellow'},
+        el
+      ], '''
+        - sun: yellow
+        - ? earth: blue
+          : moon: white''');
+    });
+
+    test('[Example 8.20]', () {
+      expectYamlLoads([
+        'flow in block',
+        'Block scalar\n',
+        {'foo': 'bar'}
+      ], '''
+        -
+          "flow in block"
+        - >
+         Block scalar
+        - !!map # Block collection
+          foo : bar''');
+    });
+
+    test('[Example 8.21]', () {
+      // The spec doesn't include a newline after "value" in the parsed map, but
+      // the block scalar is clipped so it should be retained.
+      expectYamlLoads({'literal': 'value\n', 'folded': 'value'}, '''
+        literal: |2
+          value
+        folded:
+           !!str
+          >1
+         value''');
+    });
+
+    test('[Example 8.22]', () {
+      expectYamlLoads({
+        'sequence': [
+          'entry',
+          ['nested']
+        ],
+        'mapping': {'foo': 'bar'}
+      }, '''
+        sequence: !!seq
+        - entry
+        - !!seq
+         - nested
+        mapping: !!map
+         foo: bar''');
+    });
+  });
+
+  // Chapter 9: YAML Character Stream
+  group('9.1: Documents', () {
+    // Example 9.1 tests the use of a BOM, which this implementation currently
+    // doesn't plan to support.
+
+    test('[Example 9.2]', () {
+      expectYamlLoads('Document', '''
+        %YAML 1.2
+        ---
+        Document
+        ... # Suffix''');
+    });
+
+    test('[Example 9.3]', () {
+      // The spec example indicates that the comment after "%!PS-Adobe-2.0"
+      // should be stripped, which would imply that that line is not part of the
+      // literal defined by the "|". The rest of the spec is ambiguous on this
+      // point; the allowable indentation for non-indented literal content is
+      // not clearly explained. However, if both the "|" and the text were
+      // indented the same amount, the text would be part of the literal, which
+      // implies that the spec's parse of this document is incorrect.
+      expectYamlStreamLoads(
+          ['Bare document', '%!PS-Adobe-2.0 # Not the first line\n'], '''
+        Bare
+        document
+        ...
+        # No document
+        ...
+        |
+        %!PS-Adobe-2.0 # Not the first line
+        ''');
+    });
+
+    test('[Example 9.4]', () {
+      expectYamlStreamLoads([
+        {'matches %': 20},
+        null
+      ], '''
+        ---
+        { matches
+        % : 20 }
+        ...
+        ---
+        # Empty
+        ...''');
+    });
+
+    test('[Example 9.5]', () {
+      // The spec doesn't have a space between the second
+      // "YAML" and "1.2", but this seems to be a typo.
+      expectYamlStreamLoads(['%!PS-Adobe-2.0\n', null], '''
+        %YAML 1.2
+        --- |
+        %!PS-Adobe-2.0
+        ...
+        %YAML 1.2
+        ---
+        # Empty
+        ...''');
+    });
+
+    test('[Example 9.6]', () {
+      expectYamlStreamLoads([
+        'Document',
+        null,
+        {'matches %': 20}
+      ], '''
+        Document
+        ---
+        # Empty
+        ...
+        %YAML 1.2
+        ---
+        matches %: 20''');
+    });
+  });
+
+  // Chapter 10: Recommended Schemas
+  group('10.1: Failsafe Schema', () {
+    test('[Example 10.1]', () {
+      expectYamlLoads({
+        'Block style': {
+          'Clark': 'Evans',
+          'Ingy': 'döt Net',
+          'Oren': 'Ben-Kiki'
+        },
+        'Flow style': {'Clark': 'Evans', 'Ingy': 'döt Net', 'Oren': 'Ben-Kiki'}
+      }, '''
+        Block style: !!map
+          Clark : Evans
+          Ingy  : döt Net
+          Oren  : Ben-Kiki
+
+        Flow style: !!map { Clark: Evans, Ingy: döt Net, Oren: Ben-Kiki }''');
+    });
+
+    test('[Example 10.2]', () {
+      expectYamlLoads({
+        'Block style': ['Clark Evans', 'Ingy döt Net', 'Oren Ben-Kiki'],
+        'Flow style': ['Clark Evans', 'Ingy döt Net', 'Oren Ben-Kiki']
+      }, '''
+        Block style: !!seq
+        - Clark Evans
+        - Ingy döt Net
+        - Oren Ben-Kiki
+
+        Flow style: !!seq [ Clark Evans, Ingy döt Net, Oren Ben-Kiki ]''');
+    });
+
+    test('[Example 10.3]', () {
+      expectYamlLoads({
+        'Block style': 'String: just a theory.',
+        'Flow style': 'String: just a theory.'
+      }, '''
+        Block style: !!str |-
+          String: just a theory.
+
+        Flow style: !!str "String: just a theory."''');
+    });
+  });
+
+  group('10.2: JSON Schema', () {
+    test('[Example 10.4]', () {
+      var doc = deepEqualsMap({'key with null value': null});
+      doc[null] = 'value for null key';
+      expectYamlStreamLoads([doc], '''
+        !!null null: value for null key
+        key with null value: !!null null''');
+    });
+
+    test('[Example 10.5]', () {
+      expectYamlStreamLoads([
+        {'YAML is a superset of JSON': true, 'Pluto is a planet': false}
+      ], '''
+        YAML is a superset of JSON: !!bool true
+        Pluto is a planet: !!bool false''');
+    });
+
+    test('[Example 10.6]', () {
+      expectYamlStreamLoads([
+        {'negative': -12, 'zero': 0, 'positive': 34}
+      ], '''
+        negative: !!int -12
+        zero: !!int 0
+        positive: !!int 34''');
+    });
+
+    test('[Example 10.7]', () {
+      expectYamlStreamLoads([
+        {
+          'negative': -1,
+          'zero': 0,
+          'positive': 23000,
+          'infinity': infinity,
+          'not a number': nan
+        }
+      ], '''
+        negative: !!float -1
+        zero: !!float 0
+        positive: !!float 2.3e4
+        infinity: !!float .inf
+        not a number: !!float .nan''');
+    }, skip: 'Fails for single digit float');
+
+    test('[Example 10.8]', () {
+      expectYamlStreamLoads([
+        {
+          'A null': null,
+          'Booleans': [true, false],
+          'Integers': [0, -0, 3, -19],
+          'Floats': [0, 0, 12000, -200000],
+          // Despite being invalid in the JSON schema, these values are valid in
+          // the core schema which this implementation supports.
+          'Invalid': [true, null, 7, 0x3A, 12.3]
+        }
+      ], '''
+        A null: null
+        Booleans: [ true, false ]
+        Integers: [ 0, -0, 3, -19 ]
+        Floats: [ 0., -0.0, 12e03, -2E+05 ]
+        Invalid: [ True, Null, 0o7, 0x3A, +12.3 ]''');
+    });
+  });
+
+  group('10.3: Core Schema', () {
+    test('[Example 10.9]', () {
+      expectYamlLoads({
+        'A null': null,
+        'Also a null': null,
+        'Not a null': '',
+        'Booleans': [true, true, false, false],
+        'Integers': [0, 7, 0x3A, -19],
+        'Floats': [0, 0, 0.5, 12000, -200000],
+        'Also floats': [infinity, -infinity, infinity, nan]
+      }, '''
+        A null: null
+        Also a null: # Empty
+        Not a null: ""
+        Booleans: [ true, True, false, FALSE ]
+        Integers: [ 0, 0o7, 0x3A, -19 ]
+        Floats: [ 0., -0.0, .5, +12e03, -2E+05 ]
+        Also floats: [ .inf, -.Inf, +.INF, .NAN ]''');
+    });
+  });
+
+  test('preserves key order', () {
+    const keys = ['a', 'b', 'c', 'd', 'e', 'f'];
+    var sanityCheckCount = 0;
+    for (var permutation in _generatePermutations(keys)) {
+      final yaml = permutation.map((key) => '$key: value').join('\n');
+      expect(loadYaml(yaml).keys.toList(), permutation);
+      sanityCheckCount++;
+    }
+    final expectedPermutationCount =
+        List.generate(keys.length, (i) => i + 1).reduce((n, i) => n * i);
+    expect(sanityCheckCount, expectedPermutationCount);
+  });
+}
+
+Iterable<List<String>> _generatePermutations(List<String> keys) sync* {
+  if (keys.length <= 1) {
+    yield keys;
+    return;
+  }
+  for (var i = 0; i < keys.length; i++) {
+    final first = keys[i];
+    final rest = <String>[...keys.sublist(0, i), ...keys.sublist(i + 1)];
+    for (var subPermutation in _generatePermutations(rest)) {
+      yield <String>[first, ...subPermutation];
+    }
+  }
+}
diff --git a/pkgs/yaml_edit/.gitignore b/pkgs/yaml_edit/.gitignore
new file mode 100644
index 0000000..7886c3d
--- /dev/null
+++ b/pkgs/yaml_edit/.gitignore
@@ -0,0 +1,3 @@
+/.dart_tool/
+/.packages
+/pubspec.lock
diff --git a/pkgs/yaml_edit/CHANGELOG.md b/pkgs/yaml_edit/CHANGELOG.md
new file mode 100644
index 0000000..9342e9f
--- /dev/null
+++ b/pkgs/yaml_edit/CHANGELOG.md
@@ -0,0 +1,95 @@
+## 2.2.2
+
+- Suppress warnings previously printed to `stdout` when parsing YAML internally.
+- Fix error thrown when inserting duplicate keys to different maps in the same
+  list.
+  ([#69](https://github.com/dart-lang/yaml_edit/issues/69))
+
+- Fix error thrown when inserting in nested list using `spliceList` method
+  ([#83](https://github.com/dart-lang/yaml_edit/issues/83))
+
+- Fix error thrown when string has spaces when applying `ScalarStyle.FOLDED`.
+  ([#41](https://github.com/dart-lang/yaml_edit/issues/41)). Resolves
+  ([[#86](https://github.com/dart-lang/yaml_edit/issues/86)]).
+
+- Require Dart 3.1
+
+- Move to `dart-lang/tools` monorepo.
+
+## 2.2.1
+
+- Require Dart 3.0
+- Fix removal of last key in blockmap when key has no value
+  ([#55](https://github.com/dart-lang/yaml_edit/issues/55)).
+
+## 2.2.0
+
+- Fix inconsistent line endings when inserting maps into a document using `\r\n`.
+  ([#65](https://github.com/dart-lang/yaml_edit/issues/65))
+
+- `AliasError` is changed to `AliasException` and exposed in the public API.
+
+  All node-mutating methods on `YamlEditor`, i.e. `update()`, `appendToList()`,
+  `prependToList()`, `insertIntoList()`, `spliceList()`, `remove()` will now
+  throw an exception instead of an error when encountering an alias on the path
+  to modify.
+
+  This allows catching and handling when this is happening.
+
+## 2.1.1
+
+- Require Dart 2.19
+
+## 2.1.0
+
+- **Breaking** `wrapAsYamlNode(value, collectionStyle, scalarStyle)` will apply
+  `collectionStyle` and `scalarStyle` recursively when wrapping a children of
+  `Map` and `List`.
+  While this may change the style of the YAML documents written by applications
+  that rely on the old behavior, such YAML documents should still be valid.
+  Hence, we hope it is reasonable to make this change in a minor release.
+- Fix for cases that can't be encoded correctly with
+  `scalarStyle: ScalarStyle.SINGLE_QUOTED`.
+- Fix YamlEditor `appendToList` and `insertIntoList` functions inserts new item into next yaml item
+  rather than at end of list.
+  ([#23](https://github.com/dart-lang/yaml_edit/issues/23))
+
+## 2.0.3
+
+- Updated the value of the pubspec `repository` field.
+
+## 2.0.2
+
+- Fix trailing whitespace after adding new key with block-value to map
+  ([#15](https://github.com/dart-lang/yaml_edit/issues/15)).
+- Updated `repository` and other meta-data in `pubspec.yaml`.
+
+## 2.0.1
+
+- License changed to BSD, as this package is now maintained by the Dart team.
+- Fixed minor lints.
+
+## 2.0.0
+
+- Migrated to null-safety.
+- API will no-longer return `null` in-place of a `YamlNode`, instead a
+  `YamlNode` with `YamlNode.value == null` should be used. These are easily
+  created with `wrapAsYamlNode(null)`.
+
+## 1.0.3
+
+- Fixed bug in adding an empty map as a map value.
+
+## 1.0.2
+
+- Throws an error if the final YAML after edit is not parsable.
+- Fixed bug in adding to empty map values, when it is followed by other content.
+
+## 1.0.1
+
+- Updated behavior surrounding list and map removal.
+- Fixed bug in dealing with empty values.
+
+## 1.0.0
+
+- Initial release.
diff --git a/pkgs/yaml_edit/LICENSE b/pkgs/yaml_edit/LICENSE
new file mode 100644
index 0000000..413ed83
--- /dev/null
+++ b/pkgs/yaml_edit/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2020, 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/yaml_edit/README.md b/pkgs/yaml_edit/README.md
new file mode 100644
index 0000000..f10560b
--- /dev/null
+++ b/pkgs/yaml_edit/README.md
@@ -0,0 +1,61 @@
+[![Dart CI](https://github.com/dart-lang/yaml_edit/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/yaml_edit/actions/workflows/test-package.yml)
+[![pub package](https://img.shields.io/pub/v/yaml_edit.svg)](https://pub.dev/packages/yaml_edit)
+[![package publisher](https://img.shields.io/pub/publisher/yaml_edit.svg)](https://pub.dev/packages/yaml_edit/publisher)
+[![Coverage Status](https://coveralls.io/repos/github/dart-lang/yaml_edit/badge.svg)](https://coveralls.io/github/dart-lang/yaml_edit)
+
+A library for [YAML](https://yaml.org) manipulation while preserving comments.
+
+## Usage
+
+A simple usage example:
+
+```dart
+import 'package:yaml_edit/yaml_edit.dart';
+
+void main() {
+  final yamlEditor = YamlEditor('{YAML: YAML}');
+  yamlEditor.update(['YAML'], "YAML Ain't Markup Language");
+  print(yamlEditor);
+  // Expected output:
+  // {YAML: YAML Ain't Markup Language}
+}
+```
+
+### Example: Converting JSON to YAML (block formatted)
+
+```dart
+void main() {
+  final jsonString = r'''
+{
+  "key": "value",
+  "list": [
+    "first",
+    "second",
+    "last entry in the list"
+  ],
+  "map": {
+    "multiline": "this is a fairly long string with\nline breaks..."
+  }
+}
+''';
+  final jsonValue = json.decode(jsonString);
+
+  // Convert jsonValue to YAML
+  final yamlEditor = YamlEditor('');
+  yamlEditor.update([], jsonValue);
+  print(yamlEditor.toString());
+}
+```
+
+## Testing
+
+Testing is done in two strategies: Unit testing (`/test/editor_test.dart`) and
+Golden testing (`/test/golden_test.dart`). More information on Golden testing
+and the input/output format can be found at `/test/testdata/README.md`.
+
+These tests are automatically run with `pub run test`.
+
+## Limitations
+
+1. Users are not allowed to define tags in the modifications.
+2. Map keys will always be added in the flow style.
diff --git a/pkgs/yaml_edit/analysis_options.yaml b/pkgs/yaml_edit/analysis_options.yaml
new file mode 100644
index 0000000..937e7fe
--- /dev/null
+++ b/pkgs/yaml_edit/analysis_options.yaml
@@ -0,0 +1,8 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  errors:
+    inference_failure_on_collection_literal: ignore
+    inference_failure_on_function_invocation: ignore
+    inference_failure_on_function_return_type: ignore
+    inference_failure_on_instance_creation: ignore
diff --git a/pkgs/yaml_edit/example/example.dart b/pkgs/yaml_edit/example/example.dart
new file mode 100644
index 0000000..d49c39b
--- /dev/null
+++ b/pkgs/yaml_edit/example/example.dart
@@ -0,0 +1,12 @@
+import 'package:yaml_edit/yaml_edit.dart';
+
+void main() {
+  final doc = YamlEditor('''
+- 0 # comment 0
+- 1 # comment 1
+- 2 # comment 2
+''');
+  doc.remove([1]);
+
+  print(doc);
+}
diff --git a/pkgs/yaml_edit/example/json2yaml.dart b/pkgs/yaml_edit/example/json2yaml.dart
new file mode 100644
index 0000000..d6204d3
--- /dev/null
+++ b/pkgs/yaml_edit/example/json2yaml.dart
@@ -0,0 +1,28 @@
+// Copyright (c) 2023, 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 json;
+
+import 'package:yaml_edit/yaml_edit.dart';
+
+void main() {
+  final jsonString = r'''
+{
+  "key": "value",
+  "list": [
+    "first",
+    "second",
+    "last entry in the list"
+  ],
+  "map": {
+    "multiline": "this is a fairly long string with\nline breaks..."
+  }
+}
+''';
+  final jsonValue = json.decode(jsonString);
+
+  final yamlEditor = YamlEditor('');
+  yamlEditor.update([], jsonValue);
+  print(yamlEditor.toString());
+}
diff --git a/pkgs/yaml_edit/lib/src/editor.dart b/pkgs/yaml_edit/lib/src/editor.dart
new file mode 100644
index 0000000..54775cc
--- /dev/null
+++ b/pkgs/yaml_edit/lib/src/editor.dart
@@ -0,0 +1,634 @@
+// 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:meta/meta.dart';
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+import 'equality.dart';
+import 'errors.dart';
+import 'list_mutations.dart';
+import 'map_mutations.dart';
+import 'source_edit.dart';
+import 'strings.dart';
+import 'utils.dart';
+import 'wrap.dart';
+
+/// An interface for modifying [YAML][1] documents while preserving comments
+/// and whitespaces.
+///
+/// YAML parsing is supported by `package:yaml`, and modifications are performed
+/// as string operations. An error will be thrown if internal assertions fail -
+/// such a situation should be extremely rare, and should only occur with
+/// degenerate formatting.
+///
+/// Most modification methods require the user to pass in an `Iterable<Object>`
+/// path that holds the keys/indices to navigate to the element.
+///
+/// **Example:**
+/// ```yaml
+/// a: 1
+/// b: 2
+/// c:
+///   - 3
+///   - 4
+///   - {e: 5, f: [6, 7]}
+/// ```
+///
+/// To get to `7`, our path will be `['c', 2, 'f', 1]`. The path for the base
+/// object is the empty array `[]`. All modification methods will throw a
+/// [ArgumentError] if the path provided is invalid. Note also that that the
+/// order of elements in the path is important, and it should be arranged in
+/// order of calling, with the first element being the first key or index to be
+/// called.
+///
+/// In most modification methods, users are required to pass in a value to be
+/// used for updating the YAML tree. This value is only allowed to either be a
+/// valid scalar that is recognizable by YAML (i.e. `bool`, `String`, `List`,
+/// `Map`, `num`, `null`) or a [YamlNode]. Should the user want to specify
+/// the style to be applied to the value passed in, the user may wrap the value
+/// using [wrapAsYamlNode] while passing in the appropriate `scalarStyle` or
+/// `collectionStyle`. While we try to respect the style that is passed in,
+/// there will be instances where the formatting will not result in valid YAML,
+/// and as such we will fallback to a default formatting while preserving the
+/// content.
+///
+/// To dump the YAML after all the modifications have been completed, simply
+/// call [toString()].
+///
+/// [1]: https://yaml.org/
+@sealed
+class YamlEditor {
+  final List<SourceEdit> _edits = [];
+
+  /// List of [SourceEdit]s that have been applied to [_yaml] since the creation
+  /// of this instance, in chronological order. Intended to be compatible with
+  /// `package:analysis_server`.
+  ///
+  /// The [SourceEdit] objects can be serialized to JSON using the `toJSON`
+  /// function, deserialized using [SourceEdit.fromJson], and applied to a
+  /// string using the `apply` function. Multiple [SourceEdit]s can be applied
+  /// to a string using [SourceEdit.applyAll].
+  ///
+  /// For more information, refer to the [SourceEdit] class.
+  List<SourceEdit> get edits => [..._edits];
+
+  /// Current YAML string.
+  String _yaml;
+
+  /// Root node of YAML AST.
+  YamlNode _contents;
+
+  /// Stores the list of nodes in [_contents] that are connected by aliases.
+  ///
+  /// When a node is anchored with an alias and subsequently referenced,
+  /// the full content of the anchored node is thought to be copied in the
+  /// following references.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// a: &SS Sammy Sosa
+  /// b: *SS
+  /// ```
+  ///
+  /// is equivalent to
+  ///
+  /// ```dart
+  /// a: Sammy Sosa
+  /// b: Sammy Sosa
+  /// ```
+  ///
+  /// As such, aliased nodes have to be treated with special caution when
+  /// any modification is taking place.
+  ///
+  /// See 7.1 Alias Nodes: https://yaml.org/spec/1.2/spec.html#id2786196
+  Set<YamlNode> _aliases = {};
+
+  /// Returns the current YAML string.
+  @override
+  String toString() => _yaml;
+
+  factory YamlEditor(String yaml) => YamlEditor._(yaml);
+
+  YamlEditor._(this._yaml) : _contents = loadYamlNode(_yaml) {
+    _initialize();
+  }
+
+  /// Traverses the YAML tree formed to detect alias nodes.
+  void _initialize() {
+    _aliases = {};
+
+    /// Performs a DFS on [_contents] to detect alias nodes.
+    final visited = <YamlNode>{};
+    void collectAliases(YamlNode node) {
+      if (visited.add(node)) {
+        if (node is YamlMap) {
+          node.nodes.forEach((key, value) {
+            collectAliases(key as YamlNode);
+            collectAliases(value);
+          });
+        } else if (node is YamlList) {
+          node.nodes.forEach(collectAliases);
+        }
+      } else {
+        _aliases.add(node);
+      }
+    }
+
+    collectAliases(_contents);
+  }
+
+  /// Parses the document to return [YamlNode] currently present at [path].
+  ///
+  /// If no [YamlNode]s exist at [path], the result of invoking the [orElse]
+  /// function is returned.
+  ///
+  /// If [orElse] is omitted, it defaults to throwing a [ArgumentError].
+  ///
+  /// To get a default value when [path] does not point to a value in the
+  /// [YamlNode]-tree, simply pass `orElse: () => ...`.
+  ///
+  /// **Example:** (using orElse)
+  /// ```dart
+  /// final myYamlEditor('{"key": "value"}');
+  /// final node = myYamlEditor.valueAt(
+  ///   ['invalid', 'path'],
+  ///   orElse: () => wrapAsYamlNode(null),
+  /// );
+  /// print(node.value); // null
+  /// ```
+  ///
+  /// **Example:** (common usage)
+  /// ```dart
+  ///   final doc = YamlEditor('''
+  /// a: 1
+  /// b:
+  ///   d: 4
+  ///   e: [5, 6, 7]
+  /// c: 3
+  /// ''');
+  /// print(doc.parseAt(['b', 'e', 2])); // 7
+  /// ```
+  /// The value returned by [parseAt] is invalidated when the documented is
+  /// mutated, as illustrated below:
+  ///
+  /// **Example:** (old [parseAt] value is invalidated)
+  /// ```dart
+  /// final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+  /// final node = doc.parseAt(['YAML']);
+  ///
+  /// print(node.value); // Expected output: "YAML Ain't Markup Language"
+  ///
+  /// doc.update(['YAML'], 'YAML');
+  ///
+  /// final newNode = doc.parseAt(['YAML']);
+  ///
+  /// // Note that the value does not change
+  /// print(newNode.value); // "YAML"
+  /// print(node.value); // "YAML Ain't Markup Language"
+  /// ```
+  YamlNode parseAt(Iterable<Object?> path, {YamlNode Function()? orElse}) {
+    return _traverse(path, orElse: orElse);
+  }
+
+  /// Sets [value] in the [path].
+  ///
+  /// There is a subtle difference between [update] and [remove] followed by
+  /// an [insertIntoList], because [update] preserves comments at the same
+  /// level.
+  ///
+  /// Throws a [ArgumentError] if [path] is invalid.
+  ///
+  /// Throws an [AliasException] if a node on [path] is an alias or anchor.
+  ///
+  /// **Example:** (using [update])
+  /// ```dart
+  /// final doc = YamlEditor('''
+  ///   - 0
+  ///   - 1 # comment
+  ///   - 2
+  /// ''');
+  /// doc.update([1], 'test');
+  /// ```
+  ///
+  /// **Expected Output:**
+  /// ```yaml
+  ///   - 0
+  ///   - test # comment
+  ///   - 2
+  /// ```
+  ///
+  /// **Example:** (using [remove] and [insertIntoList])
+  /// ```dart
+  /// final doc2 = YamlEditor('''
+  ///   - 0
+  ///   - 1 # comment
+  ///   - 2
+  /// ''');
+  /// doc2.remove([1]);
+  /// doc2.insertIntoList([], 1, 'test');
+  /// ```
+  ///
+  /// **Expected Output:**
+  /// ```yaml
+  ///   - 0
+  ///   - test
+  ///   - 2
+  /// ```
+  void update(Iterable<Object?> path, Object? value) {
+    final valueNode = wrapAsYamlNode(value);
+
+    if (path.isEmpty) {
+      final start = _contents.span.start.offset;
+      final end = getContentSensitiveEnd(_contents);
+      final lineEnding = getLineEnding(_yaml);
+      final edit = SourceEdit(
+          start, end - start, yamlEncodeBlock(valueNode, 0, lineEnding));
+
+      return _performEdit(edit, path, valueNode);
+    }
+
+    final pathAsList = path.toList();
+    final collectionPath = pathAsList.take(path.length - 1);
+    final keyOrIndex = pathAsList.last;
+    final parentNode = _traverse(collectionPath, checkAlias: true);
+
+    if (parentNode is YamlList) {
+      if (keyOrIndex is! int) {
+        throw PathError(path, path, parentNode);
+      }
+      final expected = wrapAsYamlNode(
+        [...parentNode.nodes]..[keyOrIndex] = valueNode,
+      );
+
+      return _performEdit(updateInList(this, parentNode, keyOrIndex, valueNode),
+          collectionPath, expected);
+    }
+
+    if (parentNode is YamlMap) {
+      final expectedMap =
+          updatedYamlMap(parentNode, (nodes) => nodes[keyOrIndex] = valueNode);
+      return _performEdit(updateInMap(this, parentNode, keyOrIndex, valueNode),
+          collectionPath, expectedMap);
+    }
+
+    throw PathError.unexpected(
+        path, 'Scalar $parentNode does not have key $keyOrIndex');
+  }
+
+  /// Appends [value] to the list at [path].
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a
+  /// [YamlList] or if the path is invalid.
+  ///
+  /// Throws an [AliasException] if a node on [path] is an alias or anchor.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[0, 1]');
+  /// doc.appendToList([], 2); // [0, 1, 2]
+  /// ```
+  void appendToList(Iterable<Object?> path, Object? value) {
+    final yamlList = _traverseToList(path);
+
+    insertIntoList(path, yamlList.length, value);
+  }
+
+  /// Prepends [value] to the list at [path].
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a
+  /// [YamlList] or if the path is invalid.
+  ///
+  /// Throws an [AliasException] if a node on [path] is an alias or anchor.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[1, 2]');
+  /// doc.prependToList([], 0); // [0, 1, 2]
+  /// ```
+  void prependToList(Iterable<Object?> path, Object? value) {
+    insertIntoList(path, 0, value);
+  }
+
+  /// Inserts [value] into the list at [path].
+  ///
+  /// [index] must be non-negative and no greater than the list's length.
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a
+  /// [YamlList] or if the path is invalid.
+  ///
+  /// Throws an [AliasException] if a node on [path] is an alias or anchor.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[0, 2]');
+  /// doc.insertIntoList([], 1, 1); // [0, 1, 2]
+  /// ```
+  void insertIntoList(Iterable<Object?> path, int index, Object? value) {
+    final valueNode = wrapAsYamlNode(value);
+
+    final list = _traverseToList(path, checkAlias: true);
+    RangeError.checkValueInInterval(index, 0, list.length);
+
+    final edit = insertInList(this, list, index, valueNode);
+    final expected = wrapAsYamlNode(
+      [...list.nodes]..insert(index, valueNode),
+    );
+
+    _performEdit(edit, path, expected);
+  }
+
+  /// Changes the contents of the list at [path] by removing [deleteCount]
+  /// items at [index], and inserting [values] in-place. Returns the elements
+  /// that are deleted.
+  ///
+  /// [index] and [deleteCount] must be non-negative and [index] + [deleteCount]
+  /// must be no greater than the list's length.
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a
+  /// [YamlList] or if the path is invalid.
+  ///
+  /// Throws an [AliasException] if a node on [path] is an alias or anchor.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[Jan, March, April, June]');
+  /// doc.spliceList([], 1, 0, ['Feb']); // [Jan, Feb, March, April, June]
+  /// doc.spliceList([], 4, 1, ['May']); // [Jan, Feb, March, April, May]
+  /// ```
+  Iterable<YamlNode> spliceList(Iterable<Object?> path, int index,
+      int deleteCount, Iterable<Object?> values) {
+    final list = _traverseToList(path, checkAlias: true);
+
+    RangeError.checkValueInInterval(index, 0, list.length);
+    RangeError.checkValueInInterval(index + deleteCount, 0, list.length);
+
+    final nodesToRemove = list.nodes.getRange(index, index + deleteCount);
+
+    // Perform addition of elements before removal to avoid scenarios where
+    // a block list gets emptied out to {} to avoid changing collection styles
+    // where possible.
+
+    // Reverse [values] and insert them.
+    final reversedValues = values.toList().reversed;
+    for (final value in reversedValues) {
+      insertIntoList(path, index, value);
+    }
+
+    for (var i = 0; i < deleteCount; i++) {
+      remove([...path, index + values.length]);
+    }
+
+    return nodesToRemove;
+  }
+
+  /// Removes the node at [path]. Comments "belonging" to the node will be
+  /// removed while surrounding comments will be left untouched.
+  ///
+  /// Throws an [ArgumentError] if [path] is invalid.
+  ///
+  /// Throws an [AliasException] if a node on [path] is an alias or anchor.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('''
+  /// - 0 # comment 0
+  /// # comment A
+  /// - 1 # comment 1
+  /// # comment B
+  /// - 2 # comment 2
+  /// ''');
+  /// doc.remove([1]);
+  /// ```
+  ///
+  /// **Expected Result:**
+  /// ```dart
+  /// '''
+  /// - 0 # comment 0
+  /// # comment A
+  /// # comment B
+  /// - 2 # comment 2
+  /// '''
+  /// ```
+  YamlNode remove(Iterable<Object?> path) {
+    late SourceEdit edit;
+    late YamlNode expectedNode;
+    final nodeToRemove = _traverse(path, checkAlias: true);
+
+    if (path.isEmpty) {
+      edit = SourceEdit(0, _yaml.length, '');
+      expectedNode = wrapAsYamlNode(null);
+
+      /// Parsing an empty YAML document returns YamlScalar with value `null`.
+      _performEdit(edit, path, expectedNode);
+      return nodeToRemove;
+    }
+
+    final pathAsList = path.toList();
+    final collectionPath = pathAsList.take(path.length - 1);
+    final keyOrIndex = pathAsList.last;
+    final parentNode = _traverse(collectionPath);
+
+    if (parentNode is YamlList) {
+      edit = removeInList(this, parentNode, keyOrIndex as int);
+      expectedNode = wrapAsYamlNode(
+        [...parentNode.nodes]..removeAt(keyOrIndex),
+      );
+    } else if (parentNode is YamlMap) {
+      edit = removeInMap(this, parentNode, keyOrIndex);
+
+      expectedNode =
+          updatedYamlMap(parentNode, (nodes) => nodes.remove(keyOrIndex));
+    }
+
+    _performEdit(edit, collectionPath, expectedNode);
+
+    return nodeToRemove;
+  }
+
+  /// Traverses down [path] to return the [YamlNode] at [path] if successful.
+  ///
+  /// If no [YamlNode]s exist at [path], the result of invoking the [orElse]
+  /// function is returned.
+  ///
+  /// If [orElse] is omitted, it defaults to throwing a [PathError].
+  ///
+  /// If [checkAlias] is `true`, throw [AliasException] if an aliased node is
+  /// encountered.
+  YamlNode _traverse(Iterable<Object?> path,
+      {bool checkAlias = false, YamlNode Function()? orElse}) {
+    if (path.isEmpty) return _contents;
+
+    var currentNode = _contents;
+    final pathList = path.toList();
+
+    for (var i = 0; i < pathList.length; i++) {
+      final keyOrIndex = pathList[i];
+
+      if (checkAlias && _aliases.contains(currentNode)) {
+        throw AliasException(path, currentNode);
+      }
+
+      if (currentNode is YamlList) {
+        final list = currentNode;
+        if (!isValidIndex(keyOrIndex, list.length)) {
+          return _pathErrorOrElse(path, path.take(i + 1), list, orElse);
+        }
+
+        currentNode = list.nodes[keyOrIndex as int];
+      } else if (currentNode is YamlMap) {
+        final map = currentNode;
+
+        if (!containsKey(map, keyOrIndex)) {
+          return _pathErrorOrElse(path, path.take(i + 1), map, orElse);
+        }
+        final keyNode = getKeyNode(map, keyOrIndex);
+
+        if (checkAlias) {
+          if (_aliases.contains(keyNode)) throw AliasException(path, keyNode);
+        }
+
+        currentNode = map.nodes[keyNode]!;
+      } else {
+        return _pathErrorOrElse(path, path.take(i + 1), currentNode, orElse);
+      }
+    }
+
+    if (checkAlias) _assertNoChildAlias(path, currentNode);
+
+    return currentNode;
+  }
+
+  /// Throws a [PathError] if [orElse] is not provided, returns the result
+  /// of invoking the [orElse] function otherwise.
+  YamlNode _pathErrorOrElse(Iterable<Object?> path, Iterable<Object?> subPath,
+      YamlNode parent, YamlNode Function()? orElse) {
+    if (orElse == null) throw PathError(path, subPath, parent);
+    return orElse();
+  }
+
+  /// Asserts that [node] and none its children are aliases
+  void _assertNoChildAlias(Iterable<Object?> path, [YamlNode? node]) {
+    if (node == null) return _assertNoChildAlias(path, _traverse(path));
+    if (_aliases.contains(node)) throw AliasException(path, node);
+
+    if (node is YamlScalar) return;
+
+    if (node is YamlList) {
+      for (var i = 0; i < node.length; i++) {
+        final updatedPath = [...path, i];
+        _assertNoChildAlias(updatedPath, node.nodes[i]);
+      }
+    }
+
+    if (node is YamlMap) {
+      final keyList = node.keys.toList();
+      for (var i = 0; i < node.length; i++) {
+        final updatedPath = [...path, keyList[i]];
+        if (_aliases.contains(keyList[i])) {
+          throw AliasException(path, keyList[i] as YamlNode);
+        }
+        _assertNoChildAlias(updatedPath, node.nodes[keyList[i]]);
+      }
+    }
+  }
+
+  /// Traverses down the provided [path] to return the [YamlList] at [path].
+  ///
+  /// Convenience function to ensure that a [YamlList] is returned.
+  ///
+  /// Throws [ArgumentError] if the element at the given path is not a
+  /// [YamlList] or if the path is invalid. If [checkAlias] is `true`, and an
+  /// aliased node is encountered along [path], an [AliasException] will be
+  /// thrown.
+  YamlList _traverseToList(Iterable<Object?> path, {bool checkAlias = false}) {
+    final possibleList = _traverse(path, checkAlias: checkAlias);
+
+    if (possibleList is YamlList) {
+      return possibleList;
+    } else {
+      throw PathError.unexpected(
+          path, 'Path $path does not point to a YamlList!');
+    }
+  }
+
+  /// Utility method to replace the substring of [_yaml] according to [edit].
+  ///
+  /// When [_yaml] is modified with this method, the resulting string is parsed
+  /// and reloaded and traversed down [path] to ensure that the reloaded YAML
+  /// tree is equal to our expectations by deep equality of values. Throws an
+  /// [AssertionError] if the two trees do not match.
+  void _performEdit(
+      SourceEdit edit, Iterable<Object?> path, YamlNode expectedNode) {
+    final expectedTree = _deepModify(_contents, path, [], expectedNode);
+    final initialYaml = _yaml;
+    _yaml = edit.apply(_yaml);
+
+    try {
+      _initialize();
+    } on YamlException {
+      throw createAssertionError(
+          'Failed to produce valid YAML after modification.',
+          initialYaml,
+          _yaml);
+    }
+
+    final actualTree = withYamlWarningCallback(() => loadYamlNode(_yaml));
+    if (!deepEquals(actualTree, expectedTree)) {
+      throw createAssertionError(
+          'Modification did not result in expected result.',
+          initialYaml,
+          _yaml);
+    }
+
+    _contents = actualTree;
+    _edits.add(edit);
+  }
+
+  /// Utility method to produce an updated YAML tree equivalent to converting
+  /// the [YamlNode] at [path] to be [expectedNode]. [subPath] holds the portion
+  /// of [path] that has been traversed thus far.
+  ///
+  /// Throws a [PathError] if path is invalid.
+  ///
+  /// When called, it creates a new [YamlNode] of the same type as [tree], and
+  /// copies its children over, except for the child that is on the path. Doing
+  /// so allows us to "update" the immutable [YamlNode] without having to clone
+  /// the whole tree.
+  ///
+  /// [SourceSpan]s in this new tree are not guaranteed to be accurate.
+  YamlNode _deepModify(YamlNode tree, Iterable<Object?> path,
+      Iterable<Object?> subPath, YamlNode expectedNode) {
+    RangeError.checkValueInInterval(subPath.length, 0, path.length);
+
+    if (path.length == subPath.length) return expectedNode;
+
+    final keyOrIndex = path.elementAt(subPath.length);
+
+    if (tree is YamlList) {
+      if (!isValidIndex(keyOrIndex, tree.length)) {
+        throw PathError(path, subPath, tree);
+      }
+
+      return wrapAsYamlNode([...tree.nodes]..[keyOrIndex as int] = _deepModify(
+          tree.nodes[keyOrIndex],
+          path,
+          path.take(subPath.length + 1),
+          expectedNode));
+    }
+
+    if (tree is YamlMap) {
+      return updatedYamlMap(
+          tree,
+          (nodes) => nodes[keyOrIndex] = _deepModify(
+              nodes[keyOrIndex] as YamlNode,
+              path,
+              path.take(subPath.length + 1),
+              expectedNode));
+    }
+
+    /// Should not ever reach here.
+    throw PathError(path, subPath, tree);
+  }
+}
diff --git a/pkgs/yaml_edit/lib/src/equality.dart b/pkgs/yaml_edit/lib/src/equality.dart
new file mode 100644
index 0000000..0c6a952
--- /dev/null
+++ b/pkgs/yaml_edit/lib/src/equality.dart
@@ -0,0 +1,116 @@
+// 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:collection';
+
+import 'package:collection/collection.dart';
+import 'package:yaml/yaml.dart';
+
+/// Creates a map that uses our custom [deepEquals] and [deepHashCode] functions
+/// to determine equality.
+Map<K, V> deepEqualsMap<K, V>() =>
+    LinkedHashMap(equals: deepEquals, hashCode: deepHashCode);
+
+/// Compares two [Object]s for deep equality. This implementation differs from
+/// `package:yaml`'s deep equality notation by allowing for comparison of
+/// non-scalar map keys.
+bool deepEquals(dynamic obj1, dynamic obj2) {
+  if (obj1 is YamlNode) obj1 = obj1.value;
+  if (obj2 is YamlNode) obj2 = obj2.value;
+
+  if (obj1 is Map && obj2 is Map) {
+    return mapDeepEquals(obj1, obj2);
+  }
+
+  if (obj1 is List && obj2 is List) {
+    return listDeepEquals(obj1, obj2);
+  }
+
+  return obj1 == obj2;
+}
+
+/// Compares two [List]s for deep equality.
+bool listDeepEquals(List list1, List list2) {
+  if (list1.length != list2.length) return false;
+
+  if (list1 is YamlList) list1 = list1.nodes;
+  if (list2 is YamlList) list2 = list2.nodes;
+
+  for (var i = 0; i < list1.length; i++) {
+    if (!deepEquals(list1[i], list2[i])) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+/// Compares two [Map]s for deep equality. Differs from `package:yaml`'s deep
+/// equality notation by allowing for comparison of non-scalar map keys.
+bool mapDeepEquals(Map map1, Map map2) {
+  if (map1.length != map2.length) return false;
+
+  if (map1 is YamlList) map1 = (map1 as YamlMap).nodes;
+  if (map2 is YamlList) map2 = (map2 as YamlMap).nodes;
+
+  return map1.keys.every((key) {
+    if (!containsKey(map2, key)) return false;
+
+    /// Because two keys may be equal by deep equality but using one key on the
+    /// other map might not get a hit since they may not be both using our
+    /// [deepEqualsMap].
+    final key2 = getKey(map2, key);
+
+    if (!deepEquals(map1[key], map2[key2])) {
+      return false;
+    }
+
+    return true;
+  });
+}
+
+/// Returns a hashcode for [value] such that structures that are equal by
+/// [deepEquals] will have the same hash code.
+int deepHashCode(Object? value) {
+  if (value is Map) {
+    const equality = UnorderedIterableEquality();
+    return equality.hash(value.keys.map(deepHashCode)) ^
+        equality.hash(value.values.map(deepHashCode));
+  } else if (value is Iterable) {
+    return const IterableEquality().hash(value.map(deepHashCode));
+  } else if (value is YamlScalar) {
+    return (value.value as Object?).hashCode;
+  }
+
+  return value.hashCode;
+}
+
+/// Returns the [YamlNode] corresponding to the provided [key].
+YamlNode getKeyNode(YamlMap map, Object? key) {
+  return map.nodes.keys.firstWhere((node) => deepEquals(node, key)) as YamlNode;
+}
+
+/// Returns the [YamlNode] after the [YamlNode] corresponding to the provided
+/// [key].
+YamlNode? getNextKeyNode(YamlMap map, Object? key) {
+  final keyIterator = map.nodes.keys.iterator;
+  while (keyIterator.moveNext()) {
+    if (deepEquals(keyIterator.current, key) && keyIterator.moveNext()) {
+      return keyIterator.current as YamlNode?;
+    }
+  }
+
+  return null;
+}
+
+/// Returns the key in [map] that is equal to the provided [key] by the notion
+/// of deep equality.
+Object? getKey(Map map, Object? key) {
+  return map.keys.firstWhere((k) => deepEquals(k, key));
+}
+
+/// Checks if [map] has any keys equal to the provided [key] by deep equality.
+bool containsKey(Map map, Object? key) {
+  return map.keys.where((node) => deepEquals(node, key)).isNotEmpty;
+}
diff --git a/pkgs/yaml_edit/lib/src/errors.dart b/pkgs/yaml_edit/lib/src/errors.dart
new file mode 100644
index 0000000..0e60dd8
--- /dev/null
+++ b/pkgs/yaml_edit/lib/src/errors.dart
@@ -0,0 +1,99 @@
+// 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:meta/meta.dart';
+import 'package:yaml/yaml.dart';
+
+/// Error thrown when a function is passed an invalid path.
+@sealed
+class PathError extends ArgumentError {
+  /// The full path that caused the error
+  final Iterable<Object?> path;
+
+  /// The subpath that caused the error
+  final Iterable<Object?> subPath;
+
+  /// The last element of [path] that could be traversed.
+  YamlNode? parent;
+
+  PathError(this.path, this.subPath, this.parent, [String? message])
+      : super.value(subPath, 'path', message);
+
+  PathError.unexpected(this.path, String message)
+      : subPath = path,
+        super(message);
+
+  @override
+  String toString() {
+    if (message == null) {
+      var errorMessage = 'Failed to traverse to subpath $subPath!';
+
+      if (subPath.isNotEmpty) {
+        errorMessage +=
+            ' Parent $parent does not contain key or index ${subPath.last}';
+      }
+
+      return 'Invalid path: $path. $errorMessage.';
+    }
+
+    return 'Invalid path: $path. $message';
+  }
+}
+
+/// Exception thrown when the path contains an alias along the way.
+///
+/// When a path contains an aliased node, the behavior becomes less well-defined
+/// because we cannot be certain if the user wishes for the change to propagate
+/// throughout all the other aliased nodes, or if the user wishes for only that
+/// particular node to be modified. As such, [AliasException] reflects the
+/// detection that our change will impact an alias, and we do not intend on
+/// supporting such changes for the foreseeable future.
+@sealed
+class AliasException extends FormatException {
+  /// The path that caused the error
+  final Iterable<Object?> path;
+
+  /// The anchor node of the alias
+  final YamlNode anchor;
+
+  AliasException(this.path, this.anchor)
+      : super('Encountered an alias node along $path! '
+            'Alias nodes are nodes that refer to a previously serialized '
+            'nodes, and are denoted by either the "*" or the "&" indicators in '
+            'the original YAML. As the resulting behavior of mutations on '
+            'these nodes is not well-defined, the operation will not be '
+            'supported by this library.\n\n'
+            '${anchor.span.message('The alias was first defined here.')}');
+}
+
+/// Error thrown when an assertion about the YAML fails. Extends
+/// [AssertionError] to override the [toString] method for pretty printing.
+class _YamlAssertionError extends AssertionError {
+  _YamlAssertionError(super.message);
+
+  @override
+  String toString() {
+    if (message != null) {
+      return 'Assertion failed: $message';
+    }
+    return 'Assertion failed';
+  }
+}
+
+/// Throws an [AssertionError] with the given [message], and format
+/// [oldYaml] and [newYaml] for information.
+Error createAssertionError(String message, String oldYaml, String newYaml) {
+  return _YamlAssertionError('''
+(package:yaml_edit) $message
+
+# YAML before edit:
+> ${oldYaml.replaceAll('\n', '\n> ')}
+
+# YAML after edit:
+> ${newYaml.replaceAll('\n', '\n> ')}
+
+Please file an issue at:
+https://github.com/dart-lang/yaml_edit/issues/new?labels=bug
+''');
+}
diff --git a/pkgs/yaml_edit/lib/src/list_mutations.dart b/pkgs/yaml_edit/lib/src/list_mutations.dart
new file mode 100644
index 0000000..17da6dd
--- /dev/null
+++ b/pkgs/yaml_edit/lib/src/list_mutations.dart
@@ -0,0 +1,403 @@
+// 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:yaml/yaml.dart';
+
+import 'editor.dart';
+import 'source_edit.dart';
+import 'strings.dart';
+import 'utils.dart';
+import 'wrap.dart';
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of setting the element at [index] to [newValue] when
+/// re-parsed.
+SourceEdit updateInList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode newValue) {
+  RangeError.checkValueInInterval(index, 0, list.length - 1);
+
+  final currValue = list.nodes[index];
+  var offset = currValue.span.start.offset;
+  final yaml = yamlEdit.toString();
+  String valueString;
+
+  /// We do not use [_formatNewBlock] since we want to only replace the contents
+  /// of this node while preserving comments/whitespace, while [_formatNewBlock]
+  /// produces a string representation of a new node.
+  if (list.style == CollectionStyle.BLOCK) {
+    final listIndentation = getListIndentation(yaml, list);
+    final indentation = listIndentation + getIndentation(yamlEdit);
+    final lineEnding = getLineEnding(yaml);
+    valueString =
+        yamlEncodeBlock(wrapAsYamlNode(newValue), indentation, lineEnding);
+
+    /// We prefer the compact nested notation for collections.
+    ///
+    /// By virtue of [yamlEncodeBlockString], collections automatically
+    /// have the necessary line endings.
+    if ((newValue is List && (newValue as List).isNotEmpty) ||
+        (newValue is Map && (newValue as Map).isNotEmpty)) {
+      valueString = valueString.substring(indentation);
+    } else if (currValue.collectionStyle == CollectionStyle.BLOCK) {
+      valueString += lineEnding;
+    }
+
+    var end = getContentSensitiveEnd(currValue);
+    if (end <= offset) {
+      offset++;
+      end = offset;
+      valueString = ' $valueString';
+    }
+
+    return SourceEdit(offset, end - offset, valueString);
+  } else {
+    valueString = yamlEncodeFlow(newValue);
+    return SourceEdit(offset, currValue.span.length, valueString);
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of appending [item] to the list.
+SourceEdit appendIntoList(YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  if (list.style == CollectionStyle.FLOW) {
+    return _appendToFlowList(yamlEdit, list, item);
+  } else {
+    return _appendToBlockList(yamlEdit, list, item);
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of inserting [item] to the list at [index].
+SourceEdit insertInList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode item) {
+  RangeError.checkValueInInterval(index, 0, list.length);
+
+  /// We call the append method if the user wants to append it to the end of the
+  /// list because appending requires different techniques.
+  if (index == list.length) {
+    return appendIntoList(yamlEdit, list, item);
+  } else {
+    if (list.style == CollectionStyle.FLOW) {
+      return _insertInFlowList(yamlEdit, list, index, item);
+    } else {
+      return _insertInBlockList(yamlEdit, list, index, item);
+    }
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of removing the element at [index] when re-parsed.
+SourceEdit removeInList(YamlEditor yamlEdit, YamlList list, int index) {
+  final nodeToRemove = list.nodes[index];
+
+  if (list.style == CollectionStyle.FLOW) {
+    return _removeFromFlowList(yamlEdit, list, nodeToRemove, index);
+  } else {
+    return _removeFromBlockList(yamlEdit, list, nodeToRemove, index);
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of addition [item] into [list], noting that this is a
+/// flow list.
+SourceEdit _appendToFlowList(
+    YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  final valueString = _formatNewFlow(list, item, true);
+  return SourceEdit(list.span.end.offset - 1, 0, valueString);
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of addition [item] into [list], noting that this is a
+/// block list.
+SourceEdit _appendToBlockList(
+    YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  var (indentSize, valueToIndent) = _formatNewBlock(yamlEdit, list, item);
+  var formattedValue = '${' ' * indentSize}$valueToIndent';
+
+  final yaml = yamlEdit.toString();
+  var offset = list.span.end.offset;
+
+  // Adjusts offset to after the trailing newline of the last entry, if it
+  // exists
+  if (list.isNotEmpty) {
+    final lastValueSpanEnd = list.nodes.last.span.end.offset;
+    final nextNewLineIndex = yaml.indexOf('\n', lastValueSpanEnd - 1);
+    if (nextNewLineIndex == -1) {
+      formattedValue = getLineEnding(yaml) + formattedValue;
+    } else {
+      offset = nextNewLineIndex + 1;
+    }
+  }
+
+  return SourceEdit(offset, 0, formattedValue);
+}
+
+/// Formats [item] into a new node for block lists.
+(int indentSize, String valueStringToIndent) _formatNewBlock(
+    YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  final yaml = yamlEdit.toString();
+  final listIndentation = getListIndentation(yaml, list);
+  final newIndentation = listIndentation + getIndentation(yamlEdit);
+  final lineEnding = getLineEnding(yaml);
+
+  var valueString = yamlEncodeBlock(item, newIndentation, lineEnding);
+  if (isCollection(item) && !isFlowYamlCollectionNode(item) && !isEmpty(item)) {
+    valueString = valueString.substring(newIndentation);
+  }
+
+  return (listIndentation, '- $valueString$lineEnding');
+}
+
+/// Formats [item] into a new node for flow lists.
+String _formatNewFlow(YamlList list, YamlNode item, [bool isLast = false]) {
+  var valueString = yamlEncodeFlow(item);
+  if (list.isNotEmpty) {
+    if (isLast) {
+      valueString = ', $valueString';
+    } else {
+      valueString += ', ';
+    }
+  }
+
+  return valueString;
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of inserting [item] into [list] at [index], noting that
+/// this is a block list.
+///
+/// [index] should be non-negative and less than or equal to `list.length`.
+SourceEdit _insertInBlockList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode item) {
+  RangeError.checkValueInInterval(index, 0, list.length);
+
+  if (index == list.length) return _appendToBlockList(yamlEdit, list, item);
+
+  var (indentSize, formattedValue) = _formatNewBlock(yamlEdit, list, item);
+
+  final currNode = list.nodes[index];
+  final currNodeStart = currNode.span.start.offset;
+  final yaml = yamlEdit.toString();
+
+  final currSequenceOffset = yaml.lastIndexOf('-', currNodeStart - 1);
+
+  final (isNested, offset) = _isNestedInBlockList(currSequenceOffset, yaml);
+
+  /// We have to get rid of the left indentation applied by default
+  if (isNested && index == 0) {
+    /// The [insertionIndex] will be equal to the start of
+    /// [currentSequenceOffset] of the element we are inserting before in most
+    /// cases.
+    ///
+    /// Example:
+    ///
+    ///   - - value
+    ///     ^ Inserting before this and we get rid of indent
+    ///
+    /// If not, we need to account for the space between them that is not an
+    /// indent.
+    ///
+    /// Example:
+    ///
+    ///   -   - value
+    ///       ^ Inserting before this and we get rid of indent. But also account
+    ///         for space in between
+    final leftPad = currSequenceOffset - offset;
+    final padding = ' ' * leftPad;
+
+    final indent = ' ' * (indentSize - leftPad);
+
+    // Give the indent to the first element
+    formattedValue = '$padding${formattedValue.trimLeft()}$indent';
+  } else {
+    final indent = ' ' * indentSize; // Calculate indent normally
+    formattedValue = '$indent$formattedValue';
+  }
+
+  return SourceEdit(offset, 0, formattedValue);
+}
+
+/// Determines if the list containing an element is nested within another list.
+/// The [currentSequenceOffset] indicates the index of the element's `-` and
+/// [yaml] represents the entire yaml document.
+///
+/// ```yaml
+/// # Returns true
+/// - - value
+///
+/// # Returns true
+/// -       - value
+///
+/// # Returns false
+/// key:
+///   - value
+///
+/// # Returns false. Even though nested, a "\n" precedes the previous "-"
+/// -
+///   - value
+/// ```
+(bool isNested, int offset) _isNestedInBlockList(
+    int currentSequenceOffset, String yaml) {
+  final startIndex = currentSequenceOffset - 1;
+
+  /// Indicates the element we are inserting before is at index `0` of the list
+  /// at the root of the yaml
+  ///
+  /// Example:
+  ///
+  /// - foo
+  /// ^ Inserting before this
+  if (startIndex < 0) return (false, 0);
+
+  final newLineStart = yaml.lastIndexOf('\n', startIndex);
+  final seqStart = yaml.lastIndexOf('-', startIndex);
+
+  /// Indicates that a `\n` is closer to the last `-`. Meaning this list is not
+  /// nested.
+  ///
+  /// Example:
+  ///
+  ///   key:
+  ///     - value
+  ///     ^ Inserting before this and we need to keep the indent.
+  ///
+  /// Also this list may be nested but the nested list starts its indent after
+  /// a new line.
+  ///
+  /// Example:
+  ///
+  ///   -
+  ///     - value
+  ///     ^ Inserting before this and we need to keep the indent.
+  if (newLineStart >= seqStart) {
+    return (false, newLineStart + 1);
+  }
+
+  return (true, seqStart + 2); // Inclusive of space
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of inserting [item] into [list] at [index], noting that
+/// this is a flow list.
+///
+/// [index] should be non-negative and less than or equal to `list.length`.
+SourceEdit _insertInFlowList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode item) {
+  RangeError.checkValueInInterval(index, 0, list.length);
+
+  if (index == list.length) return _appendToFlowList(yamlEdit, list, item);
+
+  final formattedValue = _formatNewFlow(list, item);
+
+  final yaml = yamlEdit.toString();
+  final currNode = list.nodes[index];
+  final currNodeStart = currNode.span.start.offset;
+  var start = yaml.lastIndexOf(RegExp(r',|\['), currNodeStart - 1) + 1;
+  if (yaml[start] == ' ') start++;
+
+  return SourceEdit(start, 0, formattedValue);
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of removing [nodeToRemove] from [list], noting that this
+/// is a block list.
+///
+/// [index] should be non-negative and less than or equal to `list.length`.
+SourceEdit _removeFromBlockList(
+    YamlEditor yamlEdit, YamlList list, YamlNode nodeToRemove, int index) {
+  RangeError.checkValueInInterval(index, 0, list.length - 1);
+
+  var end = getContentSensitiveEnd(nodeToRemove);
+
+  /// If we are removing the last element in a block list, convert it into a
+  /// flow empty list.
+  if (list.length == 1) {
+    final start = list.span.start.offset;
+
+    return SourceEdit(start, end - start, '[]');
+  }
+
+  final yaml = yamlEdit.toString();
+  final span = nodeToRemove.span;
+
+  /// Adjust the end to clear the new line after the end too.
+  ///
+  /// We do this because we suspect that our users will want the inline
+  /// comments to disappear too.
+  final nextNewLine = yaml.indexOf('\n', end);
+  if (nextNewLine != -1) {
+    end = nextNewLine + 1;
+  }
+
+  /// If the value is empty
+  if (span.length == 0) {
+    var start = span.start.offset;
+    return SourceEdit(start, end - start, '');
+  }
+
+  /// -1 accounts for the fact that the content can start with a dash
+  var start = yaml.lastIndexOf('-', span.start.offset - 1);
+
+  /// Check if there is a `-` before the node
+  if (start > 0) {
+    final lastHyphen = yaml.lastIndexOf('-', start - 1);
+    final lastNewLine = yaml.lastIndexOf('\n', start - 1);
+    if (lastHyphen > lastNewLine) {
+      start = lastHyphen + 2;
+
+      /// If there is a `-` before the node, we need to check if we have
+      /// to update the indentation of the next node.
+      if (index < list.length - 1) {
+        /// Since [end] is currently set to the next new line after the current
+        /// node, check if we see a possible comment first, or a hyphen first.
+        /// Note that no actual content can appear here.
+        ///
+        /// We check this way because the start of a span in a block list is
+        /// the start of its value, and checking from the back leaves us
+        /// easily confused if there are comments that have dashes in them.
+        final nextHash = yaml.indexOf('#', end);
+        final nextHyphen = yaml.indexOf('-', end);
+        final nextNewLine = yaml.indexOf('\n', end);
+
+        /// If [end] is on the same line as the hyphen of the next node
+        if ((nextHash == -1 || nextHyphen < nextHash) &&
+            nextHyphen < nextNewLine) {
+          end = nextHyphen;
+        }
+      }
+    } else if (lastNewLine > lastHyphen) {
+      start = lastNewLine + 1;
+    }
+  }
+
+  return SourceEdit(start, end - start, '');
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
+/// achieve the effect of removing [nodeToRemove] from [list], noting that this
+/// is a flow list.
+///
+/// [index] should be non-negative and less than or equal to `list.length`.
+SourceEdit _removeFromFlowList(
+    YamlEditor yamlEdit, YamlList list, YamlNode nodeToRemove, int index) {
+  RangeError.checkValueInInterval(index, 0, list.length - 1);
+
+  final span = nodeToRemove.span;
+  final yaml = yamlEdit.toString();
+  var start = span.start.offset;
+  var end = span.end.offset;
+
+  if (index == 0) {
+    start = yaml.lastIndexOf('[', start - 1) + 1;
+    if (index == list.length - 1) {
+      end = yaml.indexOf(']', end);
+    } else {
+      end = yaml.indexOf(',', end) + 1;
+    }
+  } else {
+    start = yaml.lastIndexOf(',', start - 1);
+  }
+
+  return SourceEdit(start, end - start, '');
+}
diff --git a/pkgs/yaml_edit/lib/src/map_mutations.dart b/pkgs/yaml_edit/lib/src/map_mutations.dart
new file mode 100644
index 0000000..46e8c79
--- /dev/null
+++ b/pkgs/yaml_edit/lib/src/map_mutations.dart
@@ -0,0 +1,257 @@
+// 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:yaml/yaml.dart';
+
+import 'editor.dart';
+import 'equality.dart';
+import 'source_edit.dart';
+import 'strings.dart';
+import 'utils.dart';
+import 'wrap.dart';
+
+/// Performs the string operation on [yamlEdit] to achieve the effect of setting
+/// the element at [key] to [newValue] when re-parsed.
+SourceEdit updateInMap(
+    YamlEditor yamlEdit, YamlMap map, Object? key, YamlNode newValue) {
+  if (!containsKey(map, key)) {
+    final keyNode = wrapAsYamlNode(key);
+
+    if (map.style == CollectionStyle.FLOW) {
+      return _addToFlowMap(yamlEdit, map, keyNode, newValue);
+    } else {
+      return _addToBlockMap(yamlEdit, map, keyNode, newValue);
+    }
+  } else {
+    if (map.style == CollectionStyle.FLOW) {
+      return _replaceInFlowMap(yamlEdit, map, key, newValue);
+    } else {
+      return _replaceInBlockMap(yamlEdit, map, key, newValue);
+    }
+  }
+}
+
+/// Performs the string operation on [yamlEdit] to achieve the effect of
+/// removing the element at [key] when re-parsed.
+SourceEdit removeInMap(YamlEditor yamlEdit, YamlMap map, Object? key) {
+  assert(containsKey(map, key));
+  final keyNode = getKeyNode(map, key);
+  final valueNode = map.nodes[keyNode]!;
+
+  if (map.style == CollectionStyle.FLOW) {
+    return _removeFromFlowMap(yamlEdit, map, keyNode, valueNode);
+  } else {
+    return _removeFromBlockMap(yamlEdit, map, keyNode, valueNode);
+  }
+}
+
+/// Performs the string operation on [yamlEdit] to achieve the effect of adding
+/// the [key]:[newValue] pair when reparsed, bearing in mind that this is a
+/// block map.
+SourceEdit _addToBlockMap(
+    YamlEditor yamlEdit, YamlMap map, Object key, YamlNode newValue) {
+  final yaml = yamlEdit.toString();
+  final newIndentation =
+      getMapIndentation(yaml, map) + getIndentation(yamlEdit);
+  final keyString = yamlEncodeFlow(wrapAsYamlNode(key));
+  final lineEnding = getLineEnding(yaml);
+
+  var formattedValue = ' ' * getMapIndentation(yaml, map);
+  var offset = map.span.end.offset;
+
+  final insertionIndex = getMapInsertionIndex(map, keyString);
+
+  if (map.isNotEmpty) {
+    /// Adjusts offset to after the trailing newline of the last entry, if it
+    /// exists
+    if (insertionIndex == map.length) {
+      final lastValueSpanEnd = getContentSensitiveEnd(map.nodes.values.last);
+      final nextNewLineIndex = yaml.indexOf('\n', lastValueSpanEnd);
+
+      if (nextNewLineIndex != -1) {
+        offset = nextNewLineIndex + 1;
+      } else {
+        formattedValue = lineEnding + formattedValue;
+      }
+    } else {
+      final keyAtIndex = map.nodes.keys.toList()[insertionIndex] as YamlNode;
+      final keySpanStart = keyAtIndex.span.start.offset;
+      final prevNewLineIndex = yaml.lastIndexOf('\n', keySpanStart);
+
+      offset = prevNewLineIndex + 1;
+    }
+  }
+
+  var valueString = yamlEncodeBlock(newValue, newIndentation, lineEnding);
+  if (isCollection(newValue) &&
+      !isFlowYamlCollectionNode(newValue) &&
+      !isEmpty(newValue)) {
+    formattedValue += '$keyString:$lineEnding$valueString$lineEnding';
+  } else {
+    formattedValue += '$keyString: $valueString$lineEnding';
+  }
+
+  return SourceEdit(offset, 0, formattedValue);
+}
+
+/// Performs the string operation on [yamlEdit] to achieve the effect of adding
+/// the [keyNode]:[newValue] pair when reparsed, bearing in mind that this is a
+/// flow map.
+SourceEdit _addToFlowMap(
+    YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode newValue) {
+  final keyString = yamlEncodeFlow(keyNode);
+  final valueString = yamlEncodeFlow(newValue);
+
+  // The -1 accounts for the closing bracket.
+  if (map.isEmpty) {
+    return SourceEdit(map.span.end.offset - 1, 0, '$keyString: $valueString');
+  }
+
+  final insertionIndex = getMapInsertionIndex(map, keyString);
+
+  if (insertionIndex == map.length) {
+    return SourceEdit(map.span.end.offset - 1, 0, ', $keyString: $valueString');
+  }
+
+  final insertionOffset =
+      (map.nodes.keys.toList()[insertionIndex] as YamlNode).span.start.offset;
+
+  return SourceEdit(insertionOffset, 0, '$keyString: $valueString, ');
+}
+
+/// Performs the string operation on [yamlEdit] to achieve the effect of
+/// replacing the value at [key] with [newValue] when reparsed, bearing in mind
+/// that this is a block map.
+SourceEdit _replaceInBlockMap(
+    YamlEditor yamlEdit, YamlMap map, Object? key, YamlNode newValue) {
+  final yaml = yamlEdit.toString();
+  final lineEnding = getLineEnding(yaml);
+  final newIndentation =
+      getMapIndentation(yaml, map) + getIndentation(yamlEdit);
+
+  final keyNode = getKeyNode(map, key);
+  var valueAsString =
+      yamlEncodeBlock(wrapAsYamlNode(newValue), newIndentation, lineEnding);
+  if (isCollection(newValue) &&
+      !isFlowYamlCollectionNode(newValue) &&
+      !isEmpty(newValue)) {
+    valueAsString = lineEnding + valueAsString;
+  }
+
+  if (!valueAsString.startsWith(lineEnding)) {
+    // prepend whitespace to ensure there is space after colon.
+    valueAsString = ' $valueAsString';
+  }
+
+  /// +1 accounts for the colon
+  // TODO: What if here is a whitespace following the key, before the colon?
+  final start = keyNode.span.end.offset + 1;
+  var end = getContentSensitiveEnd(map.nodes[key]!);
+
+  /// `package:yaml` parses empty nodes in a way where the start/end of the
+  /// empty value node is the end of the key node, so we have to adjust for
+  /// this.
+  if (end < start) end = start;
+
+  return SourceEdit(start, end - start, valueAsString);
+}
+
+/// Performs the string operation on [yamlEdit] to achieve the effect of
+/// replacing the value at [key] with [newValue] when reparsed, bearing in mind
+/// that this is a flow map.
+SourceEdit _replaceInFlowMap(
+    YamlEditor yamlEdit, YamlMap map, Object? key, YamlNode newValue) {
+  final valueSpan = map.nodes[key]!.span;
+  final valueString = yamlEncodeFlow(newValue);
+
+  return SourceEdit(valueSpan.start.offset, valueSpan.length, valueString);
+}
+
+/// Performs the string operation on [yamlEdit] to achieve the effect of
+/// removing the [keyNode] from the map, bearing in mind that this is a block
+/// map.
+SourceEdit _removeFromBlockMap(
+    YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode valueNode) {
+  final keySpan = keyNode.span;
+  var end = getContentSensitiveEnd(valueNode);
+  final yaml = yamlEdit.toString();
+  final lineEnding = getLineEnding(yaml);
+
+  if (map.length == 1) {
+    final start = map.span.start.offset;
+    final nextNewLine = yaml.indexOf(lineEnding, end);
+    if (nextNewLine != -1) {
+      // Remove everything up to the next newline, this strips comments that
+      // follows on the same line as the value we're removing.
+      // It also ensures we consume colon when [valueNode.value] is `null`
+      // because there is no value (e.g. `key: \n`). Because [valueNode.span] in
+      // such cases point to the colon `:`.
+      end = nextNewLine;
+    } else {
+      // Remove everything until the end of the document, if there is no newline
+      end = yaml.length;
+    }
+    return SourceEdit(start, end - start, '{}');
+  }
+
+  var start = keySpan.start.offset;
+
+  /// Adjust the end to clear the new line after the end too.
+  ///
+  /// We do this because we suspect that our users will want the inline
+  /// comments to disappear too.
+  final nextNewLine = yaml.indexOf(lineEnding, end);
+  if (nextNewLine != -1) {
+    end = nextNewLine + lineEnding.length;
+  } else {
+    // Remove everything until the end of the document, if there is no newline
+    end = yaml.length;
+  }
+
+  final nextNode = getNextKeyNode(map, keyNode);
+
+  if (start > 0) {
+    final lastHyphen = yaml.lastIndexOf('-', start - 1);
+    final lastNewLine = yaml.lastIndexOf(lineEnding, start - 1);
+    if (lastHyphen > lastNewLine) {
+      start = lastHyphen + 2;
+
+      /// If there is a `-` before the node, and the end is on the same line
+      /// as the next node, we need to add the necessary offset to the end to
+      /// make sure the next node has the correct indentation.
+      if (nextNode != null &&
+          nextNode.span.start.offset - end <= nextNode.span.start.column) {
+        end += nextNode.span.start.column;
+      }
+    } else if (lastNewLine > lastHyphen) {
+      start = lastNewLine + lineEnding.length;
+    }
+  }
+
+  return SourceEdit(start, end - start, '');
+}
+
+/// Performs the string operation on [yamlEdit] to achieve the effect of
+/// removing the [keyNode] from the map, bearing in mind that this is a flow
+/// map.
+SourceEdit _removeFromFlowMap(
+    YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode valueNode) {
+  var start = keyNode.span.start.offset;
+  var end = valueNode.span.end.offset;
+  final yaml = yamlEdit.toString();
+
+  if (deepEquals(keyNode, map.keys.first)) {
+    start = yaml.lastIndexOf('{', start - 1) + 1;
+
+    if (deepEquals(keyNode, map.keys.last)) {
+      end = yaml.indexOf('}', end);
+    } else {
+      end = yaml.indexOf(',', end) + 1;
+    }
+  } else {
+    start = yaml.lastIndexOf(',', start - 1);
+  }
+
+  return SourceEdit(start, end - start, '');
+}
diff --git a/pkgs/yaml_edit/lib/src/source_edit.dart b/pkgs/yaml_edit/lib/src/source_edit.dart
new file mode 100644
index 0000000..d177a19
--- /dev/null
+++ b/pkgs/yaml_edit/lib/src/source_edit.dart
@@ -0,0 +1,133 @@
+// 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:meta/meta.dart';
+
+/// A class representing a change on a [String], intended to be compatible with
+/// `package:analysis_server`'s [SourceEdit].
+///
+/// For example, changing a string from
+/// ```
+/// foo: foobar
+/// ```
+/// to
+/// ```
+/// foo: barbar
+/// ```
+/// will be represented by
+/// `SourceEdit(offset: 4, length: 3, replacement: 'bar')`
+@sealed
+class SourceEdit {
+  /// The offset from the start of the string where the modification begins.
+  final int offset;
+
+  /// The length of the substring to be replaced.
+  final int length;
+
+  /// The replacement string to be used.
+  final String replacement;
+
+  /// Creates a new [SourceEdit] instance. [offset], [length] and [replacement]
+  /// must be non-null, and [offset] and [length] must be non-negative.
+  factory SourceEdit(int offset, int length, String replacement) =>
+      SourceEdit._(offset, length, replacement);
+
+  SourceEdit._(this.offset, this.length, this.replacement) {
+    RangeError.checkNotNegative(offset);
+    RangeError.checkNotNegative(length);
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other is SourceEdit) {
+      return offset == other.offset &&
+          length == other.length &&
+          replacement == other.replacement;
+    }
+
+    return false;
+  }
+
+  @override
+  int get hashCode => offset.hashCode ^ length.hashCode ^ replacement.hashCode;
+
+  /// Constructs a SourceEdit from JSON.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final edit = {
+  ///   'offset': 1,
+  ///   'length': 2,
+  ///   'replacement': 'replacement string'
+  /// };
+  ///
+  /// final sourceEdit = SourceEdit.fromJson(edit);
+  /// ```
+  factory SourceEdit.fromJson(Map<String, dynamic> json) {
+    final offset = json['offset'];
+    final length = json['length'];
+    final replacement = json['replacement'];
+
+    if (offset is int && length is int && replacement is String) {
+      return SourceEdit(offset, length, replacement);
+    }
+
+    throw const FormatException('Invalid JSON passed to SourceEdit');
+  }
+
+  /// Encodes this object as JSON-compatible structure.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// import 'dart:convert' show jsonEncode;
+  ///
+  /// final edit = SourceEdit(offset, length, 'replacement string');
+  /// final jsonString = jsonEncode(edit.toJson());
+  /// print(jsonString);
+  /// ```
+  Map<String, dynamic> toJson() {
+    return {'offset': offset, 'length': length, 'replacement': replacement};
+  }
+
+  @override
+  String toString() => 'SourceEdit($offset, $length, "$replacement")';
+
+  /// Applies a series of [SourceEdit]s to an original string, and return the
+  /// final output.
+  ///
+  /// [edits] should be in order i.e. the first [SourceEdit] in [edits] should
+  /// be the first edit applied to [original].
+  ///
+  /// **Example:**
+  /// ```dart
+  /// const original = 'YAML: YAML';
+  /// final sourceEdits = [
+  ///        SourceEdit(6, 4, "YAML Ain't Markup Language"),
+  ///        SourceEdit(6, 4, "YAML Ain't Markup Language"),
+  ///        SourceEdit(0, 4, "YAML Ain't Markup Language")
+  ///      ];
+  /// final result = SourceEdit.applyAll(original, sourceEdits);
+  /// ```
+  /// **Expected result:**
+  /// ```dart
+  /// "YAML Ain't Markup Language: YAML Ain't Markup Language Ain't Markup
+  /// Language"
+  /// ```
+  static String applyAll(String original, Iterable<SourceEdit> edits) {
+    return edits.fold(original, (current, edit) => edit.apply(current));
+  }
+
+  /// Applies one [SourceEdit]s to an original string, and return the final
+  /// output.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final edit = SourceEdit(4, 3, 'bar');
+  /// final originalString = 'foo: foobar';
+  /// print(edit.apply(originalString)); // 'foo: barbar'
+  /// ```
+  String apply(String original) {
+    return original.replaceRange(offset, offset + length, replacement);
+  }
+}
diff --git a/pkgs/yaml_edit/lib/src/strings.dart b/pkgs/yaml_edit/lib/src/strings.dart
new file mode 100644
index 0000000..1b85641
--- /dev/null
+++ b/pkgs/yaml_edit/lib/src/strings.dart
@@ -0,0 +1,366 @@
+// 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:collection/collection.dart';
+import 'package:yaml/yaml.dart';
+
+import 'utils.dart';
+
+/// Given [value], tries to format it into a plain string recognizable by YAML.
+///
+/// Not all values can be formatted into a plain string. If the string contains
+/// an escape sequence, it can only be detected when in a double-quoted
+/// sequence. Plain strings may also be misinterpreted by the YAML parser (e.g.
+/// ' null').
+///
+/// Returns `null` if [value] cannot be encoded as a plain string.
+String? _tryYamlEncodePlain(String value) {
+  /// If it contains a dangerous character we want to wrap the result with
+  /// double quotes because the double quoted style allows for arbitrary
+  /// strings with "\" escape sequences.
+  ///
+  /// See 7.3.1 Double-Quoted Style
+  /// https://yaml.org/spec/1.2/spec.html#id2787109
+  return isDangerousString(value) ? null : value;
+}
+
+/// Checks if [string] has unprintable characters according to
+/// [unprintableCharCodes].
+bool _hasUnprintableCharacters(String string) {
+  final codeUnits = string.codeUnits;
+
+  for (final key in unprintableCharCodes.keys) {
+    if (codeUnits.contains(key)) return true;
+  }
+
+  return false;
+}
+
+/// Generates a YAML-safe double-quoted string based on [string], escaping the
+/// list of characters as defined by the YAML 1.2 spec.
+///
+/// See 5.7 Escaped Characters https://yaml.org/spec/1.2/spec.html#id2776092
+String _yamlEncodeDoubleQuoted(String string) {
+  final buffer = StringBuffer();
+  for (final codeUnit in string.codeUnits) {
+    if (doubleQuoteEscapeChars[codeUnit] != null) {
+      buffer.write(doubleQuoteEscapeChars[codeUnit]);
+    } else {
+      buffer.writeCharCode(codeUnit);
+    }
+  }
+
+  return '"$buffer"';
+}
+
+/// Encodes [string] as YAML single quoted string.
+///
+/// Returns `null`, if the [string] can't be encoded as single-quoted string.
+/// This might happen if it contains line-breaks or [_hasUnprintableCharacters].
+///
+/// See: https://yaml.org/spec/1.2.2/#732-single-quoted-style
+String? _tryYamlEncodeSingleQuoted(String string) {
+  // If [string] contains a newline we'll use double quoted strings instead.
+  // Single quoted strings can represent newlines, but then we have to use an
+  // empty line (replace \n with \n\n). But since leading spaces following
+  // line breaks are ignored, we can't represent "\n ".
+  // Thus, if the string contains `\n` and we're asked to do single quoted,
+  // we'll fallback to a double quoted string.
+  if (_hasUnprintableCharacters(string) || string.contains('\n')) return null;
+
+  final result = string.replaceAll('\'', '\'\'');
+  return '\'$result\'';
+}
+
+/// Attempts to encode a [string] as a _YAML folded string_ and apply the
+/// appropriate _chomping indicator_.
+///
+/// Returns `null`, if the [string] cannot be encoded as a _YAML folded
+/// string_.
+///
+/// **Examples** of folded strings.
+/// ```yaml
+/// # With the "strip" chomping indicator
+/// key: >-
+///   my folded
+///   string
+///
+/// # With the "keep" chomping indicator
+/// key: >+
+///   my folded
+///   string
+/// ```
+///
+/// See: https://yaml.org/spec/1.2.2/#813-folded-style
+String? _tryYamlEncodeFolded(String string, int indentSize, String lineEnding) {
+  // A string that starts with space or newline followed by space can't be
+  // encoded in folded mode.
+  if (string.isEmpty || string.trim().length != string.length) return null;
+
+  if (_hasUnprintableCharacters(string)) return null;
+
+  // TODO: Are there other strings we can't encode in folded mode?
+
+  final indent = ' ' * indentSize;
+
+  /// Remove trailing `\n` & white-space to ease string folding
+  var trimmed = string.trimRight();
+  final stripped = string.substring(trimmed.length);
+
+  final trimmedSplit =
+      trimmed.replaceAll('\n', lineEnding + indent).split(lineEnding);
+
+  /// Try folding to match specification:
+  /// * https://yaml.org/spec/1.2.2/#65-line-folding
+  trimmed = trimmedSplit.reduceIndexed((index, previous, current) {
+    var updated = current;
+
+    /// If initially empty, this line holds only `\n` or white-space. This
+    /// tells us we don't need to apply an additional `\n`.
+    ///
+    /// See https://yaml.org/spec/1.2.2/#64-empty-lines
+    ///
+    /// If this line is not empty, we need to apply an additional `\n` if and
+    /// only if:
+    ///   1. The preceding line was non-empty too
+    ///   2. If the current line doesn't begin with white-space
+    ///
+    /// Such that we apply `\n` for `foo\nbar` but not `foo\n bar`.
+    if (current.trim().isNotEmpty &&
+        trimmedSplit[index - 1].trim().isNotEmpty &&
+        !current.replaceFirst(indent, '').startsWith(' ')) {
+      updated = lineEnding + updated;
+    }
+
+    /// Apply a `\n` by default.
+    return previous + lineEnding + updated;
+  });
+
+  return '>-\n'
+      '$indent$trimmed'
+      '${stripped.replaceAll('\n', lineEnding + indent)}';
+}
+
+/// Attempts to encode a [string] as a _YAML literal string_ and apply the
+/// appropriate _chomping indicator_.
+///
+/// Returns `null`, if the [string] cannot be encoded as a _YAML literal
+/// string_.
+///
+/// **Examples** of literal strings.
+/// ```yaml
+/// # With the "strip" chomping indicator
+/// key: |-
+///   my literal
+///   string
+///
+/// # Without chomping indicator
+/// key: |
+///   my literal
+///   string
+/// ```
+///
+/// See: https://yaml.org/spec/1.2.2/#812-literal-style
+String? _tryYamlEncodeLiteral(
+    String string, int indentSize, String lineEnding) {
+  if (string.isEmpty || string.trim().length != string.length) return null;
+
+  // A string that starts with space or newline followed by space can't be
+  // encoded in literal mode.
+  if (_hasUnprintableCharacters(string)) return null;
+
+  // TODO: Are there other strings we can't encode in literal mode?
+
+  final indent = ' ' * indentSize;
+
+  /// Simplest block style.
+  /// * https://yaml.org/spec/1.2.2/#812-literal-style
+  return '|-\n$indent${string.replaceAll('\n', lineEnding + indent)}';
+}
+
+/// Encodes a flow [YamlScalar] based on the provided [YamlScalar.style].
+///
+/// Falls back to [ScalarStyle.DOUBLE_QUOTED] if the [yamlScalar] cannot be
+/// encoded with the [YamlScalar.style] or with [ScalarStyle.PLAIN] when the
+/// [yamlScalar] is not a [String].
+String _yamlEncodeFlowScalar(YamlScalar yamlScalar) {
+  final YamlScalar(:value, :style) = yamlScalar;
+
+  if (value is! String) {
+    return value.toString();
+  }
+
+  switch (style) {
+    /// Only encode as double-quoted if it's a string.
+    case ScalarStyle.DOUBLE_QUOTED:
+      return _yamlEncodeDoubleQuoted(value);
+
+    case ScalarStyle.SINGLE_QUOTED:
+      return _tryYamlEncodeSingleQuoted(value) ??
+          _yamlEncodeDoubleQuoted(value);
+
+    /// Cast into [String] if [null] as this condition only returns [null]
+    /// for a [String] that can't be encoded.
+    default:
+      return _tryYamlEncodePlain(value) ?? _yamlEncodeDoubleQuoted(value);
+  }
+}
+
+/// Encodes a block [YamlScalar] based on the provided [YamlScalar.style].
+///
+/// Falls back to [ScalarStyle.DOUBLE_QUOTED] if the [yamlScalar] cannot be
+/// encoded with the [YamlScalar.style] provided.
+String _yamlEncodeBlockScalar(
+  YamlScalar yamlScalar,
+  int indentation,
+  String lineEnding,
+) {
+  final YamlScalar(:value, :style) = yamlScalar;
+  assertValidScalar(value);
+
+  if (value is! String) {
+    return value.toString();
+  }
+
+  switch (style) {
+    /// Prefer 'plain', fallback to "double quoted"
+    case ScalarStyle.PLAIN:
+      return _tryYamlEncodePlain(value) ?? _yamlEncodeDoubleQuoted(value);
+
+    // Prefer 'single quoted', fallback to "double quoted"
+    case ScalarStyle.SINGLE_QUOTED:
+      return _tryYamlEncodeSingleQuoted(value) ??
+          _yamlEncodeDoubleQuoted(value);
+
+    /// Prefer folded string, fallback to "double quoted"
+    case ScalarStyle.FOLDED:
+      return _tryYamlEncodeFolded(value, indentation, lineEnding) ??
+          _yamlEncodeDoubleQuoted(value);
+
+    /// Prefer literal string, fallback to "double quoted"
+    case ScalarStyle.LITERAL:
+      return _tryYamlEncodeLiteral(value, indentation, lineEnding) ??
+          _yamlEncodeDoubleQuoted(value);
+
+    /// Prefer plain, fallback to "double quoted"
+    default:
+      return _tryYamlEncodePlain(value) ?? _yamlEncodeDoubleQuoted(value);
+  }
+}
+
+/// Returns [value] with the necessary formatting applied in a flow context.
+///
+/// If [value] is a [YamlNode], we try to respect its [YamlScalar.style]
+/// parameter where possible. Certain cases make this impossible (e.g. a plain
+/// string scalar that starts with '>', a child having a block style
+/// parameters), in which case we will produce [value] with default styling
+/// options.
+String yamlEncodeFlow(YamlNode value) {
+  if (value is YamlList) {
+    final list = value.nodes;
+
+    final safeValues = list.map(yamlEncodeFlow);
+    return '[${safeValues.join(', ')}]';
+  } else if (value is YamlMap) {
+    final safeEntries = value.nodes.entries.map((entry) {
+      final safeKey = yamlEncodeFlow(entry.key as YamlNode);
+      final safeValue = yamlEncodeFlow(entry.value);
+      return '$safeKey: $safeValue';
+    });
+
+    return '{${safeEntries.join(', ')}}';
+  }
+
+  return _yamlEncodeFlowScalar(value as YamlScalar);
+}
+
+/// Returns [value] with the necessary formatting applied in a block context.
+String yamlEncodeBlock(
+  YamlNode value,
+  int indentation,
+  String lineEnding,
+) {
+  const additionalIndentation = 2;
+
+  if (!isBlockNode(value)) return yamlEncodeFlow(value);
+
+  final newIndentation = indentation + additionalIndentation;
+
+  if (value is YamlList) {
+    if (value.isEmpty) return '${' ' * indentation}[]';
+
+    Iterable<String> safeValues;
+
+    final children = value.nodes;
+
+    safeValues = children.map((child) {
+      var valueString = yamlEncodeBlock(child, newIndentation, lineEnding);
+      if (isCollection(child) && !isFlowYamlCollectionNode(child)) {
+        valueString = valueString.substring(newIndentation);
+      }
+
+      return '${' ' * indentation}- $valueString';
+    });
+
+    return safeValues.join(lineEnding);
+  } else if (value is YamlMap) {
+    if (value.isEmpty) return '${' ' * indentation}{}';
+
+    return value.nodes.entries.map((entry) {
+      final MapEntry(:key, :value) = entry;
+
+      final safeKey = yamlEncodeFlow(key as YamlNode);
+      final formattedKey = ' ' * indentation + safeKey;
+
+      final formattedValue = yamlEncodeBlock(
+        value,
+        newIndentation,
+        lineEnding,
+      );
+
+      /// Empty collections are always encoded in flow-style, so new-line must
+      /// be avoided
+      if (isCollection(value) && !isEmpty(value)) {
+        return '$formattedKey:$lineEnding$formattedValue';
+      }
+
+      return '$formattedKey: $formattedValue';
+    }).join(lineEnding);
+  }
+
+  return _yamlEncodeBlockScalar(
+    value as YamlScalar,
+    newIndentation,
+    lineEnding,
+  );
+}
+
+/// List of unprintable characters.
+///
+/// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092
+final Map<int, String> unprintableCharCodes = {
+  0: '\\0', //  Escaped ASCII null (#x0) character.
+  7: '\\a', //  Escaped ASCII bell (#x7) character.
+  8: '\\b', //  Escaped ASCII backspace (#x8) character.
+  11: '\\v', // 	Escaped ASCII vertical tab (#xB) character.
+  12: '\\f', //  Escaped ASCII form feed (#xC) character.
+  13: '\\r', //  Escaped ASCII carriage return (#xD) character. Line Break.
+  27: '\\e', //  Escaped ASCII escape (#x1B) character.
+  133: '\\N', //  Escaped Unicode next line (#x85) character.
+  160: '\\_', //  Escaped Unicode non-breaking space (#xA0) character.
+  8232: '\\L', //  Escaped Unicode line separator (#x2028) character.
+  8233: '\\P', //  Escaped Unicode paragraph separator (#x2029) character.
+};
+
+/// List of escape characters.
+///
+/// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092
+final Map<int, String> doubleQuoteEscapeChars = {
+  ...unprintableCharCodes,
+  9: '\\t', //  Escaped ASCII horizontal tab (#x9) character. Printable
+  10: '\\n', //  Escaped ASCII line feed (#xA) character. Line Break.
+  34: '\\"', //  Escaped ASCII double quote (#x22).
+  47: '\\/', //  Escaped ASCII slash (#x2F), for JSON compatibility.
+  92: '\\\\', //  Escaped ASCII back slash (#x5C).
+};
diff --git a/pkgs/yaml_edit/lib/src/utils.dart b/pkgs/yaml_edit/lib/src/utils.dart
new file mode 100644
index 0000000..ef85526
--- /dev/null
+++ b/pkgs/yaml_edit/lib/src/utils.dart
@@ -0,0 +1,291 @@
+// 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:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+import 'editor.dart';
+import 'wrap.dart';
+
+/// Invoke [fn] while setting [yamlWarningCallback] to [warn], and restore
+/// [YamlWarningCallback] after [fn] returns.
+///
+/// Defaults to a [warn] function that ignores all warnings.
+T withYamlWarningCallback<T>(
+  T Function() fn, {
+  YamlWarningCallback warn = _ignoreWarning,
+}) {
+  final original = yamlWarningCallback;
+  try {
+    yamlWarningCallback = warn;
+    return fn();
+  } finally {
+    yamlWarningCallback = original;
+  }
+}
+
+void _ignoreWarning(String warning, [SourceSpan? span]) {/* ignore warning */}
+
+/// Determines if [string] is dangerous by checking if parsing the plain string
+/// can return a result different from [string].
+///
+/// This function is also capable of detecting if non-printable characters are
+/// in [string].
+bool isDangerousString(String string) {
+  try {
+    final node = withYamlWarningCallback(() => loadYamlNode(string));
+    if (node.value != string) {
+      return true;
+    }
+
+    // [string] should also not contain the `[`, `]`, `,`, `{` and `}` indicator
+    // characters.
+    return string.contains(RegExp(r'\{|\[|\]|\}|,'));
+  } catch (e) {
+    /// This catch statement catches [ArgumentError] in `loadYamlNode` when
+    /// a string can be interpreted as a URI tag, but catches for other
+    /// [YamlException]s
+    return true;
+  }
+}
+
+/// Asserts that [value] is a valid scalar according to YAML.
+///
+/// A valid scalar is a number, String, boolean, or null.
+void assertValidScalar(Object? value) {
+  if (value is num || value is String || value is bool || value == null) {
+    return;
+  }
+
+  throw ArgumentError.value(value, 'value', 'Not a valid scalar type!');
+}
+
+/// Checks if [node] is a [YamlNode] with block styling.
+///
+/// [ScalarStyle.ANY] and [CollectionStyle.ANY] are considered to be block
+/// styling by default for maximum flexibility.
+bool isBlockNode(YamlNode node) {
+  if (node is YamlScalar) {
+    if (node.style == ScalarStyle.LITERAL ||
+        node.style == ScalarStyle.FOLDED ||
+        node.style == ScalarStyle.ANY) {
+      return true;
+    }
+  }
+
+  if (node is YamlList &&
+      (node.style == CollectionStyle.BLOCK ||
+          node.style == CollectionStyle.ANY)) {
+    return true;
+  }
+  if (node is YamlMap &&
+      (node.style == CollectionStyle.BLOCK ||
+          node.style == CollectionStyle.ANY)) {
+    return true;
+  }
+
+  return false;
+}
+
+/// Returns the content sensitive ending offset of [yamlNode] (i.e. where the
+/// last meaningful content happens)
+int getContentSensitiveEnd(YamlNode yamlNode) {
+  if (yamlNode is YamlList) {
+    if (yamlNode.style == CollectionStyle.FLOW) {
+      return yamlNode.span.end.offset;
+    } else {
+      return getContentSensitiveEnd(yamlNode.nodes.last);
+    }
+  } else if (yamlNode is YamlMap) {
+    if (yamlNode.style == CollectionStyle.FLOW) {
+      return yamlNode.span.end.offset;
+    } else {
+      return getContentSensitiveEnd(yamlNode.nodes.values.last);
+    }
+  }
+
+  return yamlNode.span.end.offset;
+}
+
+/// Checks if the item is a Map or a List
+bool isCollection(Object item) => item is Map || item is List;
+
+/// Checks if [index] is [int], >=0, < [length]
+bool isValidIndex(Object? index, int length) {
+  return index is int && index >= 0 && index < length;
+}
+
+/// Checks if the item is empty, if it is a List or a Map.
+///
+/// Returns `false` if [item] is not a List or Map.
+bool isEmpty(Object item) {
+  if (item is Map) return item.isEmpty;
+  if (item is List) return item.isEmpty;
+
+  return false;
+}
+
+/// Creates a [SourceSpan] from [sourceUrl] with no meaningful location
+/// information.
+///
+/// Mainly used with [wrapAsYamlNode] to allow for a reasonable
+/// implementation of [SourceSpan.message].
+SourceSpan shellSpan(Object? sourceUrl) {
+  final shellSourceLocation = SourceLocation(0, sourceUrl: sourceUrl);
+  return SourceSpanBase(shellSourceLocation, shellSourceLocation, '');
+}
+
+/// Returns if [value] is a [YamlList] or [YamlMap] with [CollectionStyle.FLOW].
+bool isFlowYamlCollectionNode(Object value) =>
+    value is YamlNode && value.collectionStyle == CollectionStyle.FLOW;
+
+/// Determines the index where [newKey] will be inserted if the keys in [map]
+/// are in alphabetical order when converted to strings.
+///
+/// Returns the length of [map] if the keys in [map] are not in alphabetical
+/// order.
+int getMapInsertionIndex(YamlMap map, Object newKey) {
+  final keys = map.nodes.keys.map((k) => k.toString()).toList();
+
+  // We can't deduce ordering if list is empty, so then we just we just append
+  if (keys.length <= 1) {
+    return map.length;
+  }
+
+  for (var i = 1; i < keys.length; i++) {
+    if (keys[i].compareTo(keys[i - 1]) < 0) {
+      return map.length;
+    }
+  }
+
+  final insertionIndex =
+      keys.indexWhere((key) => key.compareTo(newKey as String) > 0);
+
+  if (insertionIndex != -1) return insertionIndex;
+
+  return map.length;
+}
+
+/// Returns the detected indentation step used in [editor], or defaults to a
+/// value of `2` if no indentation step can be detected.
+///
+/// Indentation step is determined by the difference in indentation of the
+/// first block-styled yaml collection in the second level as compared to the
+/// top-level elements. In the case where there are multiple possible
+/// candidates, we choose the candidate closest to the start of [editor].
+int getIndentation(YamlEditor editor) {
+  final node = editor.parseAt([]);
+  Iterable<YamlNode>? children;
+  var indentation = 2;
+
+  if (node is YamlMap && node.style == CollectionStyle.BLOCK) {
+    children = node.nodes.values;
+  } else if (node is YamlList && node.style == CollectionStyle.BLOCK) {
+    children = node.nodes;
+  }
+
+  if (children != null) {
+    for (final child in children) {
+      var indent = 0;
+      if (child is YamlList) {
+        indent = getListIndentation(editor.toString(), child);
+      } else if (child is YamlMap) {
+        indent = getMapIndentation(editor.toString(), child);
+      }
+
+      if (indent != 0) indentation = indent;
+    }
+  }
+  return indentation;
+}
+
+/// Gets the indentation level of [list]. This is 0 if it is a flow list,
+/// but returns the number of spaces before the hyphen of elements for
+/// block lists.
+///
+/// Throws [UnsupportedError] if an empty block map is passed in.
+int getListIndentation(String yaml, YamlList list) {
+  if (list.style == CollectionStyle.FLOW) return 0;
+
+  /// An empty block map doesn't really exist.
+  if (list.isEmpty) {
+    throw UnsupportedError('Unable to get indentation for empty block list');
+  }
+
+  final lastSpanOffset = list.nodes.last.span.start.offset;
+  final lastHyphen = yaml.lastIndexOf('-', lastSpanOffset - 1);
+
+  if (lastHyphen == 0) return lastHyphen;
+
+  // Look for '\n' that's before hyphen
+  final lastNewLine = yaml.lastIndexOf('\n', lastHyphen - 1);
+
+  return lastHyphen - lastNewLine - 1;
+}
+
+/// Gets the indentation level of [map]. This is 0 if it is a flow map,
+/// but returns the number of spaces before the keys for block maps.
+int getMapIndentation(String yaml, YamlMap map) {
+  if (map.style == CollectionStyle.FLOW) return 0;
+
+  /// An empty block map doesn't really exist.
+  if (map.isEmpty) {
+    throw UnsupportedError('Unable to get indentation for empty block map');
+  }
+
+  /// Use the number of spaces between the last key and the newline as
+  /// indentation.
+  final lastKey = map.nodes.keys.last as YamlNode;
+  final lastSpanOffset = lastKey.span.start.offset;
+  final lastNewLine = yaml.lastIndexOf('\n', lastSpanOffset);
+  final lastQuestionMark = yaml.lastIndexOf('?', lastSpanOffset);
+
+  if (lastQuestionMark == -1) {
+    if (lastNewLine == -1) return lastSpanOffset;
+    return lastSpanOffset - lastNewLine - 1;
+  }
+
+  /// If there is a question mark, it might be a complex key. Check if it
+  /// is on the same line as the key node to verify.
+  if (lastNewLine == -1) return lastQuestionMark;
+  if (lastQuestionMark > lastNewLine) {
+    return lastQuestionMark - lastNewLine - 1;
+  }
+
+  return lastSpanOffset - lastNewLine - 1;
+}
+
+/// Returns the detected line ending used in [yaml], more specifically, whether
+/// [yaml] appears to use Windows `\r\n` or Unix `\n` line endings.
+///
+/// The heuristic used is to count all `\n` in the text and if strictly more
+/// than half of them are preceded by `\r` we report that windows line endings
+/// are used.
+String getLineEnding(String yaml) {
+  var index = -1;
+  var unixNewlines = 0;
+  var windowsNewlines = 0;
+  while ((index = yaml.indexOf('\n', index + 1)) != -1) {
+    if (index != 0 && yaml[index - 1] == '\r') {
+      windowsNewlines++;
+    } else {
+      unixNewlines++;
+    }
+  }
+
+  return windowsNewlines > unixNewlines ? '\r\n' : '\n';
+}
+
+extension YamlNodeExtension on YamlNode {
+  /// Returns the [CollectionStyle] of `this` if `this` is [YamlMap] or
+  /// [YamlList].
+  ///
+  /// Otherwise, returns `null`.
+  CollectionStyle? get collectionStyle {
+    final me = this;
+    if (me is YamlMap) return me.style;
+    if (me is YamlList) return me.style;
+    return null;
+  }
+}
diff --git a/pkgs/yaml_edit/lib/src/wrap.dart b/pkgs/yaml_edit/lib/src/wrap.dart
new file mode 100644
index 0000000..73f7751
--- /dev/null
+++ b/pkgs/yaml_edit/lib/src/wrap.dart
@@ -0,0 +1,216 @@
+// 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:collection' as collection;
+
+import 'package:collection/collection.dart';
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+import 'equality.dart';
+import 'utils.dart';
+
+/// Returns a new [YamlMap] constructed by applying [update] onto the nodes of
+/// this [YamlMap].
+YamlMap updatedYamlMap(YamlMap map, Function(Map) update) {
+  final dummyMap = deepEqualsMap();
+  dummyMap.addAll(map.nodes);
+
+  update(dummyMap);
+
+  return wrapAsYamlNode(dummyMap) as YamlMap;
+}
+
+/// Wraps [value] into a [YamlNode].
+///
+/// [Map]s, [List]s and Scalars will be wrapped as [YamlMap]s, [YamlList]s,
+/// and [YamlScalar]s respectively. If [collectionStyle]/[scalarStyle] is
+/// defined, and [value] is a collection or scalar, the wrapped [YamlNode] will
+/// have the respective style, otherwise it defaults to the ANY style.
+///
+/// If [value] is a [Map] or [List], then [wrapAsYamlNode] will be called
+/// recursively on all children, and [collectionStyle]/[scalarStyle] will be
+/// applied to any children that are not instances of [YamlNode].
+///
+/// If a [YamlNode] is passed in, no further wrapping will be done, and the
+/// [collectionStyle]/[scalarStyle] will not be applied.
+YamlNode wrapAsYamlNode(
+  Object? value, {
+  CollectionStyle collectionStyle = CollectionStyle.ANY,
+  ScalarStyle scalarStyle = ScalarStyle.ANY,
+}) {
+  if (value is YamlScalar) {
+    assertValidScalar(value.value);
+    return value;
+  } else if (value is YamlList) {
+    for (final item in value.nodes) {
+      wrapAsYamlNode(item);
+    }
+
+    return value;
+  } else if (value is YamlMap) {
+    /// Both [entry.key] and [entry.values] are guaranteed to be [YamlNode]s,
+    /// so running this will just assert that they are valid scalars.
+    for (final entry in value.nodes.entries) {
+      wrapAsYamlNode(entry.key);
+      wrapAsYamlNode(entry.value);
+    }
+
+    return value;
+  } else if (value is Map) {
+    return YamlMapWrap(
+      value,
+      collectionStyle: collectionStyle,
+      scalarStyle: scalarStyle,
+    );
+  } else if (value is List) {
+    return YamlListWrap(
+      value,
+      collectionStyle: collectionStyle,
+      scalarStyle: scalarStyle,
+    );
+  } else {
+    assertValidScalar(value);
+
+    return YamlScalarWrap(value, style: scalarStyle);
+  }
+}
+
+/// Internal class that allows us to define a constructor on [YamlScalar]
+/// which takes in [style] as an argument.
+class YamlScalarWrap implements YamlScalar {
+  /// The [ScalarStyle] to be used for the scalar.
+  @override
+  final ScalarStyle style;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  final dynamic value;
+
+  YamlScalarWrap(this.value, {this.style = ScalarStyle.ANY, Object? sourceUrl})
+      : span = shellSpan(sourceUrl);
+
+  @override
+  String toString() => value.toString();
+}
+
+/// Internal class that allows us to define a constructor on [YamlMap]
+/// which takes in [style] as an argument.
+class YamlMapWrap
+    with collection.MapMixin, UnmodifiableMapMixin
+    implements YamlMap {
+  /// The [CollectionStyle] to be used for the map.
+  @override
+  final CollectionStyle style;
+
+  @override
+  final Map<dynamic, YamlNode> nodes;
+
+  @override
+  final SourceSpan span;
+
+  factory YamlMapWrap(
+    Map dartMap, {
+    CollectionStyle collectionStyle = CollectionStyle.ANY,
+    ScalarStyle scalarStyle = ScalarStyle.ANY,
+    Object? sourceUrl,
+  }) {
+    final wrappedMap = deepEqualsMap<dynamic, YamlNode>();
+
+    for (final entry in dartMap.entries) {
+      final wrappedKey = wrapAsYamlNode(
+        entry.key,
+        collectionStyle: collectionStyle,
+        scalarStyle: scalarStyle,
+      );
+      final wrappedValue = wrapAsYamlNode(
+        entry.value,
+        collectionStyle: collectionStyle,
+        scalarStyle: scalarStyle,
+      );
+      wrappedMap[wrappedKey] = wrappedValue;
+    }
+
+    return YamlMapWrap._(
+      wrappedMap,
+      style: collectionStyle,
+      sourceUrl: sourceUrl,
+    );
+  }
+
+  YamlMapWrap._(
+    this.nodes, {
+    CollectionStyle style = CollectionStyle.ANY,
+    Object? sourceUrl,
+  })  : span = shellSpan(sourceUrl),
+        style = nodes.isEmpty ? CollectionStyle.FLOW : style;
+
+  @override
+  dynamic operator [](Object? key) => nodes[key]?.value;
+
+  @override
+  Iterable get keys => nodes.keys.map((node) => (node as YamlNode).value);
+
+  @override
+  Map get value => this;
+}
+
+/// Internal class that allows us to define a constructor on [YamlList]
+/// which takes in [style] as an argument.
+class YamlListWrap with collection.ListMixin implements YamlList {
+  /// The [CollectionStyle] to be used for the list.
+  @override
+  final CollectionStyle style;
+
+  @override
+  final List<YamlNode> nodes;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  int get length => nodes.length;
+
+  @override
+  set length(int index) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+
+  factory YamlListWrap(
+    List dartList, {
+    CollectionStyle collectionStyle = CollectionStyle.ANY,
+    ScalarStyle scalarStyle = ScalarStyle.ANY,
+    Object? sourceUrl,
+  }) {
+    return YamlListWrap._(
+      dartList
+          .map((v) => wrapAsYamlNode(
+                v,
+                collectionStyle: collectionStyle,
+                scalarStyle: scalarStyle,
+              ))
+          .toList(),
+      style: collectionStyle,
+      sourceUrl: sourceUrl,
+    );
+  }
+
+  YamlListWrap._(this.nodes,
+      {CollectionStyle style = CollectionStyle.ANY, Object? sourceUrl})
+      : span = shellSpan(sourceUrl),
+        style = nodes.isEmpty ? CollectionStyle.FLOW : style;
+
+  @override
+  dynamic operator [](int index) => nodes[index].value;
+
+  @override
+  void operator []=(int index, Object? value) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+
+  @override
+  List get value => this;
+}
diff --git a/pkgs/yaml_edit/lib/yaml_edit.dart b/pkgs/yaml_edit/lib/yaml_edit.dart
new file mode 100644
index 0000000..49558b2
--- /dev/null
+++ b/pkgs/yaml_edit/lib/yaml_edit.dart
@@ -0,0 +1,28 @@
+// 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.
+
+/// YAML parsing is supported by `package:yaml`, and each time a change is
+/// made, the resulting YAML AST is compared against our expected output
+/// with deep equality to ensure that the output conforms to our expectations.
+///
+/// **Example**
+/// ```dart
+/// import 'package:yaml_edit/yaml_edit.dart';
+///
+/// void main() {
+///   final yamlEditor = YamlEditor('{YAML: YAML}');
+///   yamlEditor.update(['YAML'], "YAML Ain't Markup Language");
+///   print(yamlEditor);
+///   // Expected Output:
+///   // {YAML: YAML Ain't Markup Language}
+/// }
+/// ```
+///
+/// [1]: https://yaml.org/
+library;
+
+export 'src/editor.dart';
+export 'src/errors.dart' show AliasException;
+export 'src/source_edit.dart';
+export 'src/wrap.dart' show wrapAsYamlNode;
diff --git a/pkgs/yaml_edit/pubspec.yaml b/pkgs/yaml_edit/pubspec.yaml
new file mode 100644
index 0000000..8127a12
--- /dev/null
+++ b/pkgs/yaml_edit/pubspec.yaml
@@ -0,0 +1,25 @@
+name: yaml_edit
+version: 2.2.2
+description: >-
+  A library for YAML manipulation with comment and whitespace preservation.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/yaml_edit
+
+issue_tracker: https://github.com/dart-lang/yaml_edit/issues
+
+topics:
+ - yaml
+
+environment:
+  sdk: ^3.1.0
+
+dependencies:
+  collection: ^1.15.0
+  meta: ^1.7.0
+  source_span: ^1.8.1
+  yaml: ^3.1.0
+
+dev_dependencies:
+  coverage: any # we only need format_coverage, don't care what version
+  dart_flutter_team_lints: ^3.0.0
+  path: ^1.8.0
+  test: ^1.17.12
diff --git a/pkgs/yaml_edit/test/alias_test.dart b/pkgs/yaml_edit/test/alias_test.dart
new file mode 100644
index 0000000..acc0df7
--- /dev/null
+++ b/pkgs/yaml_edit/test/alias_test.dart
@@ -0,0 +1,139 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+/// This test suite is a temporary measure until we are able to better handle
+/// aliases.
+void main() {
+  group('list ', () {
+    test('removing an alias anchor results in AliasError', () {
+      final doc = YamlEditor('''
+- &SS Sammy Sosa
+- *SS
+''');
+      expect(() => doc.remove([0]), throwsAliasException);
+    });
+
+    test('removing an alias reference results in AliasError', () {
+      final doc = YamlEditor('''
+- &SS Sammy Sosa
+- *SS
+''');
+
+      expect(() => doc.remove([1]), throwsAliasException);
+    });
+
+    test('it is okay to remove a non-alias node', () {
+      final doc = YamlEditor('''
+- &SS Sammy Sosa
+- *SS
+- Sammy Sosa
+''');
+
+      doc.remove([2]);
+      expect(doc.toString(), equals('''
+- &SS Sammy Sosa
+- *SS
+'''));
+    });
+  });
+
+  group('map', () {
+    test('removing an alias anchor value results in AliasError', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+b: *SS
+''');
+
+      expect(() => doc.remove(['a']), throwsAliasException);
+    });
+
+    test('removing an alias reference value results in AliasError', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+b: *SS
+''');
+
+      expect(() => doc.remove(['b']), throwsAliasException);
+    });
+
+    test('removing an alias anchor key results in AliasError', () {
+      final doc = YamlEditor('''
+&SS Sammy Sosa: a
+b: *SS
+''');
+
+      expect(() => doc.remove(['Sammy Sosa']), throwsAliasException);
+    });
+
+    test('removing an alias reference key results in AliasError', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+*SS : b
+''');
+
+      expect(() => doc.remove(['Sammy Sosa']), throwsAliasException);
+    });
+
+    test('it is okay to remove a non-alias node', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+b: *SS
+c: Sammy Sosa
+''');
+
+      doc.remove(['c']);
+      expect(doc.toString(), equals('''
+a: &SS Sammy Sosa
+b: *SS
+'''));
+    });
+  });
+
+  group('nested alias', () {
+    test('nested list alias anchors are detected too', () {
+      final doc = YamlEditor('''
+- 
+  - &SS Sammy Sosa
+- *SS
+''');
+
+      expect(() => doc.remove([0]), throwsAliasException);
+    });
+
+    test('nested list alias references are detected too', () {
+      final doc = YamlEditor('''
+- &SS Sammy Sosa
+- 
+  - *SS
+''');
+
+      expect(() => doc.remove([1]), throwsAliasException);
+    });
+
+    test('removing nested map alias anchor results in AliasError', () {
+      final doc = YamlEditor('''
+a:
+  c: &SS Sammy Sosa
+b: *SS
+''');
+
+      expect(() => doc.remove(['a']), throwsAliasException);
+    });
+
+    test('removing nested map alias reference results in AliasError', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+b:
+  c: *SS
+''');
+
+      expect(() => doc.remove(['b']), throwsAliasException);
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/append_test.dart b/pkgs/yaml_edit/test/append_test.dart
new file mode 100644
index 0000000..cb705ed
--- /dev/null
+++ b/pkgs/yaml_edit/test/append_test.dart
@@ -0,0 +1,269 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws PathError', () {
+    test('if it is a map', () {
+      final doc = YamlEditor('a:1');
+      expect(() => doc.appendToList([], 4), throwsPathError);
+    });
+
+    test('if it is a scalar', () {
+      final doc = YamlEditor('1');
+      expect(() => doc.appendToList([], 4), throwsPathError);
+    });
+  });
+
+  group('block list', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+- 0
+- 1
+- 2
+- 3
+''');
+      doc.appendToList([], 4);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2
+- 3
+- 4
+'''));
+      expectYamlBuilderValue(doc, [0, 1, 2, 3, 4]);
+    });
+
+    test('null path', () {
+      final doc = YamlEditor('''
+~:
+  - 0
+  - 1
+  - 2
+  - 3
+''');
+      doc.appendToList([null], 4);
+      expect(doc.toString(), equals('''
+~:
+  - 0
+  - 1
+  - 2
+  - 3
+  - 4
+'''));
+      expectYamlBuilderValue(doc, {
+        null: [0, 1, 2, 3, 4]
+      });
+    });
+
+    test('element to simple block list ', () {
+      final doc = YamlEditor('''
+- 0
+- 1
+- 2
+- 3
+''');
+      doc.appendToList([], [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2
+- 3
+- - 4
+  - 5
+  - 6
+'''));
+      expectYamlBuilderValue(doc, [
+        0,
+        1,
+        2,
+        3,
+        [4, 5, 6]
+      ]);
+    });
+
+    test('nested', () {
+      final doc = YamlEditor('''
+- 0
+- - 1
+  - 2
+''');
+      doc.appendToList([1], 3);
+      expect(doc.toString(), equals('''
+- 0
+- - 1
+  - 2
+  - 3
+'''));
+      expectYamlBuilderValue(doc, [
+        0,
+        [1, 2, 3]
+      ]);
+    });
+
+    test('block list element to nested block list ', () {
+      final doc = YamlEditor('''
+- 0
+- - 1
+  - 2
+''');
+      doc.appendToList([1], [3, 4, 5]);
+
+      expect(doc.toString(), equals('''
+- 0
+- - 1
+  - 2
+  - - 3
+    - 4
+    - 5
+'''));
+      expectYamlBuilderValue(doc, [
+        0,
+        [
+          1,
+          2,
+          [3, 4, 5]
+        ]
+      ]);
+    });
+
+    test('nested', () {
+      final yamlEditor = YamlEditor('''
+a:
+  1:
+    - null
+  2: null
+''');
+      yamlEditor.appendToList(['a', 1], false);
+
+      expect(yamlEditor.toString(), equals('''
+a:
+  1:
+    - null
+    - false
+  2: null
+'''));
+    });
+
+    test('block append (1)', () {
+      final yamlEditor = YamlEditor('''
+# comment
+- z:
+    x: 1
+    y: 2
+- z:
+    x: 3
+    y: 4
+''');
+      yamlEditor.appendToList([], {
+        'z': {'x': 5, 'y': 6}
+      });
+
+      expect(yamlEditor.toString(), equals('''
+# comment
+- z:
+    x: 1
+    y: 2
+- z:
+    x: 3
+    y: 4
+- z:
+    x: 5
+    y: 6
+'''));
+    });
+
+    test('block append (2)', () {
+      final yamlEditor = YamlEditor('''
+# comment
+a:
+  - z:
+      x: 1
+      y: 2
+  - z:
+      x: 3
+      y: 4
+b:
+  - w:
+      m: 2
+      n: 4
+''');
+      yamlEditor.appendToList([
+        'a'
+      ], {
+        'z': {'x': 5, 'y': 6}
+      });
+
+      expect(yamlEditor.toString(), equals('''
+# comment
+a:
+  - z:
+      x: 1
+      y: 2
+  - z:
+      x: 3
+      y: 4
+  - z:
+      x: 5
+      y: 6
+b:
+  - w:
+      m: 2
+      n: 4
+'''));
+    });
+
+    test('block append nested and with comments', () {
+      final yamlEditor = YamlEditor('''
+a:
+  b:
+    - c:
+        d: 1
+    - c:
+        d: 2
+# comment
+  e:
+    - g:
+        e: 1
+        f: 2 
+# comment
+''');
+      expect(
+          () => yamlEditor.appendToList([
+                'a',
+                'e'
+              ], {
+                'g': {'e': 3, 'f': 4}
+              }),
+          returnsNormally);
+    });
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[0, 1, 2]');
+      doc.appendToList([], 3);
+      expect(doc.toString(), equals('[0, 1, 2, 3]'));
+      expectYamlBuilderValue(doc, [0, 1, 2, 3]);
+    });
+
+    test('null value', () {
+      final doc = YamlEditor('[0, 1, 2]');
+      doc.appendToList([], null);
+      expect(doc.toString(), equals('[0, 1, 2, null]'));
+      expectYamlBuilderValue(doc, [0, 1, 2, null]);
+    });
+
+    test('empty ', () {
+      final doc = YamlEditor('[]');
+      doc.appendToList([], 0);
+      expect(doc.toString(), equals('[0]'));
+      expectYamlBuilderValue(doc, [0]);
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/editor_test.dart b/pkgs/yaml_edit/test/editor_test.dart
new file mode 100644
index 0000000..b0a0081
--- /dev/null
+++ b/pkgs/yaml_edit/test/editor_test.dart
@@ -0,0 +1,56 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+void main() {
+  group('YamlEditor records edits', () {
+    test('returns empty list at start', () {
+      final yamlEditor = YamlEditor('YAML: YAML');
+
+      expect(yamlEditor.edits, []);
+    });
+
+    test('after one change', () {
+      final yamlEditor = YamlEditor('YAML: YAML');
+      yamlEditor.update(['YAML'], "YAML Ain't Markup Language");
+
+      expect(
+          yamlEditor.edits, [SourceEdit(5, 5, " YAML Ain't Markup Language")]);
+    });
+
+    test('after multiple changes', () {
+      final yamlEditor = YamlEditor('YAML: YAML');
+      yamlEditor.update(['YAML'], "YAML Ain't Markup Language");
+      yamlEditor.update(['XML'], 'Extensible Markup Language');
+      yamlEditor.remove(['YAML']);
+
+      expect(yamlEditor.edits, [
+        SourceEdit(5, 5, " YAML Ain't Markup Language"),
+        SourceEdit(32, 0, '\nXML: Extensible Markup Language\n'),
+        SourceEdit(0, 33, '')
+      ]);
+    });
+
+    test('that do not automatically update with internal list', () {
+      final yamlEditor = YamlEditor('YAML: YAML');
+      yamlEditor.update(['YAML'], "YAML Ain't Markup Language");
+
+      final firstEdits = yamlEditor.edits;
+
+      expect(firstEdits, [SourceEdit(5, 5, " YAML Ain't Markup Language")]);
+
+      yamlEditor.update(['XML'], 'Extensible Markup Language');
+      yamlEditor.remove(['YAML']);
+
+      expect(firstEdits, [SourceEdit(5, 5, " YAML Ain't Markup Language")]);
+      expect(yamlEditor.edits, [
+        SourceEdit(5, 5, " YAML Ain't Markup Language"),
+        SourceEdit(32, 0, '\nXML: Extensible Markup Language\n'),
+        SourceEdit(0, 33, '')
+      ]);
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/golden_test.dart b/pkgs/yaml_edit/test/golden_test.dart
new file mode 100644
index 0000000..1dd6ff3
--- /dev/null
+++ b/pkgs/yaml_edit/test/golden_test.dart
@@ -0,0 +1,40 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:test/test.dart';
+
+import 'test_case.dart';
+
+/// This script performs snapshot testing of the inputs in the testing directory
+/// against golden files if they exist, and creates the golden files otherwise.
+///
+/// Input directory should be in `test/test_cases`, while the golden files should
+/// be in `test/test_cases_golden`.
+///
+/// For more information on the expected input and output, refer to the README
+/// in the testdata folder
+Future<void> main() async {
+  final packageUri = await Isolate.resolvePackageUri(
+      Uri.parse('package:yaml_edit/yaml_edit.dart'));
+
+  final testdataUri = packageUri!.resolve('../test/testdata/');
+  final inputDirectory = Directory.fromUri(testdataUri.resolve('input/'));
+  final goldDirectoryUri = testdataUri.resolve('output/');
+
+  if (!inputDirectory.existsSync()) {
+    throw FileSystemException(
+        'Testing Directory does not exist!', inputDirectory.path);
+  }
+
+  final testCases =
+      await TestCases.getTestCases(inputDirectory.uri, goldDirectoryUri);
+
+  testCases.test();
+}
diff --git a/pkgs/yaml_edit/test/insert_test.dart b/pkgs/yaml_edit/test/insert_test.dart
new file mode 100644
index 0000000..8c0e3b2
--- /dev/null
+++ b/pkgs/yaml_edit/test/insert_test.dart
@@ -0,0 +1,207 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws PathError', () {
+    test('if it is a map', () {
+      final doc = YamlEditor('a:1');
+      expect(() => doc.insertIntoList([], 0, 4), throwsPathError);
+    });
+
+    test('if it is a scalar', () {
+      final doc = YamlEditor('1');
+      expect(() => doc.insertIntoList([], 0, 4), throwsPathError);
+    });
+  });
+
+  test('throws RangeError if index is out of range', () {
+    final doc = YamlEditor('[1, 2]');
+    expect(() => doc.insertIntoList([], -1, 0), throwsRangeError);
+    expect(() => doc.insertIntoList([], 3, 0), throwsRangeError);
+  });
+
+  group('block list', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+- 1
+- 2''');
+      doc.insertIntoList([], 0, 0);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('''
+- 1
+- 2''');
+      doc.insertIntoList([], 1, 3);
+      expect(doc.toString(), equals('''
+- 1
+- 3
+- 2'''));
+      expectYamlBuilderValue(doc, [1, 3, 2]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+''');
+      doc.insertIntoList([], 2, 3);
+      expect(doc.toString(), equals('''
+- 1
+- 2
+- 3
+'''));
+      expectYamlBuilderValue(doc, [1, 2, 3]);
+    });
+
+    test('(4)', () {
+      final doc = YamlEditor('''
+- 1
+- 3
+''');
+      doc.insertIntoList([], 1, [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- 1
+- - 4
+  - 5
+  - 6
+- 3
+'''));
+      expectYamlBuilderValue(doc, [
+        1,
+        [4, 5, 6],
+        3
+      ]);
+    });
+
+    test(' with comments', () {
+      final doc = YamlEditor('''
+- 0 # comment a
+- 2 # comment b
+''');
+      doc.insertIntoList([], 1, 1);
+      expect(doc.toString(), equals('''
+- 0 # comment a
+- 1
+- 2 # comment b
+'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    for (var i = 0; i < 3; i++) {
+      test('block insert(1) at $i', () {
+        final yamlEditor = YamlEditor('''
+# comment
+- z:
+    x: 1
+    y: 2
+- z:
+    x: 3
+    y: 4
+''');
+        expect(
+            () => yamlEditor.insertIntoList(
+                [],
+                i,
+                {
+                  'z': {'x': 5, 'y': 6}
+                }),
+            returnsNormally);
+      });
+    }
+
+    for (var i = 0; i < 3; i++) {
+      test('block insert(2) at $i', () {
+        final yamlEditor = YamlEditor('''
+a:
+  - z:
+      x: 1
+      y: 2
+  - z:
+      x: 3
+      y: 4
+b:
+  - w:
+      m: 2
+      n: 4
+''');
+        expect(
+            () => yamlEditor.insertIntoList(
+                ['a'],
+                i,
+                {
+                  'z': {'x': 5, 'y': 6}
+                }),
+            returnsNormally);
+      });
+    }
+
+    for (var i = 0; i < 2; i++) {
+      test('block insert nested and with comments at $i', () {
+        final yamlEditor = YamlEditor('''
+a:
+  b:
+    - c:
+        d: 1
+    - c:
+        d: 2
+# comment
+  e:
+    - g:
+        e: 1
+        f: 2 
+# comment
+''');
+        expect(
+            () => yamlEditor.insertIntoList(
+                ['a', 'b'],
+                i,
+                {
+                  'g': {'e': 3, 'f': 4}
+                }),
+            returnsNormally);
+      });
+    }
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[1, 2]');
+      doc.insertIntoList([], 0, 0);
+      expect(doc.toString(), equals('[0, 1, 2]'));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('[1, 2]');
+      doc.insertIntoList([], 1, 3);
+      expect(doc.toString(), equals('[1, 3, 2]'));
+      expectYamlBuilderValue(doc, [1, 3, 2]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('[1, 2]');
+      doc.insertIntoList([], 2, 3);
+      expect(doc.toString(), equals('[1, 2, 3]'));
+      expectYamlBuilderValue(doc, [1, 2, 3]);
+    });
+
+    test('(4)', () {
+      final doc = YamlEditor('["[],", "[],"]');
+      doc.insertIntoList([], 1, 'test');
+      expect(doc.toString(), equals('["[],", test, "[],"]'));
+      expectYamlBuilderValue(doc, ['[],', 'test', '[],']);
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/naughty_test.dart b/pkgs/yaml_edit/test/naughty_test.dart
new file mode 100644
index 0000000..533a535
--- /dev/null
+++ b/pkgs/yaml_edit/test/naughty_test.dart
@@ -0,0 +1,30 @@
+// 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:async';
+
+import 'package:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'problem_strings.dart';
+
+void main() {
+  for (final string in problemStrings) {
+    test('expect string $string', () {
+      final doc = YamlEditor('');
+
+      /// Using [runZoned] to hide `package:yaml`'s warnings.
+      /// Test failures and errors will still be shown.
+      runZoned(() {
+        expect(() => doc.update([], string), returnsNormally);
+        final value = doc.parseAt([]).value;
+        expect(value, isA<String>());
+        expect(value, equals(string));
+      },
+          zoneSpecification: ZoneSpecification(
+              print: (Zone self, ZoneDelegate parent, Zone zone,
+                  String message) {}));
+    });
+  }
+}
diff --git a/pkgs/yaml_edit/test/parse_test.dart b/pkgs/yaml_edit/test/parse_test.dart
new file mode 100644
index 0000000..382307c
--- /dev/null
+++ b/pkgs/yaml_edit/test/parse_test.dart
@@ -0,0 +1,156 @@
+// 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:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws', () {
+    test('PathError if key does not exist', () {
+      final doc = YamlEditor('{a: 4}');
+      final path = ['b'];
+
+      expect(() => doc.parseAt(path), throwsPathError);
+    });
+
+    test('PathError if path tries to go deeper into a scalar', () {
+      final doc = YamlEditor('{a: 4}');
+      final path = ['a', 'b'];
+
+      expect(() => doc.parseAt(path), throwsPathError);
+    });
+
+    test('PathError if index is out of bounds', () {
+      final doc = YamlEditor('[0,1]');
+      final path = [2];
+
+      expect(() => doc.parseAt(path), throwsPathError);
+    });
+
+    test('PathError if index is not an integer', () {
+      final doc = YamlEditor('[0,1]');
+      final path = ['2'];
+
+      expect(() => doc.parseAt(path), throwsPathError);
+    });
+  });
+
+  group('orElse provides a default value', () {
+    test('simple example with null node return ', () {
+      final doc = YamlEditor('{a: {d: 4}, c: ~}');
+      final result = doc.parseAt(['b'], orElse: () => wrapAsYamlNode(null));
+
+      expect(result.value, equals(null));
+    });
+
+    test('simple example with map return', () {
+      final doc = YamlEditor('{a: {d: 4}, c: ~}');
+      final result =
+          doc.parseAt(['b'], orElse: () => wrapAsYamlNode({'a': 42}));
+
+      expect(result, isA<YamlMap>());
+      expect(result.value, equals({'a': 42}));
+    });
+
+    test('simple example with scalar return', () {
+      final doc = YamlEditor('{a: {d: 4}, c: ~}');
+      final result = doc.parseAt(['b'], orElse: () => wrapAsYamlNode(42));
+
+      expect(result, isA<YamlScalar>());
+      expect(result.value, equals(42));
+    });
+
+    test('simple example with list return', () {
+      final doc = YamlEditor('{a: {d: 4}, c: ~}');
+      final result = doc.parseAt(['b'], orElse: () => wrapAsYamlNode([42]));
+
+      expect(result, isA<YamlList>());
+      expect(result.value, equals([42]));
+    });
+  });
+
+  group('returns a YamlNode', () {
+    test('with the correct type', () {
+      final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+      final expectedYamlScalar = doc.parseAt(['YAML']);
+
+      expect(expectedYamlScalar, isA<YamlScalar>());
+    });
+
+    test('with the correct value', () {
+      final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+
+      expect(doc.parseAt(['YAML']).value, "YAML Ain't Markup Language");
+    });
+
+    test('with the correct value in nested collection', () {
+      final doc = YamlEditor('''
+a: 1
+b:
+  d: 4
+  e: [5, 6, 7]
+c: 3
+''');
+
+      expect(doc.parseAt(['b', 'e', 2]).value, 7);
+    });
+
+    test('with a null value in nested collection', () {
+      final doc = YamlEditor('''
+key1:
+  key2: null
+''');
+
+      expect(doc.parseAt(['key1', 'key2']).value, null);
+    });
+
+    test('with the correct type (2)', () {
+      final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+      final expectedYamlMap = doc.parseAt([]);
+
+      expect(expectedYamlMap is YamlMap, equals(true));
+    });
+
+    test('that is immutable', () {
+      final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+      final expectedYamlMap = doc.parseAt([]);
+
+      expect(() => (expectedYamlMap as YamlMap)['YAML'] = 'test',
+          throwsUnsupportedError);
+    });
+
+    test('that has immutable children', () {
+      final doc = YamlEditor("YAML: ['Y', 'A', 'M', 'L']");
+      final expectedYamlMap = doc.parseAt([]);
+
+      expect(() => ((expectedYamlMap as YamlMap)['YAML'] as List)[0] = 'X',
+          throwsUnsupportedError);
+    });
+  });
+
+  test('works with map keys', () {
+    final doc = YamlEditor('{a: {{[1, 2]: 3}: 4}}');
+    expect(
+        doc.parseAt([
+          'a',
+          {
+            [1, 2]: 3
+          }
+        ]).value,
+        equals(4));
+  });
+
+  test('works with null in path', () {
+    final doc = YamlEditor('{a: { ~: 4}}');
+    expect(doc.parseAt(['a', null]).value, equals(4));
+  });
+
+  test('works with null value', () {
+    final doc = YamlEditor('{a: null}');
+    expect(doc.parseAt(['a']).value, equals(null));
+  });
+}
diff --git a/pkgs/yaml_edit/test/prepend_test.dart b/pkgs/yaml_edit/test/prepend_test.dart
new file mode 100644
index 0000000..3112653
--- /dev/null
+++ b/pkgs/yaml_edit/test/prepend_test.dart
@@ -0,0 +1,169 @@
+// 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:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws PathError', () {
+    test('if it is a map', () {
+      final doc = YamlEditor('a:1');
+      expect(() => doc.prependToList([], 4), throwsPathError);
+    });
+
+    test('if it is a scalar', () {
+      final doc = YamlEditor('1');
+      expect(() => doc.prependToList([], 4), throwsPathError);
+    });
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[1, 2]');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('[0, 1, 2]'));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('null value', () {
+      final doc = YamlEditor('[1, 2]');
+      doc.prependToList([], null);
+      expect(doc.toString(), equals('[null, 1, 2]'));
+      expectYamlBuilderValue(doc, [null, 1, 2]);
+    });
+
+    test('with spaces (1)', () {
+      final doc = YamlEditor('[ 1 , 2 ]');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('[ 0, 1 , 2 ]'));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+  });
+
+  group('block list', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+- 1
+- 2''');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    /// Regression testing for no trailing spaces.
+    test('(2)', () {
+      final doc = YamlEditor('''- 1
+- 2''');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('''- 0
+- 1
+- 2'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+''');
+      doc.prependToList([], [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- - 4
+  - 5
+  - 6
+- 1
+- 2
+'''));
+      expectYamlBuilderValue(doc, [
+        [4, 5, 6],
+        1,
+        2
+      ]);
+    });
+
+    test('(4)', () {
+      final doc = YamlEditor('''
+a:
+ - b
+ - - c
+   - d
+''');
+      doc.prependToList(
+          ['a'], wrapAsYamlNode({1: 2}, collectionStyle: CollectionStyle.FLOW));
+
+      expect(doc.toString(), equals('''
+a:
+ - {1: 2}
+ - b
+ - - c
+   - d
+'''));
+      expectYamlBuilderValue(doc, {
+        'a': [
+          {1: 2},
+          'b',
+          ['c', 'd']
+        ]
+      });
+    });
+
+    test('with comments ', () {
+      final doc = YamlEditor('''
+# comments
+- 1 # comments
+- 2
+''');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('''
+# comments
+- 0
+- 1 # comments
+- 2
+'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('nested in map', () {
+      final doc = YamlEditor('''
+a:
+  - 1
+  - 2
+''');
+      doc.prependToList(['a'], 0);
+      expect(doc.toString(), equals('''
+a:
+  - 0
+  - 1
+  - 2
+'''));
+      expectYamlBuilderValue(doc, {
+        'a': [0, 1, 2]
+      });
+    });
+
+    test('nested in map with comments ', () {
+      final doc = YamlEditor('''
+a: # comments
+  - 1 # comments
+  - 2
+''');
+      doc.prependToList(['a'], 0);
+      expect(doc.toString(), equals('''
+a: # comments
+  - 0
+  - 1 # comments
+  - 2
+'''));
+      expectYamlBuilderValue(doc, {
+        'a': [0, 1, 2]
+      });
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/preservation_test.dart b/pkgs/yaml_edit/test/preservation_test.dart
new file mode 100644
index 0000000..a763296
--- /dev/null
+++ b/pkgs/yaml_edit/test/preservation_test.dart
@@ -0,0 +1,61 @@
+// 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:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('preserves original yaml: ', () {
+    test('number', expectLoadPreservesYAML('2'));
+    test('number with leading and trailing lines', expectLoadPreservesYAML('''
+      
+      2
+      
+      '''));
+    test('octal numbers', expectLoadPreservesYAML('0o14'));
+    test('negative numbers', expectLoadPreservesYAML('-345'));
+    test('hexadecimal numbers', expectLoadPreservesYAML('0x123abc'));
+    test('floating point numbers', expectLoadPreservesYAML('345.678'));
+    test('exponential numbers', expectLoadPreservesYAML('12.3015e+02'));
+    test('string', expectLoadPreservesYAML('a string'));
+    test('string with control characters',
+        expectLoadPreservesYAML('a string \\n'));
+    test('string with control characters',
+        expectLoadPreservesYAML('a string \n\r'));
+    test('string with hex escapes',
+        expectLoadPreservesYAML('\\x0d\\x0a is \\r\\n'));
+    test('flow map', expectLoadPreservesYAML('{a: 2}'));
+    test('flow list', expectLoadPreservesYAML('[1, 2]'));
+    test('flow list with different types of elements',
+        expectLoadPreservesYAML('[1, a]'));
+    test('flow list with weird spaces',
+        expectLoadPreservesYAML('[ 1 ,      2]'));
+    test('multiline string', expectLoadPreservesYAML('''
+      Mark set a major league
+      home run record in 1998.'''));
+    test('tilde', expectLoadPreservesYAML('~'));
+    test('false', expectLoadPreservesYAML('false'));
+
+    test('block map', expectLoadPreservesYAML('''a:
+    b: 1
+    '''));
+    test('block list', expectLoadPreservesYAML('''a:
+    - 1
+    '''));
+    test('complicated example', () {
+      expectLoadPreservesYAML('''verb: RecommendCafes
+map:
+  a:
+    b: 1
+recipe:
+  - verb: Score
+    outputs: ["DishOffering[]/Scored", "Suggestions"]
+    name: Hotpot
+  - verb: Rate
+    inputs: Dish
+    ''');
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/problem_strings.dart b/pkgs/yaml_edit/test/problem_strings.dart
new file mode 100644
index 0000000..527a9e0
--- /dev/null
+++ b/pkgs/yaml_edit/test/problem_strings.dart
@@ -0,0 +1,91 @@
+// 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.
+
+// ignore_for_file: lines_longer_than_80_chars
+
+const problemStrings = [
+  '[]',
+  '{}',
+  '',
+  ',',
+  '~',
+  'undefined',
+  'undef',
+  'null',
+  'NULL',
+  '(null)',
+  'nil',
+  'NIL',
+  'true',
+  'false',
+  'True',
+  'False',
+  'TRUE',
+  'FALSE',
+  'None',
+  '\\',
+  '\\\\',
+  '0',
+  '1',
+  '\$1.00',
+  '1/2',
+  '1E2',
+  '-\$1.00',
+  '-1/2',
+  '-1E+02',
+  '1/0',
+  '0/0',
+  '-0',
+  '+0.0',
+  '0..0',
+  '.',
+  '0.0.0',
+  '0,00',
+  ',',
+  '0.0/0',
+  '1.0/0.0',
+  '0.0/0.0',
+  '--1',
+  '-',
+  '-.',
+  '-,',
+  '999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999',
+  'NaN',
+  'Infinity',
+  '-Infinity',
+  'INF',
+  '1#INF',
+  '0x0',
+  '0xffffffffffffffff',
+  "1'000.00",
+  '1,000,000.00',
+  '1.000,00',
+  "1'000,00",
+  '1.000.000,00',
+  ",./;'[]\\-=",
+  '<>?:"{}|_+',
+  '!@#\$%^&*()`~',
+  '\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f',
+  '\t\u000b\f …             ​

   ',
+  'ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็',
+  "'",
+  '"',
+  "''",
+  '\'"',
+  "'\"'",
+  '社會科學院語學研究所',
+  'Ⱥ',
+  'ヽ༼ຈل͜ຈ༽ノ ヽ༼ຈل͜ຈ༽ノ',
+  '❤️ 💔 💌 💕 💞 💓 💗 💖 💘 💝 💟 💜 💛 💚 💙',
+  '𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘',
+  ' ',
+  '%',
+  '%d',
+  '%s%s%s%s%s',
+  '{0}',
+  '%*.*s',
+  '%@',
+  '%n',
+  'The quic\b\b\b\b\b\bk brown fo\u0007\u0007\u0007\u0007\u0007\u0007\u0007\u0007\u0007\u0007\u0007x... [Beeeep]',
+];
diff --git a/pkgs/yaml_edit/test/random_test.dart b/pkgs/yaml_edit/test/random_test.dart
new file mode 100644
index 0000000..85cea4a
--- /dev/null
+++ b/pkgs/yaml_edit/test/random_test.dart
@@ -0,0 +1,311 @@
+// 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:math' show Random;
+
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'problem_strings.dart';
+import 'test_utils.dart';
+
+/// Performs naive fuzzing on an initial YAML file based on an initial seed.
+///
+/// Starting with a template YAML, we randomly generate modifications and their
+/// inputs (boolean, null, strings, or numbers) to modify the YAML and assert
+/// that the change produced was expected.
+void main() {
+  final generator = _Generator(maxDepth: 5);
+
+  const roundsOfTesting = 40;
+  const modificationsPerRound = 1000;
+
+  for (var i = 0; i < roundsOfTesting; i++) {
+    test(
+      'testing with randomly generated modifications: test $i',
+      () {
+        final editor = YamlEditor('''
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 0.0.1-dev
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8
+  quiver_hashcode: ^2.0.0
+
+dev_dependencies:
+  pedantic: ^1.9.0
+  test: ^1.14.4
+''');
+
+        for (var j = 0; j < modificationsPerRound; j++) {
+          expect(
+            () => generator.performNextModification(editor, i),
+            returnsNormally,
+          );
+        }
+      },
+    );
+  }
+}
+
+/// Generates the random variables we need for fuzzing.
+class _Generator {
+  final Random r;
+
+  /// 2^32
+  static const int maxInt = 4294967296;
+
+  /// Maximum depth of random YAML collection generated.
+  final int maxDepth;
+
+  // ignore: unused_element
+  _Generator({int seed = 0, required this.maxDepth}) : r = Random(seed);
+
+  int nextInt([int max = maxInt]) => r.nextInt(max);
+
+  double nextDouble() => r.nextDouble();
+
+  bool nextBool() => r.nextBool();
+
+  /// Generates a new string by individually generating characters and
+  /// appending them to a buffer. Currently only generates strings from
+  /// ascii 32 - 127.
+  String nextString() {
+    if (nextBool()) {
+      return problemStrings[nextInt(problemStrings.length)];
+    }
+
+    final length = nextInt(100);
+    final buffer = StringBuffer();
+
+    for (var i = 0; i < length; i++) {
+      final charCode = nextInt(95) + 32;
+      buffer.writeCharCode(charCode);
+    }
+
+    return buffer.toString();
+  }
+
+  /// Generates a new scalar recognizable by YAML.
+  Object? nextScalar() {
+    final typeIndex = nextInt(5);
+
+    switch (typeIndex) {
+      case 0:
+        return nextBool();
+      case 1:
+        return nextDouble();
+      case 2:
+        return nextInt();
+      case 3:
+        return null;
+      default:
+        return nextString();
+    }
+  }
+
+  YamlScalar nextYamlScalar() {
+    return wrapAsYamlNode(nextScalar(), scalarStyle: nextScalarStyle())
+        as YamlScalar;
+  }
+
+  /// Generates the next [YamlList], with the current [depth].
+  YamlList nextYamlList(int depth) {
+    final length = nextInt(9);
+    final list = [];
+
+    for (var i = 0; i < length; i++) {
+      list.add(nextYamlNode(depth + 1));
+    }
+
+    return wrapAsYamlNode(list, collectionStyle: nextCollectionStyle())
+        as YamlList;
+  }
+
+  /// Generates the next [YamlList], with the current [depth].
+  YamlMap nextYamlMap(int depth) {
+    final length = nextInt(9);
+    final nodes = {};
+
+    for (var i = 0; i < length; i++) {
+      nodes[nextYamlNode(depth + 1)] = nextYamlScalar();
+    }
+
+    return wrapAsYamlNode(nodes, collectionStyle: nextCollectionStyle())
+        as YamlMap;
+  }
+
+  /// Returns a [YamlNode], with it being a [YamlScalar] 80% of the time, a
+  /// [YamlList] 10% of the time, and a [YamlMap] 10% of the time.
+  ///
+  /// If [depth] is greater than [maxDepth], we instantly return a [YamlScalar]
+  /// to prevent the parent from growing any further, to improve our speeds.
+  YamlNode nextYamlNode([int depth = 0]) {
+    if (depth >= maxDepth) {
+      return nextYamlScalar();
+    }
+
+    final roll = nextInt(10);
+
+    if (roll < 8) {
+      return nextYamlScalar();
+    } else if (roll == 8) {
+      return nextYamlList(depth);
+    } else {
+      return nextYamlMap(depth);
+    }
+  }
+
+  /// Performs a random modification
+  void performNextModification(YamlEditor editor, int count) {
+    final path = findPath(editor);
+    final node = editor.parseAt(path);
+    final initialString = editor.toString();
+    final args = [];
+    var method = YamlModificationMethod.remove;
+
+    try {
+      if (node is YamlScalar) {
+        editor.remove(path);
+        return;
+      }
+
+      if (node is YamlList) {
+        final methodIndex = nextInt(YamlModificationMethod.values.length);
+        method = YamlModificationMethod.values[methodIndex];
+
+        switch (method) {
+          case YamlModificationMethod.remove:
+            editor.remove(path);
+            break;
+          case YamlModificationMethod.update:
+            if (node.isEmpty) break;
+            final index = nextInt(node.length);
+            args.add(nextYamlNode());
+            path.add(index);
+            editor.update(path, args[0]);
+            break;
+          case YamlModificationMethod.appendTo:
+            args.add(nextYamlNode());
+            editor.appendToList(path, args[0]);
+            break;
+          case YamlModificationMethod.prependTo:
+            args.add(nextYamlNode());
+            editor.prependToList(path, args[0]);
+            break;
+          case YamlModificationMethod.insert:
+            args.add(nextInt(node.length + 1));
+            args.add(nextYamlNode());
+            editor.insertIntoList(path, args[0] as int, args[1]);
+            break;
+          case YamlModificationMethod.splice:
+            args.add(nextInt(node.length + 1));
+            args.add(nextInt(node.length + 1 - (args[0] as int)));
+            args.add(nextYamlList(0));
+            editor.spliceList(
+                path, args[0] as int, args[1] as int, args[2] as List);
+            break;
+        }
+        return;
+      }
+
+      if (node is YamlMap) {
+        final replace = nextBool();
+        method = YamlModificationMethod.update;
+
+        if (replace && node.isNotEmpty) {
+          final keyList = node.keys.toList();
+          path.add(keyList[nextInt(keyList.length)]);
+        } else {
+          path.add(nextScalar());
+        }
+        final value = nextYamlNode();
+        args.add(value);
+        editor.update(path, value);
+        return;
+      }
+    } catch (error, stacktrace) {
+      /// TODO: Fix once reproducible. Identify pattern.
+      if (count == 20) return;
+
+      print('''
+Failed to call $method on:
+$initialString
+with the following arguments:
+$args
+and path:
+$path
+
+Error Details:
+$error
+
+$stacktrace
+''');
+      rethrow;
+    }
+
+    throw AssertionError('Got invalid node');
+  }
+
+  /// Obtains a random path by traversing [editor].
+  ///
+  /// At every node, we return the path to the node if the node has no children.
+  /// Otherwise, we return at a 50% chance, or traverse to one random child.
+  List<Object?> findPath(YamlEditor editor) {
+    final path = <Object?>[];
+
+    // 50% chance of stopping at the collection
+    while (nextBool()) {
+      final node = editor.parseAt(path);
+
+      if (node is YamlList && node.isNotEmpty) {
+        path.add(nextInt(node.length));
+      } else if (node is YamlMap && node.isNotEmpty) {
+        final keyList = node.keys.toList();
+        path.add(keyList[nextInt(keyList.length)]);
+      } else {
+        break;
+      }
+    }
+
+    return path;
+  }
+
+  ScalarStyle nextScalarStyle() {
+    final seed = nextInt(6);
+
+    switch (seed) {
+      case 0:
+        return ScalarStyle.DOUBLE_QUOTED;
+      case 1:
+        return ScalarStyle.FOLDED;
+      case 2:
+        return ScalarStyle.LITERAL;
+      case 3:
+        return ScalarStyle.PLAIN;
+      case 4:
+        return ScalarStyle.SINGLE_QUOTED;
+      default:
+        return ScalarStyle.ANY;
+    }
+  }
+
+  CollectionStyle nextCollectionStyle() {
+    final seed = nextInt(3);
+
+    switch (seed) {
+      case 0:
+        return CollectionStyle.BLOCK;
+      case 1:
+        return CollectionStyle.FLOW;
+      default:
+        return CollectionStyle.ANY;
+    }
+  }
+}
diff --git a/pkgs/yaml_edit/test/remove_test.dart b/pkgs/yaml_edit/test/remove_test.dart
new file mode 100644
index 0000000..4742b56
--- /dev/null
+++ b/pkgs/yaml_edit/test/remove_test.dart
@@ -0,0 +1,603 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws', () {
+    test('PathError if collectionPath points to a scalar', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+''');
+
+      expect(() => doc.remove(['a', 0]), throwsPathError);
+    });
+
+    test('PathError if collectionPath is invalid', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+''');
+
+      expect(() => doc.remove(['d']), throwsPathError);
+    });
+
+    test('PathError if collectionPath is invalid in nested path', () {
+      final doc = YamlEditor('''
+a:
+  b: 'foo'
+''');
+
+      expect(() => doc.remove(['d']), throwsPathError);
+    });
+
+    test('PathError if collectionPath is invalid - list', () {
+      final doc = YamlEditor('''
+[1, 2, 3]
+''');
+
+      expect(() => doc.remove([4]), throwsPathError);
+    });
+
+    test('PathError in list if using a non-integer as index', () {
+      final doc = YamlEditor("{ a: ['b', 'c'] }");
+      expect(() => doc.remove(['a', 'b']), throwsPathError);
+    });
+
+    test('PathError if path is invalid', () {
+      final doc = YamlEditor("{ a: ['b', 'c'] }");
+      expect(() => doc.remove(['a', 0, '1']), throwsPathError);
+    });
+  });
+
+  group('returns', () {
+    test('returns the removed node when successful', () {
+      final doc = YamlEditor('{ a: { b: foo } }');
+      final node = doc.remove(['a', 'b']);
+      expect(node.value, equals('foo'));
+    });
+
+    test('returns null-value node when doc is empty and path is empty', () {
+      final doc = YamlEditor('');
+      final node = doc.remove([]);
+      expect(node.value, equals(null));
+    });
+  });
+
+  test('empty path should clear string', () {
+    final doc = YamlEditor('''
+a: 1
+b: 2
+c: [3, 4]
+''');
+    doc.remove([]);
+    expect(doc.toString(), equals(''));
+  });
+
+  group('block map', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+''');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('''
+a: 1
+c: 3
+'''));
+    });
+
+    test('empty value', () {
+      final doc = YamlEditor('''
+a: 1
+b:
+c: 3
+''');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('''
+a: 1
+c: 3
+'''));
+    });
+
+    test('empty value (2)', () {
+      final doc = YamlEditor('''
+- a: 1
+  b:
+  c: 3
+''');
+      doc.remove([0, 'b']);
+      expect(doc.toString(), equals('''
+- a: 1
+  c: 3
+'''));
+    });
+
+    test('empty value (3)', () {
+      final doc = YamlEditor('''
+- a: 1
+  b:
+
+  c: 3
+''');
+      doc.remove([0, 'b']);
+      expect(doc.toString(), equals('''
+- a: 1
+
+  c: 3
+'''));
+    });
+
+    test('preserves comments', () {
+      final doc = YamlEditor('''
+a: 1 # preserved 1
+# preserved 2
+b: 2
+# preserved 3
+c: 3 # preserved 4
+''');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('''
+a: 1 # preserved 1
+# preserved 2
+# preserved 3
+c: 3 # preserved 4
+'''));
+    });
+
+    test('final element in map', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+''');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('''
+a: 1
+'''));
+    });
+
+    test('final element in nested map', () {
+      final doc = YamlEditor('''
+a:
+  aa: 11
+  bb: 22
+b: 2
+''');
+      doc.remove(['a', 'bb']);
+      expect(doc.toString(), equals('''
+a:
+  aa: 11
+b: 2
+'''));
+    });
+
+    test('last element should return flow empty map', () {
+      final doc = YamlEditor('''
+a: 1
+''');
+      doc.remove(['a']);
+      expect(doc.toString(), equals('''
+{}
+'''));
+    });
+
+    test('last element should return flow empty map (2)', () {
+      final doc = YamlEditor('''
+- a: 1
+- b: 2
+''');
+      doc.remove([0, 'a']);
+      expect(doc.toString(), equals('''
+- {}
+- b: 2
+'''));
+    });
+
+    test('nested', () {
+      final doc = YamlEditor('''
+a: 1
+b:
+  d: 4
+  e: 5
+c: 3
+''');
+      doc.remove(['b', 'd']);
+      expect(doc.toString(), equals('''
+a: 1
+b:
+  e: 5
+c: 3
+'''));
+    });
+
+    test('issue #55 reopend', () {
+      final doc = YamlEditor('''name: sample
+version: 0.1.0
+environment:
+  sdk: ^3.0.0
+dependencies:
+  retry: ^3.1.2
+dev_dependencies:
+  retry:''');
+      doc.remove(['dev_dependencies']);
+    });
+
+    test('issue #55 reopend, variant 2', () {
+      final doc = YamlEditor('''name: sample
+version: 0.1.0
+environment:
+  sdk: ^3.0.0
+dependencies:
+  retry: ^3.1.2
+dev_dependencies:
+  retry:''');
+      doc.remove(['dev_dependencies', 'retry']);
+    });
+  });
+
+  group('flow map', () {
+    test('(1)', () {
+      final doc = YamlEditor('{a: 1, b: 2, c: 3}');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('{a: 1, c: 3}'));
+    });
+
+    test('(2) ', () {
+      final doc = YamlEditor('{a: 1}');
+      doc.remove(['a']);
+      expect(doc.toString(), equals('{}'));
+    });
+
+    test('(3) ', () {
+      final doc = YamlEditor('{a: 1, b: 2}');
+      doc.remove(['a']);
+      expect(doc.toString(), equals('{ b: 2}'));
+    });
+
+    test('(4) ', () {
+      final doc =
+          YamlEditor('{"{}[],": {"{}[],": 1, b: "}{[]},", "}{[],": 3}}');
+      doc.remove(['{}[],', 'b']);
+      expect(doc.toString(), equals('{"{}[],": {"{}[],": 1, "}{[],": 3}}'));
+    });
+
+    test('empty value', () {
+      final doc = YamlEditor('{a: 1, b:, c: 3}');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('{a: 1, c: 3}'));
+    });
+
+    test('nested flow map ', () {
+      final doc = YamlEditor('{a: 1, b: {d: 4, e: 5}, c: 3}');
+      doc.remove(['b', 'd']);
+      expect(doc.toString(), equals('{a: 1, b: { e: 5}, c: 3}'));
+    });
+
+    test('nested flow map (2)', () {
+      final doc = YamlEditor('{a: {{[1] : 2}: 3, b: 2}}');
+      doc.remove([
+        'a',
+        {
+          [1]: 2
+        }
+      ]);
+      expect(doc.toString(), equals('{a: { b: 2}}'));
+    });
+  });
+
+  group('block list', () {
+    test('empty value', () {
+      final doc = YamlEditor('''
+- 0
+-
+- 2
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+- 2
+'''));
+    });
+
+    test('last element should return flow empty list', () {
+      final doc = YamlEditor('''
+- 0
+''');
+      doc.remove([0]);
+      expect(doc.toString(), equals('''
+[]
+'''));
+    });
+
+    test('last element should return flow empty list (2)', () {
+      final doc = YamlEditor('''
+a:
+  - 1
+b: [3]
+''');
+      doc.remove(['a', 0]);
+      expect(doc.toString(), equals('''
+a:
+  []
+b: [3]
+'''));
+    });
+
+    test('last element should return flow empty list (3)', () {
+      final doc = YamlEditor('''
+a:
+  - 1
+b:
+  - 3
+''');
+      doc.remove(['a', 0]);
+      expect(doc.toString(), equals('''
+a:
+  []
+b:
+  - 3
+'''));
+    });
+
+    test('(1) ', () {
+      final doc = YamlEditor('''
+- 0
+- 1
+- 2
+- 3
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+- 2
+- 3
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('''
+- 0
+- [1,2,3]
+- 2
+- 3
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+- 2
+- 3
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('''
+- 0
+- {a: 1, b: 2}
+- 2
+- 3
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+- 2
+- 3
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('last element', () {
+      final doc = YamlEditor('''
+- 0
+- 1
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+'''));
+      expectYamlBuilderValue(doc, [0]);
+    });
+
+    test('with comments', () {
+      final doc = YamlEditor('''
+- 0 # comment 0
+# comment 1
+- 1 # comment 2
+# comment 3
+- 2 # comment 4
+- 3
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0 # comment 0
+# comment 1
+# comment 3
+- 2 # comment 4
+- 3
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('nested list', () {
+      final doc = YamlEditor('''
+- - - 0
+    - 1
+''');
+      doc.remove([0, 0, 0]);
+      expect(doc.toString(), equals('''
+- - - 1
+'''));
+      expectYamlBuilderValue(doc, [
+        [
+          [1]
+        ]
+      ]);
+    });
+
+    test('nested list (2)', () {
+      final doc = YamlEditor('''
+- - 0
+  - 1
+- 2
+''');
+      doc.remove([0]);
+      expect(doc.toString(), equals('''
+- 2
+'''));
+      expectYamlBuilderValue(doc, [2]);
+    });
+
+    test('nested list (3)', () {
+      final doc = YamlEditor('''
+- - 0
+  - 1
+- 2
+''');
+      doc.remove([0, 1]);
+      expect(doc.toString(), equals('''
+- - 0
+- 2
+'''));
+      expectYamlBuilderValue(doc, [
+        [0],
+        2
+      ]);
+    });
+
+    test('nested list (4)', () {
+      final doc = YamlEditor('''
+-
+  - - 0
+    - 1
+  - 2
+''');
+      doc.remove([0, 0, 1]);
+      expect(doc.toString(), equals('''
+-
+  - - 0
+  - 2
+'''));
+      expectYamlBuilderValue(doc, [
+        [
+          [0],
+          2
+        ]
+      ]);
+    });
+
+    test('nested list (5)', () {
+      final doc = YamlEditor('''
+- - 0
+  -
+    1
+''');
+      doc.remove([0, 0]);
+      expect(doc.toString(), equals('''
+- -
+    1
+'''));
+      expectYamlBuilderValue(doc, [
+        [1]
+      ]);
+    });
+
+    test('nested list (6)', () {
+      final doc = YamlEditor('''
+- - 0 # -
+  # -
+  -
+    1
+''');
+      doc.remove([0, 0]);
+      expect(doc.toString(), equals('''
+-   # -
+  -
+    1
+'''));
+      expectYamlBuilderValue(doc, [
+        [1]
+      ]);
+    });
+
+    test('nested map', () {
+      final doc = YamlEditor('''
+- - a: b
+    c: d
+''');
+      doc.remove([0, 0, 'a']);
+      expect(doc.toString(), equals('''
+- - c: d
+'''));
+      expectYamlBuilderValue(doc, [
+        [
+          {'c': 'd'}
+        ]
+      ]);
+    });
+
+    test('nested map (2)', () {
+      final doc = YamlEditor('''
+- a:
+    - 0
+    - 1
+  c: d
+''');
+      doc.remove([0, 'a', 1]);
+      expect(doc.toString(), equals('''
+- a:
+    - 0
+  c: d
+'''));
+      expectYamlBuilderValue(doc, [
+        {
+          'a': [0],
+          'c': 'd'
+        }
+      ]);
+    });
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[1, 2, 3]');
+      doc.remove([1]);
+      expect(doc.toString(), equals('[1, 3]'));
+      expectYamlBuilderValue(doc, [1, 3]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('[1, "b", "c"]');
+      doc.remove([0]);
+      expect(doc.toString(), equals('[ "b", "c"]'));
+      expectYamlBuilderValue(doc, ['b', 'c']);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('[1, {a: 1}, "c"]');
+      doc.remove([1]);
+      expect(doc.toString(), equals('[1, "c"]'));
+      expectYamlBuilderValue(doc, [1, 'c']);
+    });
+
+    test('(4) ', () {
+      final doc = YamlEditor('["{}", b, "}{"]');
+      doc.remove([1]);
+      expect(doc.toString(), equals('["{}", "}{"]'));
+    });
+
+    test('(5) ', () {
+      final doc = YamlEditor('["{}[],", [test, "{}[],", "{}[],"], "{}[],"]');
+      doc.remove([1, 0]);
+      expect(doc.toString(), equals('["{}[],", [ "{}[],", "{}[],"], "{}[],"]'));
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/source_edit_test.dart b/pkgs/yaml_edit/test/source_edit_test.dart
new file mode 100644
index 0000000..693455e
--- /dev/null
+++ b/pkgs/yaml_edit/test/source_edit_test.dart
@@ -0,0 +1,139 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+void main() {
+  group('SourceEdit', () {
+    group('fromJson', () {
+      test('converts from jsonEncode', () {
+        final sourceEditMap = {
+          'offset': 1,
+          'length': 2,
+          'replacement': 'replacement string'
+        };
+        final sourceEdit = SourceEdit.fromJson(sourceEditMap);
+
+        expect(sourceEdit.offset, 1);
+        expect(sourceEdit.length, 2);
+        expect(sourceEdit.replacement, 'replacement string');
+      });
+
+      test('throws formatException if offset is non-int', () {
+        final sourceEditJson = {
+          'offset': '1',
+          'length': 2,
+          'replacement': 'replacement string'
+        };
+
+        expect(
+            () => SourceEdit.fromJson(sourceEditJson), throwsFormatException);
+      });
+
+      test('throws formatException if length is non-int', () {
+        final sourceEditJson = {
+          'offset': 1,
+          'length': '2',
+          'replacement': 'replacement string'
+        };
+
+        expect(
+            () => SourceEdit.fromJson(sourceEditJson), throwsFormatException);
+      });
+
+      test('throws formatException if replacement is non-string', () {
+        final sourceEditJson = {'offset': 1, 'length': 2, 'replacement': 3};
+
+        expect(
+            () => SourceEdit.fromJson(sourceEditJson), throwsFormatException);
+      });
+
+      test('throws formatException if a field is not present', () {
+        final sourceEditJson = {'offset': 1, 'length': 2};
+
+        expect(
+            () => SourceEdit.fromJson(sourceEditJson), throwsFormatException);
+      });
+    });
+
+    test('toString returns a nice string representation', () {
+      final sourceEdit = SourceEdit(1, 2, 'replacement string');
+      expect(sourceEdit.toString(),
+          equals('SourceEdit(1, 2, "replacement string")'));
+    });
+
+    group('hashCode', () {
+      test('returns same value for equal SourceEdits', () {
+        final sourceEdit1 = SourceEdit(1, 2, 'replacement string');
+        final sourceEdit2 = SourceEdit(1, 2, 'replacement string');
+        expect(sourceEdit1.hashCode, equals(sourceEdit2.hashCode));
+      });
+
+      test('returns different value for equal SourceEdits', () {
+        final sourceEdit1 = SourceEdit(1, 2, 'replacement string');
+        final sourceEdit2 = SourceEdit(1, 3, 'replacement string');
+        expect(sourceEdit1.hashCode == sourceEdit2.hashCode, equals(false));
+      });
+    });
+
+    group('toJson', () {
+      test('behaves as expected', () {
+        final sourceEdit = SourceEdit(1, 2, 'replacement string');
+        final sourceEditJson = sourceEdit.toJson();
+
+        expect(
+            sourceEditJson,
+            equals({
+              'offset': 1,
+              'length': 2,
+              'replacement': 'replacement string'
+            }));
+      });
+
+      test('is compatible with fromJson', () {
+        final sourceEdit = SourceEdit(1, 2, 'replacement string');
+        final sourceEditJson = sourceEdit.toJson();
+        final newSourceEdit = SourceEdit.fromJson(sourceEditJson);
+
+        expect(newSourceEdit.offset, 1);
+        expect(newSourceEdit.length, 2);
+        expect(newSourceEdit.replacement, 'replacement string');
+      });
+    });
+
+    group('applyAll', () {
+      test('returns original string when empty list is passed in', () {
+        const original = 'YAML: YAML';
+        final result = SourceEdit.applyAll(original, []);
+
+        expect(result, original);
+      });
+      test('works with list of one SourceEdit', () {
+        const original = 'YAML: YAML';
+        final sourceEdits = [SourceEdit(6, 4, 'YAML Ain\'t Markup Language')];
+
+        final result = SourceEdit.applyAll(original, sourceEdits);
+
+        expect(result, "YAML: YAML Ain't Markup Language");
+      });
+      test('works with list of multiple SourceEdits', () {
+        const original = 'YAML: YAML';
+        final sourceEdits = [
+          SourceEdit(6, 4, "YAML Ain't Markup Language"),
+          SourceEdit(6, 4, "YAML Ain't Markup Language"),
+          SourceEdit(0, 4, "YAML Ain't Markup Language")
+        ];
+
+        final result = SourceEdit.applyAll(original, sourceEdits);
+
+        expect(
+          result,
+          "YAML Ain't Markup Language: YAML Ain't Markup Language Ain't Markup "
+          'Language',
+        );
+      });
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/special_test.dart b/pkgs/yaml_edit/test/special_test.dart
new file mode 100644
index 0000000..9e6c011
--- /dev/null
+++ b/pkgs/yaml_edit/test/special_test.dart
@@ -0,0 +1,111 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  test('test if "No" is recognized as false', () {
+    final doc = YamlEditor('''
+~: null
+false: false
+No: No
+true: true
+''');
+    doc.update([null], 'tilde');
+    doc.update([false], false);
+    doc.update(['No'], 'no');
+    doc.update([true], 'true');
+
+    expect(doc.toString(), equals('''
+~: tilde
+false: false
+No: no
+true: "true"
+'''));
+
+    expectYamlBuilderValue(
+        doc, {null: 'tilde', false: false, 'No': 'no', true: 'true'});
+  });
+
+  test('array keys are recognized', () {
+    final doc = YamlEditor('{[1,2,3]: a}');
+    doc.update([
+      [1, 2, 3]
+    ], 'sums to 6');
+
+    expect(doc.toString(), equals('{[1,2,3]: sums to 6}'));
+    expectYamlBuilderValue(doc, {
+      [1, 2, 3]: 'sums to 6'
+    });
+  });
+
+  test('map keys are recognized', () {
+    final doc = YamlEditor('{{a: 1}: a}');
+    doc.update([
+      {'a': 1}
+    ], 'sums to 6');
+
+    expect(doc.toString(), equals('{{a: 1}: sums to 6}'));
+    expectYamlBuilderValue(doc, {
+      {'a': 1}: 'sums to 6'
+    });
+  });
+
+  test('documents can have directives', () {
+    final doc = YamlEditor('''%YAML 1.2
+--- text''');
+    doc.update([], 'test');
+
+    expect(doc.toString(), equals('%YAML 1.2\n--- test'));
+    expectYamlBuilderValue(doc, 'test');
+  });
+
+  test('tags should be removed if value is changed', () {
+    final doc = YamlEditor('''
+ - !!str a
+ - b
+ - !!int 42
+ - d
+''');
+    doc.update([2], 'test');
+
+    expect(doc.toString(), equals('''
+ - !!str a
+ - b
+ - test
+ - d
+'''));
+    expectYamlBuilderValue(doc, ['a', 'b', 'test', 'd']);
+  });
+
+  test('tags should be removed if key is changed', () {
+    final doc = YamlEditor('''
+!!str a: b
+c: !!int 42
+e: !!str f
+g: h
+!!str 23: !!bool false
+''');
+    doc.remove(['23']);
+
+    expect(doc.toString(), equals('''
+!!str a: b
+c: !!int 42
+e: !!str f
+g: h
+'''));
+    expectYamlBuilderValue(doc, {'a': 'b', 'c': 42, 'e': 'f', 'g': 'h'});
+  });
+
+  test('detect invalid extra closing bracket', () {
+    final doc = YamlEditor('''[ a, b ]''');
+    doc.appendToList([], 'c ]');
+
+    expect(doc.toString(), equals('''[ a, b , "c ]"]'''));
+    expectYamlBuilderValue(doc, ['a', 'b', 'c ]']);
+  });
+}
diff --git a/pkgs/yaml_edit/test/splice_test.dart b/pkgs/yaml_edit/test/splice_test.dart
new file mode 100644
index 0000000..3efeea1
--- /dev/null
+++ b/pkgs/yaml_edit/test/splice_test.dart
@@ -0,0 +1,191 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  test(
+      'throws RangeError if invalid index + deleteCount combination is '
+      'passed in', () {
+    final doc = YamlEditor('[0, 0]');
+    expect(() => doc.spliceList([], 1, 5, [1, 2]), throwsRangeError);
+  });
+
+  group('block list', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+- 0
+- 0
+''');
+      final nodes = doc.spliceList([], 1, 1, [1, 2]);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2
+'''));
+
+      expectDeepEquals(nodes.toList(), [0]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('''
+- 0
+- 0
+''');
+      final nodes = doc.spliceList([], 0, 2, [0, 1, 2]);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2
+'''));
+
+      expectDeepEquals(nodes.toList(), [0, 0]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('''
+- Jan
+- March
+- April
+- June
+''');
+      final nodes = doc.spliceList([], 1, 0, ['Feb']);
+      expect(doc.toString(), equals('''
+- Jan
+- Feb
+- March
+- April
+- June
+'''));
+
+      expectDeepEquals(nodes.toList(), []);
+
+      final nodes2 = doc.spliceList([], 4, 1, ['May']);
+      expect(doc.toString(), equals('''
+- Jan
+- Feb
+- March
+- April
+- May
+'''));
+
+      expectDeepEquals(nodes2.toList(), ['June']);
+    });
+
+    test('nested block list (inline)', () {
+      final doc = YamlEditor('''
+- - Jan
+  - Tuesday
+  - April
+''');
+
+      final nodes = doc.spliceList([0], 1, 1, ['Feb', 'March']);
+
+      expectDeepEquals(nodes.toList(), ['Tuesday']);
+
+      expect(doc.toString(), equals('''
+- - Jan
+  - Feb
+  - March
+  - April
+'''));
+    });
+
+    test('nested block list (inline with multiple new lines)', () {
+      final doc = YamlEditor('''
+- 
+
+
+
+
+  - Jan
+  - Tuesday
+  - April
+''');
+
+      final nodes = doc.spliceList([0], 1, 1, ['Feb', 'March']);
+
+      expectDeepEquals(nodes.toList(), ['Tuesday']);
+
+      expect(doc.toString(), equals('''
+- 
+
+
+
+
+  - Jan
+  - Feb
+  - March
+  - April
+'''));
+    });
+
+    test('update before nested list', () {
+      final doc = YamlEditor('''
+key:
+  - value
+  - another
+  - - nested
+    - continued
+''');
+
+      final nodes = doc.spliceList(['key'], 2, 0, ['spliced']);
+
+      expectDeepEquals(nodes.toList(), []);
+
+      expect(doc.toString(), equals('''
+key:
+  - value
+  - another
+  - spliced
+  - - nested
+    - continued
+'''));
+    });
+
+    test('replace nested block', () {
+      final doc = YamlEditor('''
+key:
+  - value
+  - another
+  - - nested
+    - continued
+''');
+
+      final nodes = doc.spliceList(['key'], 2, 1, ['spliced']);
+
+      expectDeepEquals(nodes.toList(), [
+        ['nested', 'continued'],
+      ]);
+
+      expect(doc.toString(), equals('''
+key:
+  - value
+  - another
+  - spliced
+'''));
+    });
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[0, 0]');
+      final nodes = doc.spliceList([], 1, 1, [1, 2]);
+      expect(doc.toString(), equals('[0, 1, 2]'));
+
+      expectDeepEquals(nodes.toList(), [0]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('[0, 0]');
+      final nodes = doc.spliceList([], 0, 2, [0, 1, 2]);
+      expect(doc.toString(), equals('[0, 1, 2]'));
+
+      expectDeepEquals(nodes.toList(), [0, 0]);
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/string_test.dart b/pkgs/yaml_edit/test/string_test.dart
new file mode 100644
index 0000000..5ea7e73
--- /dev/null
+++ b/pkgs/yaml_edit/test/string_test.dart
@@ -0,0 +1,49 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+final _testStrings = [
+  "this is a fairly' long string with\nline breaks",
+  'whitespace\n after line breaks',
+  'whitespace\n \nbetween line breaks',
+  '\n line break at the start',
+  'whitespace and line breaks at end 1\n ',
+  'whitespace and line breaks at end 2 \n \n',
+  'whitespace and line breaks at end 3 \n\n',
+  'whitespace and line breaks at end 4 \n\n ',
+  '\n\nline with multiple trailing line break \n\n\n\n\n',
+  'word',
+  'foo bar',
+  'foo\nbar',
+  '"',
+  '\'',
+  'word"word',
+  'word\'word'
+];
+
+final _scalarStyles = [
+  ScalarStyle.ANY,
+  ScalarStyle.DOUBLE_QUOTED,
+  ScalarStyle.FOLDED,
+  ScalarStyle.LITERAL,
+  ScalarStyle.PLAIN,
+  ScalarStyle.SINGLE_QUOTED,
+];
+
+void main() {
+  for (final style in _scalarStyles) {
+    for (var i = 0; i < _testStrings.length; i++) {
+      final testString = _testStrings[i];
+      test('Root $style string (${i + 1})', () {
+        final yamlEditor = YamlEditor('');
+        yamlEditor.update([], wrapAsYamlNode(testString, scalarStyle: style));
+        final yaml = yamlEditor.toString();
+        expect(loadYaml(yaml), equals(testString));
+      });
+    }
+  }
+}
diff --git a/pkgs/yaml_edit/test/test_case.dart b/pkgs/yaml_edit/test/test_case.dart
new file mode 100644
index 0000000..107e4f7
--- /dev/null
+++ b/pkgs/yaml_edit/test/test_case.dart
@@ -0,0 +1,299 @@
+// 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:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/src/utils.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+/// Interface for creating golden Test cases
+class TestCases {
+  final List<_TestCase> _testCases;
+
+  /// Creates a [TestCases] object based on test directory and golden directory
+  /// path.
+  static Future<TestCases> getTestCases(Uri testDirUri, Uri goldDirUri) async {
+    final testDir = Directory.fromUri(testDirUri);
+
+    if (!testDir.existsSync()) return TestCases([]);
+
+    /// Recursively grab all the files in the testing directory.
+    return TestCases(await testDir
+        .list(recursive: true, followLinks: false)
+        .where((entity) => entity.path.endsWith('.test'))
+        .map((entity) => entity.uri)
+        .map((inputUri) {
+      final inputWithoutExtension =
+          p.basenameWithoutExtension(inputUri.toFilePath());
+      final goldenUri = goldDirUri.resolve('./$inputWithoutExtension.golden');
+
+      return _TestCase(inputUri, goldenUri);
+    }).toList());
+  }
+
+  /// Tests all the [_TestCase]s if the golden files exist, create the golden
+  /// files otherwise.
+  void test() {
+    var tested = 0;
+    var created = 0;
+
+    for (final testCase in _testCases) {
+      testCase.testOrCreate();
+      if (testCase.state == _TestCaseStates.testedGoldenFile) {
+        tested++;
+      } else if (testCase.state == _TestCaseStates.createdGoldenFile) {
+        created++;
+      }
+    }
+
+    print('Successfully tested $tested inputs against golden files, created '
+        '$created golden files');
+  }
+
+  TestCases(this._testCases);
+
+  int get length => _testCases.length;
+}
+
+/// Enum representing the different states of [_TestCase]s.
+enum _TestCaseStates { initialized, createdGoldenFile, testedGoldenFile }
+
+/// Interface for a golden test case. Handles the logic for test conduct/golden
+/// test update accordingly.
+class _TestCase {
+  final Uri inputUri;
+  final Uri goldenUri;
+  final List<String> states = [];
+
+  late String info;
+  late YamlEditor yamlBuilder;
+  late List<_YamlModification> modifications;
+
+  String inputLineEndings = '\n';
+
+  _TestCaseStates state = _TestCaseStates.initialized;
+
+  _TestCase(this.inputUri, this.goldenUri) {
+    final inputFile = File.fromUri(inputUri);
+    if (!inputFile.existsSync()) {
+      throw Exception('Input File does not exist!');
+    }
+
+    _initialize(inputFile);
+  }
+
+  /// Initializes the [_TestCase] by reading the corresponding [inputFile] and
+  /// parsing the different portions, and then running the input yaml against
+  /// the specified modifications.
+  ///
+  /// Precondition: [inputFile] must exist, and inputs must be well-formatted.
+  void _initialize(File inputFile) {
+    final input = inputFile.readAsStringSync();
+
+    final inputLineEndings = getLineEnding(input);
+    final inputElements = input.split('---$inputLineEndings');
+
+    if (inputElements.length != 3) {
+      throw AssertionError('File ${inputFile.path} is not properly formatted.');
+    }
+
+    info = inputElements[0];
+    yamlBuilder = YamlEditor(inputElements[1]);
+    final rawModifications =
+        _getValueFromYamlNode(loadYaml(inputElements[2]) as YamlNode) as List;
+    modifications = _parseModifications(rawModifications);
+
+    /// Adds the initial state as well, so we can check that the simplest
+    /// parse -> immediately dump does not affect the string.
+    states.add(yamlBuilder.toString());
+
+    _performModifications();
+  }
+
+  void _performModifications() {
+    for (final mod in modifications) {
+      _performModification(mod);
+      states.add(yamlBuilder.toString());
+    }
+  }
+
+  void _performModification(_YamlModification mod) {
+    switch (mod.method) {
+      case YamlModificationMethod.update:
+        yamlBuilder.update(mod.path, mod.value);
+        return;
+      case YamlModificationMethod.remove:
+        yamlBuilder.remove(mod.path);
+        return;
+      case YamlModificationMethod.appendTo:
+        yamlBuilder.appendToList(mod.path, mod.value);
+        return;
+      case YamlModificationMethod.prependTo:
+        yamlBuilder.prependToList(mod.path, mod.value);
+        return;
+      case YamlModificationMethod.insert:
+        yamlBuilder.insertIntoList(mod.path, mod.index, mod.value);
+        return;
+      case YamlModificationMethod.splice:
+        yamlBuilder.spliceList(
+            mod.path, mod.index, mod.deleteCount, mod.value as List);
+        return;
+    }
+  }
+
+  void testOrCreate() {
+    final goldenFile = File.fromUri(goldenUri);
+    if (!goldenFile.existsSync()) {
+      createGoldenFile(goldenFile);
+    } else {
+      testGoldenFile(goldenFile);
+    }
+  }
+
+  void createGoldenFile(File goldenFile) {
+    /// Assumes user wants the golden file to have the same line endings as
+    /// the input file.
+    final goldenOutput = states.join('---$inputLineEndings');
+
+    goldenFile.writeAsStringSync(goldenOutput);
+    state = _TestCaseStates.createdGoldenFile;
+  }
+
+  /// Tests the golden file. Ensures that the number of states are the same, and
+  /// that the individual states are the same.
+  void testGoldenFile(File goldenFile) {
+    final inputFileName = p.basename(inputUri.toFilePath());
+    final golden = goldenFile.readAsStringSync();
+    final goldenStates = golden.split('---${getLineEnding(golden)}');
+
+    group('testing $inputFileName - input and golden files have', () {
+      test('same number of states', () {
+        expect(states.length, equals(goldenStates.length));
+      });
+
+      for (var i = 0; i < states.length; i++) {
+        test('same state $i', () {
+          expect(states[i], equals(goldenStates[i]));
+        });
+      }
+    });
+
+    state = _TestCaseStates.testedGoldenFile;
+  }
+}
+
+/// Converts [yamlList] into a Dart list.
+List _getValueFromYamlList(YamlList yamlList) {
+  return yamlList.value.map((n) {
+    if (n is YamlNode) return _getValueFromYamlNode(n);
+    return n;
+  }).toList();
+}
+
+/// Converts [yamlMap] into a Dart Map.
+Map _getValueFromYamlMap(YamlMap yamlMap) {
+  final keys = yamlMap.keys;
+  final result = {};
+  for (final key in keys) {
+    final value = yamlMap[key];
+
+    if (value is YamlNode) {
+      result[key] = _getValueFromYamlNode(value);
+    } else {
+      result[key] = value;
+    }
+  }
+
+  return result;
+}
+
+/// Converts a [YamlNode] into a Dart object.
+dynamic _getValueFromYamlNode(YamlNode node) {
+  if (node is YamlList) {
+    return _getValueFromYamlList(node);
+  }
+  if (node is YamlMap) {
+    return _getValueFromYamlMap(node);
+  }
+  return node.value;
+}
+
+/// Converts the list of modifications from the raw input to [_YamlModification]
+/// objects.
+List<_YamlModification> _parseModifications(List<dynamic> modifications) {
+  return modifications.map((mod) {
+    if (mod is! List) throw UnimplementedError();
+    Object? value;
+    var index = 0;
+    var deleteCount = 0;
+    final method = _getModificationMethod(mod[0] as String);
+
+    final path = mod[1] as List;
+
+    if (method == YamlModificationMethod.appendTo ||
+        method == YamlModificationMethod.update ||
+        method == YamlModificationMethod.prependTo) {
+      value = mod[2];
+    } else if (method == YamlModificationMethod.insert) {
+      index = mod[2] as int;
+      value = mod[3];
+    } else if (method == YamlModificationMethod.splice) {
+      index = mod[2] as int;
+      deleteCount = mod[3] as int;
+
+      if (mod[4] is! List) {
+        throw ArgumentError('Invalid array ${mod[4]} used in splice');
+      }
+
+      value = mod[4];
+    }
+
+    return _YamlModification(method, path, index, value, deleteCount);
+  }).toList();
+}
+
+/// Gets the YAML modification method corresponding to [method]
+YamlModificationMethod _getModificationMethod(String method) {
+  switch (method) {
+    case 'update':
+      return YamlModificationMethod.update;
+    case 'remove':
+      return YamlModificationMethod.remove;
+    case 'append':
+    case 'appendTo':
+      return YamlModificationMethod.appendTo;
+    case 'prepend':
+    case 'prependTo':
+      return YamlModificationMethod.prependTo;
+    case 'insert':
+    case 'insertIn':
+      return YamlModificationMethod.insert;
+    case 'splice':
+      return YamlModificationMethod.splice;
+    default:
+      throw Exception('$method not recognized!');
+  }
+}
+
+/// Class representing an abstract YAML modification to be performed
+class _YamlModification {
+  final YamlModificationMethod method;
+  final List<Object?> path;
+  final int index;
+  final dynamic value;
+  final int deleteCount;
+
+  _YamlModification(
+      this.method, this.path, this.index, this.value, this.deleteCount);
+
+  @override
+  String toString() =>
+      'method: $method, path: $path, index: $index, value: $value, '
+      'deleteCount: $deleteCount';
+}
diff --git a/pkgs/yaml_edit/test/test_utils.dart b/pkgs/yaml_edit/test/test_utils.dart
new file mode 100644
index 0000000..f60467d
--- /dev/null
+++ b/pkgs/yaml_edit/test/test_utils.dart
@@ -0,0 +1,45 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/src/equality.dart';
+import 'package:yaml_edit/src/errors.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+/// Asserts that a string containing a single YAML document is unchanged
+/// when dumped right after loading.
+void Function() expectLoadPreservesYAML(String source) {
+  final doc = YamlEditor(source);
+  return () => expect(doc.toString(), equals(source));
+}
+
+/// Asserts that [builder] has the same internal value as [expected].
+void expectYamlBuilderValue(YamlEditor builder, Object expected) {
+  final builderValue = builder.parseAt([]);
+  expectDeepEquals(builderValue, expected);
+}
+
+/// Asserts that [actual] has the same internal value as [expected].
+void expectDeepEquals(Object? actual, Object expected) {
+  expect(
+      actual, predicate((actual) => deepEquals(actual, expected), '$expected'));
+}
+
+Matcher notEquals(dynamic expected) => isNot(equals(expected));
+
+/// A matcher for functions that throw [PathError].
+Matcher throwsPathError = throwsA(isA<PathError>());
+
+/// A matcher for functions that throw [AliasException].
+Matcher throwsAliasException = throwsA(isA<AliasException>());
+
+/// Enum to hold the possible modification methods.
+enum YamlModificationMethod {
+  appendTo,
+  insert,
+  prependTo,
+  remove,
+  splice,
+  update,
+}
diff --git a/pkgs/yaml_edit/test/testdata/README.md b/pkgs/yaml_edit/test/testdata/README.md
new file mode 100644
index 0000000..5145264
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/README.md
@@ -0,0 +1,79 @@
+# Golden Testing
+
+This folder contains the files used for Golden testing performed by [golden_test.dart](../golden_test.dart).
+
+With golden testing, we are able to quickly ensure that our output conforms to our expectations given input parameters, which are extremely valuable especially on complex test cases not easily captured by unit tests.
+
+When the tests are run (see [Running Tests](#Running-Tests)), the series of specified modifications will be performed on the input, and the various output states will be compared against the `.golden` files if they exist. Otherwise, if the `.golden` files do not exist (such as in the case of a new test case), they will be created.
+
+## Table of Contents
+
+1. [Running Tests](#Running-Tests)
+1. [Input Format](#Input-Format)
+1. [Adding Test Cases](#Adding-Test-Cases)
+1. [Output Format](#Output-Format)
+
+## Running Tests
+
+By default, golden testing is performed with `pub run test`. If we only wanted to
+performed golden testing, simply do: `pub run test test/golden_test.dart`.
+
+## Input Format
+
+Input files have the following format:
+
+```
+INFORMATION (e.g. description) - parsed as text
+---
+INPUT - parsed as YAML
+---
+Modifications - parsed as YAML, must be a list.
+```
+
+The information section is meant for a brief description of your test, and other further elaboration on what your test is targeted at (e.g. modification of complex keys). The Input section should be the YAML that you wish to parse, and the modifications section should be a list of modification operations, formatted as a YAML list. The valid list of modifications are as follows:
+
+- [update, [ path ], newValue]
+- [remove, [ path ], keyOrIndex]
+- [append, [ collectionPath ], newValue]
+
+An example of what an input file might look like is:
+
+```
+BASIC LIST TEST - Ensures that the basic list operations work.
+---
+- 0
+- 1
+- 2
+- 3
+---
+- [remove, [1]]
+- [append, [], 4]
+```
+
+Note that the parser uses `\n---\n` as the delimiter to separate the different sections.
+
+## Adding Test Cases
+
+To add test cases, simple create `<your-test-name>.test` files in `/test/testdata/input` in the format explained in [Input Format](#Input-Format). When the test script is first run, the respective `.golden` files will be created in `/test/testdata/output`, you should check to ensure that the output is as expected since future collaborators will be counting on your output!
+
+## Output Format
+
+The output `.golden` files contain a series of YAML strings representing the state of the YAML after each specified modification, with the first string being the initial state as specified in the output. These states are separated by `\n---\n` as a delimiter. For example, the output file for the sample input file above is:
+
+```
+- 0
+- 1
+- 2
+- 3
+---
+- 0
+- 2
+- 3
+---
+- 0
+- 2
+- 3
+- 4
+```
+
+The first state is the input, the second state is the first state with the removal of the element at index 1, and the last state is the result of the second state with the insertion of the element 4 into the list.
diff --git a/pkgs/yaml_edit/test/testdata/input/allowed_characters_in_keys.test b/pkgs/yaml_edit/test/testdata/input/allowed_characters_in_keys.test
new file mode 100644
index 0000000..1057df0
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/allowed_characters_in_keys.test
@@ -0,0 +1,9 @@
+ALLOWED CHARACTERS IN KEYS
+---
+test: test
+---
+- [update, ["a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~"], safe]
+- [update, ['?foo'], safe question mark]
+- [update, [':foo'], safe colon]
+- [update, ['-foo'], safe dash]
+- [update, ['this is#not'], a comment]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/basic.test b/pkgs/yaml_edit/test/testdata/input/basic.test
new file mode 100644
index 0000000..02c082d
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/basic.test
@@ -0,0 +1,5 @@
+BASIC TEST
+---
+YAML: YAML Ain't Markup Language
+---
+- [update, ['YAML'], hi]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/basic_list.test b/pkgs/yaml_edit/test/testdata/input/basic_list.test
new file mode 100644
index 0000000..3ba942b
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/basic_list.test
@@ -0,0 +1,12 @@
+BASIC LIST TEST - Ensures that the basic list operations work.
+---
+- 0
+- 1
+- 2
+- 3
+---
+- [remove, [1]]
+- [append, [], 4]
+- [update, [2], 5]
+- [prepend, [], 6]
+- [splice, [], 2, 3, [7, 8]]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/ignore_key_order.test b/pkgs/yaml_edit/test/testdata/input/ignore_key_order.test
new file mode 100644
index 0000000..e9d6201
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/ignore_key_order.test
@@ -0,0 +1,7 @@
+IGNORES KEY ORDER
+---
+Z: 1
+D: 2
+F: 3
+---
+- [update, ['A'], 4]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/issue_55.test b/pkgs/yaml_edit/test/testdata/input/issue_55.test
new file mode 100644
index 0000000..ff1ac90
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/issue_55.test
@@ -0,0 +1,11 @@
+TEST FOR ISSUE #55 -- https://github.com/dart-lang/yaml_edit/issues/55
+---
+name: sample
+version: 0.1.0
+environment:
+  sdk: ^3.0.0
+dependencies:
+dev_dependencies:
+  retry:
+---
+ - [remove, ['dev_dependencies', 'retry']]
diff --git a/pkgs/yaml_edit/test/testdata/input/nested_block_map.test b/pkgs/yaml_edit/test/testdata/input/nested_block_map.test
new file mode 100644
index 0000000..bc7c35b
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/nested_block_map.test
@@ -0,0 +1,11 @@
+NESTED BLOCK MAP
+---
+a: 1
+b:
+  d: 4
+  e: 5
+c: 3
+---
+- [update, ['b', 'e'], 6]
+- [update, ['b', 'e'], [1,2,3]]
+- [update, ['b', 'f'], 6]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/pubspec.test b/pkgs/yaml_edit/test/testdata/input/pubspec.test
new file mode 100644
index 0000000..5559e1d
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/pubspec.test
@@ -0,0 +1,29 @@
+TESTING WITH A SAMPLE PUBSPEC
+---
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8 # To annotate
+  # quiver_hashcode
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  # yaml
+  yaml: ^2.2.1 # For YAML
+
+dev_dependencies:
+
+---
+- [update, ['dependencies', 'yaml'], ^3.2.0]
+- [update, ['dependencies', 'retry'], ^3.0.1]
+- [remove, ['dependencies', 'meta']]
+- [update, ['dev_dependencies'], {'test': '^1.14.4'}]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/remove_block_list.test b/pkgs/yaml_edit/test/testdata/input/remove_block_list.test
new file mode 100644
index 0000000..cf61522
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/remove_block_list.test
@@ -0,0 +1,14 @@
+REMOVE FROM LIST IN BLOCK MODE
+---
+ - true
+ - test:
+ - foo: true
+   bar:
+ - baz:
+ - nested:
+     foo:
+---
+ - [remove, [1, 'test']]
+ - [remove, [2, 'bar']]
+ - [remove, [3, 'baz']]
+ - [remove, [4, 'nested', 'foo']]
diff --git a/pkgs/yaml_edit/test/testdata/input/remove_flow_map.test b/pkgs/yaml_edit/test/testdata/input/remove_flow_map.test
new file mode 100644
index 0000000..6bbf7ab
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/remove_flow_map.test
@@ -0,0 +1,44 @@
+REMOVE FROM MAP IN FLOW MODE
+---
+A: true
+B: {foo:  }
+C: {
+  foo:,bar:true
+}
+D: {
+  foo:
+  ,bar:true
+}
+E: {
+  foo: # comment
+  ,bar:true
+}
+F: {
+  # comment
+  foo:
+  ,bar:true
+}
+G: {
+  # comment
+  foo:
+  # comment
+  ,bar:true
+}
+H: {
+  foo: # comment
+  ,
+  bar:true
+}
+I: {
+  bar: true, foo: }
+J: {  foo   :  }
+---
+- [remove, [B, foo]]
+- [remove, [C, foo]]
+- [remove, [D, foo]]
+- [remove, [E, foo]]
+- [remove, [F, foo]]
+- [remove, [G, foo]]
+- [remove, [H, foo]]
+- [remove, [I, foo]]
+- [remove, [J, foo]]
diff --git a/pkgs/yaml_edit/test/testdata/input/remove_key.test b/pkgs/yaml_edit/test/testdata/input/remove_key.test
new file mode 100644
index 0000000..b28818c
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/remove_key.test
@@ -0,0 +1,6 @@
+REMOVE KEY FROM MAP
+---
+foo: true
+bar: false
+---
+- [remove, [bar]]
diff --git a/pkgs/yaml_edit/test/testdata/input/remove_key_with_trailing_comma.test b/pkgs/yaml_edit/test/testdata/input/remove_key_with_trailing_comma.test
new file mode 100644
index 0000000..ce67551
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/remove_key_with_trailing_comma.test
@@ -0,0 +1,6 @@
+REMOVE KEY FROM MAP WITH TRAILING COMMA
+---
+foo: true
+bar: false # remove this comment
+---
+- [remove, [bar]]
diff --git a/pkgs/yaml_edit/test/testdata/input/remove_nested_key.test b/pkgs/yaml_edit/test/testdata/input/remove_nested_key.test
new file mode 100644
index 0000000..201caca
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/remove_nested_key.test
@@ -0,0 +1,9 @@
+REMOVE NESTED KEY FROM MAP
+---
+A: true
+B:
+  foo: true
+  bar: true
+---
+- [remove, [B, foo]]
+- [remove, [B, bar]]
diff --git a/pkgs/yaml_edit/test/testdata/input/remove_nested_key_with_null.test b/pkgs/yaml_edit/test/testdata/input/remove_nested_key_with_null.test
new file mode 100644
index 0000000..2a66559
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/remove_nested_key_with_null.test
@@ -0,0 +1,7 @@
+REMOVE NESTED KEY FROM MAP WITH NULL
+---
+A: true
+B:
+  foo:
+---
+- [remove, [B, foo]]
diff --git a/pkgs/yaml_edit/test/testdata/input/remove_nested_key_with_trailing_comma.test b/pkgs/yaml_edit/test/testdata/input/remove_nested_key_with_trailing_comma.test
new file mode 100644
index 0000000..35ad665
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/remove_nested_key_with_trailing_comma.test
@@ -0,0 +1,7 @@
+REMOVE NESTED KEY FROM MAP WITH TRAILING COMMA
+---
+A: true
+B:
+  bar: false # remove this comment
+---
+- [remove, [B, bar]]
diff --git a/pkgs/yaml_edit/test/testdata/input/respect_key_order.test b/pkgs/yaml_edit/test/testdata/input/respect_key_order.test
new file mode 100644
index 0000000..dee95f4
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/respect_key_order.test
@@ -0,0 +1,6 @@
+RESPECTS THE KEY ORDER
+---
+A: first
+C: third
+---
+- [update, [B], second]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/spaces.test b/pkgs/yaml_edit/test/testdata/input/spaces.test
new file mode 100644
index 0000000..d74fe73
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/spaces.test
@@ -0,0 +1,8 @@
+SPACE IN STRING - update a string that starts and ends with space
+---
+a: ' space around me '
+c: 'hello world'
+---
+- [update, ['b'], ' also spaced ']
+- [update, ['d'], ' ']
+- [update, ["\ne"], with newline]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/special_keywords.test b/pkgs/yaml_edit/test/testdata/input/special_keywords.test
new file mode 100644
index 0000000..0b3b81f
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/special_keywords.test
@@ -0,0 +1,11 @@
+SPECIAL KEYWORDS 
+---
+~: null
+false: false
+No: No
+true: true
+---
+- [update, [null], tilde]
+- [update, [false], false]
+- [update, [No], no]
+- [update, [true], True]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/splice_list_in_nested_block_with_weird_spaces.test b/pkgs/yaml_edit/test/testdata/input/splice_list_in_nested_block_with_weird_spaces.test
new file mode 100644
index 0000000..ed9b990
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/splice_list_in_nested_block_with_weird_spaces.test
@@ -0,0 +1,13 @@
+SPLICE LIST IN A NESTED BLOCK LIST WITH WEIRD SPACES
+---
+key:
+   -   - bar1
+       -   bar2
+   -       - foo
+   -     -   baz
+---
+ - [splice, [key, 0], 0, 0, ['pre-bar1']]
+ - [splice, [key, 0], 2, 0, ['post-bar2']]
+ - [splice, [key, 2], 1, 0, ['post-baz']]
+ - [splice, [key, 2], 0, 0, ['pre-baz']]
+ - [splice, [key, 1], 0, 0, ['pre-foo']]
\ No newline at end of file
diff --git a/pkgs/yaml_edit/test/testdata/input/splice_list_in_nested_block_without_initial_spaces.test b/pkgs/yaml_edit/test/testdata/input/splice_list_in_nested_block_without_initial_spaces.test
new file mode 100644
index 0000000..52c01b5
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/splice_list_in_nested_block_without_initial_spaces.test
@@ -0,0 +1,13 @@
+SPLICE LIST IN A NESTED BLOCK LIST WITHOUT INITIAL SPACES
+---
+key:
+- - bar1
+  - bar2
+- - foo
+- - baz
+---
+ - [splice, [key, 0], 0, 0, ['pre-bar1']]
+ - [splice, [key, 0], 2, 0, ['post-bar2']]
+ - [splice, [key, 2], 1, 0, ['post-baz']]
+ - [splice, [key, 2], 0, 0, ['pre-baz']]
+ - [splice, [key, 1], 0, 0, ['pre-foo']]
diff --git a/pkgs/yaml_edit/test/testdata/input/splicelist_in_nested_block_list.test b/pkgs/yaml_edit/test/testdata/input/splicelist_in_nested_block_list.test
new file mode 100644
index 0000000..da6c7c3
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/input/splicelist_in_nested_block_list.test
@@ -0,0 +1,13 @@
+SLICE LIST IN NESTED BLOCK LIST
+---
+key:
+ - foo:
+   - - bar:
+ - - - false
+ - - - false
+ - - - false
+---
+- [splice, [key], 0, 0, ['pre-foo']]
+- [splice, [key, 1, 'foo', 0], 0, 1, ['test']] 
+- [splice, [key, 2], 0, 0, ['test']]
+- [splice, [key], 4, 1, ['tail-foo']]
diff --git a/pkgs/yaml_edit/test/testdata/output/allowed_characters_in_keys.golden b/pkgs/yaml_edit/test/testdata/output/allowed_characters_in_keys.golden
new file mode 100644
index 0000000..3fd7885
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/allowed_characters_in_keys.golden
@@ -0,0 +1,26 @@
+test: test
+---
+test: test
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+---
+test: test
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+?foo: safe question mark
+---
+test: test
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+?foo: safe question mark
+:foo: safe colon
+---
+test: test
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+?foo: safe question mark
+:foo: safe colon
+-foo: safe dash
+---
+test: test
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+?foo: safe question mark
+:foo: safe colon
+-foo: safe dash
+this is#not: a comment
diff --git a/pkgs/yaml_edit/test/testdata/output/basic.golden b/pkgs/yaml_edit/test/testdata/output/basic.golden
new file mode 100644
index 0000000..e200f97
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/basic.golden
@@ -0,0 +1,3 @@
+YAML: YAML Ain't Markup Language
+---
+YAML: hi
diff --git a/pkgs/yaml_edit/test/testdata/output/basic_list.golden b/pkgs/yaml_edit/test/testdata/output/basic_list.golden
new file mode 100644
index 0000000..c3a2b65
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/basic_list.golden
@@ -0,0 +1,29 @@
+- 0
+- 1
+- 2
+- 3
+---
+- 0
+- 2
+- 3
+---
+- 0
+- 2
+- 3
+- 4
+---
+- 0
+- 2
+- 5
+- 4
+---
+- 6
+- 0
+- 2
+- 5
+- 4
+---
+- 6
+- 0
+- 7
+- 8
diff --git a/pkgs/yaml_edit/test/testdata/output/ignore_key_order.golden b/pkgs/yaml_edit/test/testdata/output/ignore_key_order.golden
new file mode 100644
index 0000000..93cc9df
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/ignore_key_order.golden
@@ -0,0 +1,8 @@
+Z: 1
+D: 2
+F: 3
+---
+Z: 1
+D: 2
+F: 3
+A: 4
diff --git a/pkgs/yaml_edit/test/testdata/output/issue_55.golden b/pkgs/yaml_edit/test/testdata/output/issue_55.golden
new file mode 100644
index 0000000..f752a14
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/issue_55.golden
@@ -0,0 +1,15 @@
+name: sample
+version: 0.1.0
+environment:
+  sdk: ^3.0.0
+dependencies:
+dev_dependencies:
+  retry:
+---
+name: sample
+version: 0.1.0
+environment:
+  sdk: ^3.0.0
+dependencies:
+dev_dependencies:
+  {}
diff --git a/pkgs/yaml_edit/test/testdata/output/nested_block_map.golden b/pkgs/yaml_edit/test/testdata/output/nested_block_map.golden
new file mode 100644
index 0000000..d28bf6c
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/nested_block_map.golden
@@ -0,0 +1,30 @@
+a: 1
+b:
+  d: 4
+  e: 5
+c: 3
+---
+a: 1
+b:
+  d: 4
+  e: 6
+c: 3
+---
+a: 1
+b:
+  d: 4
+  e:
+    - 1
+    - 2
+    - 3
+c: 3
+---
+a: 1
+b:
+  d: 4
+  e:
+    - 1
+    - 2
+    - 3
+  f: 6
+c: 3
diff --git a/pkgs/yaml_edit/test/testdata/output/pubspec.golden b/pkgs/yaml_edit/test/testdata/output/pubspec.golden
new file mode 100644
index 0000000..ffbe8ba
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/pubspec.golden
@@ -0,0 +1,116 @@
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8 # To annotate
+  # quiver_hashcode
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  # yaml
+  yaml: ^2.2.1 # For YAML
+
+dev_dependencies:
+
+---
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8 # To annotate
+  # quiver_hashcode
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  # yaml
+  yaml: ^3.2.0 # For YAML
+
+dev_dependencies:
+
+---
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8 # To annotate
+  # quiver_hashcode
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  # yaml
+  retry: ^3.0.1
+  yaml: ^3.2.0 # For YAML
+
+dev_dependencies:
+
+---
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  # quiver_hashcode
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  # yaml
+  retry: ^3.0.1
+  yaml: ^3.2.0 # For YAML
+
+dev_dependencies:
+
+---
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  # quiver_hashcode
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  # yaml
+  retry: ^3.0.1
+  yaml: ^3.2.0 # For YAML
+
+dev_dependencies:
+  test: ^1.14.4
+
diff --git a/pkgs/yaml_edit/test/testdata/output/remove_block_list.golden b/pkgs/yaml_edit/test/testdata/output/remove_block_list.golden
new file mode 100644
index 0000000..7aa3880
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/remove_block_list.golden
@@ -0,0 +1,36 @@
+ - true
+ - test:
+ - foo: true
+   bar:
+ - baz:
+ - nested:
+     foo:
+---
+ - true
+ - {}
+ - foo: true
+   bar:
+ - baz:
+ - nested:
+     foo:
+---
+ - true
+ - {}
+ - foo: true
+ - baz:
+ - nested:
+     foo:
+---
+ - true
+ - {}
+ - foo: true
+ - {}
+ - nested:
+     foo:
+---
+ - true
+ - {}
+ - foo: true
+ - {}
+ - nested:
+     {}
diff --git a/pkgs/yaml_edit/test/testdata/output/remove_flow_map.golden b/pkgs/yaml_edit/test/testdata/output/remove_flow_map.golden
new file mode 100644
index 0000000..690dcab
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/remove_flow_map.golden
@@ -0,0 +1,258 @@
+A: true
+B: {foo:  }
+C: {
+  foo:,bar:true
+}
+D: {
+  foo:
+  ,bar:true
+}
+E: {
+  foo: # comment
+  ,bar:true
+}
+F: {
+  # comment
+  foo:
+  ,bar:true
+}
+G: {
+  # comment
+  foo:
+  # comment
+  ,bar:true
+}
+H: {
+  foo: # comment
+  ,
+  bar:true
+}
+I: {
+  bar: true, foo: }
+J: {  foo   :  }
+---
+A: true
+B: {}
+C: {
+  foo:,bar:true
+}
+D: {
+  foo:
+  ,bar:true
+}
+E: {
+  foo: # comment
+  ,bar:true
+}
+F: {
+  # comment
+  foo:
+  ,bar:true
+}
+G: {
+  # comment
+  foo:
+  # comment
+  ,bar:true
+}
+H: {
+  foo: # comment
+  ,
+  bar:true
+}
+I: {
+  bar: true, foo: }
+J: {  foo   :  }
+---
+A: true
+B: {}
+C: {bar:true
+}
+D: {
+  foo:
+  ,bar:true
+}
+E: {
+  foo: # comment
+  ,bar:true
+}
+F: {
+  # comment
+  foo:
+  ,bar:true
+}
+G: {
+  # comment
+  foo:
+  # comment
+  ,bar:true
+}
+H: {
+  foo: # comment
+  ,
+  bar:true
+}
+I: {
+  bar: true, foo: }
+J: {  foo   :  }
+---
+A: true
+B: {}
+C: {bar:true
+}
+D: {bar:true
+}
+E: {
+  foo: # comment
+  ,bar:true
+}
+F: {
+  # comment
+  foo:
+  ,bar:true
+}
+G: {
+  # comment
+  foo:
+  # comment
+  ,bar:true
+}
+H: {
+  foo: # comment
+  ,
+  bar:true
+}
+I: {
+  bar: true, foo: }
+J: {  foo   :  }
+---
+A: true
+B: {}
+C: {bar:true
+}
+D: {bar:true
+}
+E: {bar:true
+}
+F: {
+  # comment
+  foo:
+  ,bar:true
+}
+G: {
+  # comment
+  foo:
+  # comment
+  ,bar:true
+}
+H: {
+  foo: # comment
+  ,
+  bar:true
+}
+I: {
+  bar: true, foo: }
+J: {  foo   :  }
+---
+A: true
+B: {}
+C: {bar:true
+}
+D: {bar:true
+}
+E: {bar:true
+}
+F: {bar:true
+}
+G: {
+  # comment
+  foo:
+  # comment
+  ,bar:true
+}
+H: {
+  foo: # comment
+  ,
+  bar:true
+}
+I: {
+  bar: true, foo: }
+J: {  foo   :  }
+---
+A: true
+B: {}
+C: {bar:true
+}
+D: {bar:true
+}
+E: {bar:true
+}
+F: {bar:true
+}
+G: {bar:true
+}
+H: {
+  foo: # comment
+  ,
+  bar:true
+}
+I: {
+  bar: true, foo: }
+J: {  foo   :  }
+---
+A: true
+B: {}
+C: {bar:true
+}
+D: {bar:true
+}
+E: {bar:true
+}
+F: {bar:true
+}
+G: {bar:true
+}
+H: {
+  bar:true
+}
+I: {
+  bar: true, foo: }
+J: {  foo   :  }
+---
+A: true
+B: {}
+C: {bar:true
+}
+D: {bar:true
+}
+E: {bar:true
+}
+F: {bar:true
+}
+G: {bar:true
+}
+H: {
+  bar:true
+}
+I: {
+  bar: true}
+J: {  foo   :  }
+---
+A: true
+B: {}
+C: {bar:true
+}
+D: {bar:true
+}
+E: {bar:true
+}
+F: {bar:true
+}
+G: {bar:true
+}
+H: {
+  bar:true
+}
+I: {
+  bar: true}
+J: {}
diff --git a/pkgs/yaml_edit/test/testdata/output/remove_key.golden b/pkgs/yaml_edit/test/testdata/output/remove_key.golden
new file mode 100644
index 0000000..48b8640
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/remove_key.golden
@@ -0,0 +1,4 @@
+foo: true
+bar: false
+---
+foo: true
diff --git a/pkgs/yaml_edit/test/testdata/output/remove_key_with_trailing_comma.golden b/pkgs/yaml_edit/test/testdata/output/remove_key_with_trailing_comma.golden
new file mode 100644
index 0000000..d85be35
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/remove_key_with_trailing_comma.golden
@@ -0,0 +1,4 @@
+foo: true
+bar: false # remove this comment
+---
+foo: true
diff --git a/pkgs/yaml_edit/test/testdata/output/remove_nested_key.golden b/pkgs/yaml_edit/test/testdata/output/remove_nested_key.golden
new file mode 100644
index 0000000..011f1c1
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/remove_nested_key.golden
@@ -0,0 +1,12 @@
+A: true
+B:
+  foo: true
+  bar: true
+---
+A: true
+B:
+  bar: true
+---
+A: true
+B:
+  {}
diff --git a/pkgs/yaml_edit/test/testdata/output/remove_nested_key_with_null.golden b/pkgs/yaml_edit/test/testdata/output/remove_nested_key_with_null.golden
new file mode 100644
index 0000000..b0e771c
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/remove_nested_key_with_null.golden
@@ -0,0 +1,7 @@
+A: true
+B:
+  foo:
+---
+A: true
+B:
+  {}
diff --git a/pkgs/yaml_edit/test/testdata/output/remove_nested_key_with_trailing_comma.golden b/pkgs/yaml_edit/test/testdata/output/remove_nested_key_with_trailing_comma.golden
new file mode 100644
index 0000000..c93c46a
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/remove_nested_key_with_trailing_comma.golden
@@ -0,0 +1,7 @@
+A: true
+B:
+  bar: false # remove this comment
+---
+A: true
+B:
+  {}
diff --git a/pkgs/yaml_edit/test/testdata/output/respect_key_order.golden b/pkgs/yaml_edit/test/testdata/output/respect_key_order.golden
new file mode 100644
index 0000000..a20ecde
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/respect_key_order.golden
@@ -0,0 +1,6 @@
+A: first
+C: third
+---
+A: first
+B: second
+C: third
diff --git a/pkgs/yaml_edit/test/testdata/output/spaces.golden b/pkgs/yaml_edit/test/testdata/output/spaces.golden
new file mode 100644
index 0000000..7ee3372
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/spaces.golden
@@ -0,0 +1,17 @@
+a: ' space around me '
+c: 'hello world'
+---
+a: ' space around me '
+b: " also spaced "
+c: 'hello world'
+---
+a: ' space around me '
+b: " also spaced "
+c: 'hello world'
+d: " "
+---
+"\ne": with newline
+a: ' space around me '
+b: " also spaced "
+c: 'hello world'
+d: " "
diff --git a/pkgs/yaml_edit/test/testdata/output/special_keywords.golden b/pkgs/yaml_edit/test/testdata/output/special_keywords.golden
new file mode 100644
index 0000000..e0c179d
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/special_keywords.golden
@@ -0,0 +1,24 @@
+~: null
+false: false
+No: No
+true: true
+---
+~: tilde
+false: false
+No: No
+true: true
+---
+~: tilde
+false: false
+No: No
+true: true
+---
+~: tilde
+false: false
+No: no
+true: true
+---
+~: tilde
+false: false
+No: no
+true: true
diff --git a/pkgs/yaml_edit/test/testdata/output/splice_list_in_nested_block_with_weird_spaces.golden b/pkgs/yaml_edit/test/testdata/output/splice_list_in_nested_block_with_weird_spaces.golden
new file mode 100644
index 0000000..cfc4cc0
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/splice_list_in_nested_block_with_weird_spaces.golden
@@ -0,0 +1,50 @@
+key:
+   -   - bar1
+       -   bar2
+   -       - foo
+   -     -   baz
+---
+key:
+   -   - pre-bar1
+       - bar1
+       -   bar2
+   -       - foo
+   -     -   baz
+---
+key:
+   -   - pre-bar1
+       - bar1
+       - post-bar2
+       -   bar2
+   -       - foo
+   -     -   baz
+---
+key:
+   -   - pre-bar1
+       - bar1
+       - post-bar2
+       -   bar2
+   -       - foo
+   -     -   baz
+         - post-baz
+---
+key:
+   -   - pre-bar1
+       - bar1
+       - post-bar2
+       -   bar2
+   -       - foo
+   -     - pre-baz
+         -   baz
+         - post-baz
+---
+key:
+   -   - pre-bar1
+       - bar1
+       - post-bar2
+       -   bar2
+   -       - pre-foo
+           - foo
+   -     - pre-baz
+         -   baz
+         - post-baz
diff --git a/pkgs/yaml_edit/test/testdata/output/splice_list_in_nested_block_without_initial_spaces.golden b/pkgs/yaml_edit/test/testdata/output/splice_list_in_nested_block_without_initial_spaces.golden
new file mode 100644
index 0000000..3454bbd
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/splice_list_in_nested_block_without_initial_spaces.golden
@@ -0,0 +1,50 @@
+key:
+- - bar1
+  - bar2
+- - foo
+- - baz
+---
+key:
+- - pre-bar1
+  - bar1
+  - bar2
+- - foo
+- - baz
+---
+key:
+- - pre-bar1
+  - bar1
+  - post-bar2
+  - bar2
+- - foo
+- - baz
+---
+key:
+- - pre-bar1
+  - bar1
+  - post-bar2
+  - bar2
+- - foo
+- - baz
+  - post-baz
+---
+key:
+- - pre-bar1
+  - bar1
+  - post-bar2
+  - bar2
+- - foo
+- - pre-baz
+  - baz
+  - post-baz
+---
+key:
+- - pre-bar1
+  - bar1
+  - post-bar2
+  - bar2
+- - pre-foo
+  - foo
+- - pre-baz
+  - baz
+  - post-baz
diff --git a/pkgs/yaml_edit/test/testdata/output/splicelist_in_nested_block_list.golden b/pkgs/yaml_edit/test/testdata/output/splicelist_in_nested_block_list.golden
new file mode 100644
index 0000000..0c9d7c5
--- /dev/null
+++ b/pkgs/yaml_edit/test/testdata/output/splicelist_in_nested_block_list.golden
@@ -0,0 +1,40 @@
+key:
+ - foo:
+   - - bar:
+ - - - false
+ - - - false
+ - - - false
+---
+key:
+ - pre-foo
+ - foo:
+   - - bar:
+ - - - false
+ - - - false
+ - - - false
+---
+key:
+ - pre-foo
+ - foo:
+   - - test
+ - - - false
+ - - - false
+ - - - false
+---
+key:
+ - pre-foo
+ - foo:
+   - - test
+ - - test
+   - - false
+ - - - false
+ - - - false
+---
+key:
+ - pre-foo
+ - foo:
+   - - test
+ - - test
+   - - false
+ - - - false
+ - tail-foo
diff --git a/pkgs/yaml_edit/test/update_test.dart b/pkgs/yaml_edit/test/update_test.dart
new file mode 100644
index 0000000..01dfde0
--- /dev/null
+++ b/pkgs/yaml_edit/test/update_test.dart
@@ -0,0 +1,994 @@
+// 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:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws', () {
+    test('RangeError in list if index is negative', () {
+      final doc = YamlEditor("- YAML Ain't Markup Language");
+      expect(() => doc.update([-1], 'test'), throwsRangeError);
+    });
+
+    test('RangeError in list if index is larger than list length', () {
+      final doc = YamlEditor("- YAML Ain't Markup Language");
+      expect(() => doc.update([2], 'test'), throwsRangeError);
+    });
+
+    test('PathError in list if attempting to set a key of a scalar', () {
+      final doc = YamlEditor("- YAML Ain't Markup Language");
+      expect(() => doc.update([0, 'a'], 'a'), throwsPathError);
+    });
+
+    test('PathError in list if using a non-integer as index', () {
+      final doc = YamlEditor("{ a: ['b', 'c'] }");
+      expect(() => doc.update(['a', 'b'], 'x'), throwsPathError);
+    });
+  });
+
+  group('works on top-level', () {
+    test('empty document', () {
+      final doc = YamlEditor('');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+
+    test('replaces string in document containing only a string', () {
+      final doc = YamlEditor('test');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+
+    test('replaces top-level string to map', () {
+      final doc = YamlEditor('test');
+      doc.update([], {'a': 1});
+
+      expect(doc.toString(), equals('a: 1'));
+      expectYamlBuilderValue(doc, {'a': 1});
+    });
+
+    test('replaces top-level list', () {
+      final doc = YamlEditor('- 1');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+
+    test('replaces top-level map', () {
+      final doc = YamlEditor('a: 1');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+
+    test('replaces top-level map with comment', () {
+      final doc = YamlEditor('a: 1 # comment');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement # comment'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+  });
+
+  group('replaces in', () {
+    group('block map', () {
+      test('(1)', () {
+        final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('YAML: test'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('(2)', () {
+        final doc = YamlEditor('test: test');
+        doc.update(['test'], []);
+
+        expect(doc.toString(), equals('test: []'));
+        expectYamlBuilderValue(doc, {'test': []});
+      });
+
+      test('empty value', () {
+        final doc = YamlEditor('YAML:');
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('YAML: test'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('empty value (2)', () {
+        final doc = YamlEditor('YAML :');
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('YAML : test'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('with comment', () {
+        final doc = YamlEditor("YAML: YAML Ain't Markup Language # comment");
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('YAML: test # comment'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('nested', () {
+        final doc = YamlEditor('''
+a: 1
+b:
+  d: 4
+  e: 5
+c: 3
+''');
+        doc.update(['b', 'e'], 6);
+
+        expect(doc.toString(), equals('''
+a: 1
+b:
+  d: 4
+  e: 6
+c: 3
+'''));
+
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {'d': 4, 'e': 6},
+          'c': 3
+        });
+      });
+
+      test('nested (2)', () {
+        final doc = YamlEditor('''
+a: 1
+b: {d: 4, e: 5}
+c: 3
+''');
+        doc.update(['b', 'e'], 6);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: {d: 4, e: 6}
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {'d': 4, 'e': 6},
+          'c': 3
+        });
+      });
+
+      test('nested (3)', () {
+        final doc = YamlEditor('''
+a:
+ b: 4
+''');
+        doc.update(['a'], true);
+
+        expect(doc.toString(), equals('''
+a: true
+'''));
+
+        expectYamlBuilderValue(doc, {'a': true});
+      });
+
+      test('nested (4)', () {
+        final doc = YamlEditor('''
+a: 1
+''');
+        doc.update([
+          'a'
+        ], [
+          {'a': true, 'b': false}
+        ]);
+
+        expectYamlBuilderValue(doc, {
+          'a': [
+            {'a': true, 'b': false}
+          ]
+        });
+      });
+
+      test('nested (5)', () {
+        final doc = YamlEditor('''
+a:
+  - a: 1
+    b: 2
+  - null
+''');
+        doc.update(['a', 0], false);
+        expect(doc.toString(), equals('''
+a:
+  - false
+
+  - null
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': [false, null]
+        });
+      });
+
+      test('nested (6)', () {
+        final doc = YamlEditor('''
+a:
+  - - 1
+    - 2
+  - null
+''');
+        doc.update(['a', 0], false);
+        expect(doc.toString(), equals('''
+a:
+  - false
+
+  - null
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': [false, null]
+        });
+      });
+
+      test('nested (7)', () {
+        final doc = YamlEditor('''
+a:
+  - - 0
+b: false
+''');
+        doc.update(['a', 0], true);
+
+        expect(doc.toString(), equals('''
+a:
+  - true
+
+b: false
+'''));
+      });
+
+      test('nested (8)', () {
+        final doc = YamlEditor('''
+a:
+b: false
+''');
+        doc.update(['a'], {'retry': '3.0.1'});
+
+        expect(doc.toString(), equals('''
+a:
+  retry: 3.0.1
+b: false
+'''));
+      });
+
+      test('nested (9)', () {
+        final doc = YamlEditor('''
+# comment
+a: # comment
+# comment
+''');
+        doc.update(['a'], {'retry': '3.0.1'});
+
+        expect(doc.toString(), equals('''
+# comment
+a:
+  retry: 3.0.1 # comment
+# comment
+'''));
+      });
+
+      test('nested scalar -> flow list', () {
+        final doc = YamlEditor('''
+a: 1
+b:
+  d: 4
+  e: 5
+c: 3
+''');
+        doc.update(['b', 'e'], [1, 2, 3]);
+
+        expect(doc.toString(), equals('''
+a: 1
+b:
+  d: 4
+  e:
+    - 1
+    - 2
+    - 3
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {
+            'd': 4,
+            'e': [1, 2, 3]
+          },
+          'c': 3
+        });
+      });
+
+      test('nested block map -> scalar', () {
+        final doc = YamlEditor('''
+a: 1
+b:
+  d: 4
+  e: 5
+c: 3
+''');
+        doc.update(['b'], 2);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: 2
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {'a': 1, 'b': 2, 'c': 3});
+      });
+
+      test('nested block map -> scalar with comments', () {
+        final doc = YamlEditor('''
+a: 1
+b:
+  d: 4
+  e: 5
+
+
+# comment
+''');
+        doc.update(['b'], 2);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: 2
+
+
+# comment
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': 2,
+        });
+      });
+
+      test('nested scalar -> block map', () {
+        final doc = YamlEditor('''
+a: 1
+b:
+  d: 4
+  e: 5
+c: 3
+''');
+        doc.update(['b', 'e'], {'x': 3, 'y': 4});
+
+        expect(doc.toString(), equals('''
+a: 1
+b:
+  d: 4
+  e:
+    x: 3
+    y: 4
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {
+            'd': 4,
+            'e': {'x': 3, 'y': 4}
+          },
+          'c': 3
+        });
+      });
+
+      test('nested block map with comments', () {
+        final doc = YamlEditor('''
+a: 1
+b:
+  d: 4
+  e: 5 # comment
+c: 3
+''');
+        doc.update(['b', 'e'], 6);
+
+        expect(doc.toString(), equals('''
+a: 1
+b:
+  d: 4
+  e: 6 # comment
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {'d': 4, 'e': 6},
+          'c': 3
+        });
+      });
+
+      test('nested block map with comments (2)', () {
+        final doc = YamlEditor('''
+a: 1
+b:
+  d: 4 # comment
+# comment
+  e: 5 # comment
+# comment
+c: 3
+''');
+        doc.update(['b', 'e'], 6);
+
+        expect(doc.toString(), equals('''
+a: 1
+b:
+  d: 4 # comment
+# comment
+  e: 6 # comment
+# comment
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {'d': 4, 'e': 6},
+          'c': 3
+        });
+      });
+    });
+
+    group('flow map', () {
+      test('(1)', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('{YAML: test}'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('(2)', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(['YAML'], 'd9]zH`FoYC/>]');
+
+        expect(doc.toString(), equals('{YAML: "d9]zH`FoYC\\/>]"}'));
+        expectYamlBuilderValue(doc, {'YAML': 'd9]zH`FoYC/>]'});
+      });
+
+      test('empty value', () {
+        final doc = YamlEditor('{YAML: }');
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('{YAML: test}'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('empty value (2)', () {
+        final doc = YamlEditor('{YAML: , hi: bye}');
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('{YAML: test, hi: bye}'));
+        expectYamlBuilderValue(doc, {'YAML': 'test', 'hi': 'bye'});
+      });
+
+      test('with spacing', () {
+        final doc = YamlEditor("{ YAML:  YAML Ain't Markup Language , "
+            'XML: Extensible Markup Language , '
+            'HTML: Hypertext Markup Language }');
+        doc.update(['XML'], 'XML Markup Language');
+
+        expect(
+            doc.toString(),
+            equals("{ YAML:  YAML Ain't Markup Language , "
+                'XML: XML Markup Language, '
+                'HTML: Hypertext Markup Language }'));
+        expectYamlBuilderValue(doc, {
+          'YAML': "YAML Ain't Markup Language",
+          'XML': 'XML Markup Language',
+          'HTML': 'Hypertext Markup Language'
+        });
+      });
+    });
+
+    group('block list', () {
+      test('(1)', () {
+        final doc = YamlEditor("- YAML Ain't Markup Language");
+        doc.update([0], 'test');
+
+        expect(doc.toString(), equals('- test'));
+        expectYamlBuilderValue(doc, ['test']);
+      });
+
+      test('(2)', () {
+        final doc = YamlEditor('''
+- 1
+- 
+- 3
+''');
+        doc.update([1], 2);
+
+        expect(doc.toString(), equals('''
+- 1
+- 2 
+- 3
+'''));
+        expectYamlBuilderValue(doc, [1, 2, 3]);
+      });
+
+      test('nested (1)', () {
+        final doc = YamlEditor("- YAML Ain't Markup Language");
+        doc.update([0], [1, 2]);
+
+        expect(doc.toString(), equals('- - 1\n  - 2'));
+        expectYamlBuilderValue(doc, [
+          [1, 2]
+        ]);
+      });
+
+      test('with comment', () {
+        final doc = YamlEditor("- YAML Ain't Markup Language # comment");
+        doc.update([0], 'test');
+
+        expect(doc.toString(), equals('- test # comment'));
+        expectYamlBuilderValue(doc, ['test']);
+      });
+
+      test('with comment (2)', () {
+        final doc = YamlEditor('''
+- 1
+- # comment
+- 3
+''');
+        doc.update([1], 2);
+
+        expect(doc.toString(), equals('''
+- 1
+- 2 # comment
+- 3
+'''));
+        expectYamlBuilderValue(doc, [1, 2, 3]);
+      });
+
+      test('with comment and spaces', () {
+        final doc = YamlEditor("-  YAML Ain't Markup Language  # comment");
+        doc.update([0], 'test');
+
+        expect(doc.toString(), equals('-  test  # comment'));
+        expectYamlBuilderValue(doc, ['test']);
+      });
+
+      test('nested (2)', () {
+        final doc = YamlEditor('''
+- 0
+- - 0
+  - 1
+  - 2
+- 2
+- 3
+''');
+        doc.update([1, 1], 4);
+        expect(doc.toString(), equals('''
+- 0
+- - 0
+  - 4
+  - 2
+- 2
+- 3
+'''));
+
+        expectYamlBuilderValue(doc, [
+          0,
+          [0, 4, 2],
+          2,
+          3
+        ]);
+      });
+
+      test('nested (3)', () {
+        final doc = YamlEditor('''
+- 0
+- 1
+''');
+        doc.update([0], {'item': 'Super Hoop', 'quantity': 1});
+        doc.update([1], {'item': 'BasketBall', 'quantity': 4});
+        expect(doc.toString(), equals('''
+- item: Super Hoop
+  quantity: 1
+- item: BasketBall
+  quantity: 4
+'''));
+
+        expectYamlBuilderValue(doc, [
+          {'item': 'Super Hoop', 'quantity': 1},
+          {'item': 'BasketBall', 'quantity': 4}
+        ]);
+      });
+
+      test('nested list flow map -> scalar', () {
+        final doc = YamlEditor('''
+- 0
+- {a: 1, b: 2}
+- 2
+- 3
+''');
+        doc.update([1], 4);
+        expect(doc.toString(), equals('''
+- 0
+- 4
+- 2
+- 3
+'''));
+        expectYamlBuilderValue(doc, [0, 4, 2, 3]);
+      });
+
+      test('nested list-map-list-number update', () {
+        final doc = YamlEditor('''
+- 0
+- a:
+   - 1
+   - 2
+   - 3
+- 2
+- 3
+''');
+        doc.update([1, 'a', 0], 15);
+        expect(doc.toString(), equals('''
+- 0
+- a:
+   - 15
+   - 2
+   - 3
+- 2
+- 3
+'''));
+        expectYamlBuilderValue(doc, [
+          0,
+          {
+            'a': [15, 2, 3]
+          },
+          2,
+          3
+        ]);
+      });
+    });
+
+    group('flow list', () {
+      test('(1)', () {
+        final doc = YamlEditor("[YAML Ain't Markup Language]");
+        doc.update([0], 'test');
+
+        expect(doc.toString(), equals('[test]'));
+        expectYamlBuilderValue(doc, ['test']);
+      });
+
+      test('(2)', () {
+        final doc = YamlEditor("[YAML Ain't Markup Language]");
+        doc.update([0], [1, 2, 3]);
+
+        expect(doc.toString(), equals('[[1, 2, 3]]'));
+        expectYamlBuilderValue(doc, [
+          [1, 2, 3]
+        ]);
+      });
+
+      /// We cannot have empty values in a flow list.
+
+      test('with spacing (1)', () {
+        final doc = YamlEditor('[ 0 , 1 , 2 , 3 ]');
+        doc.update([1], 4);
+
+        expect(doc.toString(), equals('[ 0 , 4, 2 , 3 ]'));
+        expectYamlBuilderValue(doc, [0, 4, 2, 3]);
+      });
+    });
+  });
+
+  group('adds to', () {
+    group('flow map', () {
+      test('that is empty ', () {
+        final doc = YamlEditor('{}');
+        doc.update(['a'], 1);
+        expect(doc.toString(), equals('{a: 1}'));
+        expectYamlBuilderValue(doc, {'a': 1});
+      });
+
+      test('that is empty (2)', () {
+        final doc = YamlEditor('''
+- {}
+- []
+''');
+        doc.update([0, 'a'], [1]);
+        expect(doc.toString(), equals('''
+- {a: [1]}
+- []
+'''));
+        expectYamlBuilderValue(doc, [
+          {
+            'a': [1]
+          },
+          []
+        ]);
+      });
+
+      test('(1)', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(['XML'], 'Extensible Markup Language');
+
+        expect(
+          doc.toString(),
+          equals(
+            "{YAML: YAML Ain't Markup Language, "
+            'XML: Extensible Markup Language}',
+          ),
+        );
+        expectYamlBuilderValue(doc, {
+          'XML': 'Extensible Markup Language',
+          'YAML': "YAML Ain't Markup Language",
+        });
+      });
+    });
+
+    group('block map', () {
+      test('(1)', () {
+        final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+''');
+        doc.update(['d'], 4);
+        expect(doc.toString(), equals('''
+a: 1
+b: 2
+c: 3
+d: 4
+'''));
+        expectYamlBuilderValue(doc, {'a': 1, 'b': 2, 'c': 3, 'd': 4});
+      });
+
+      // Regression testing to ensure it works without leading whitespace
+      test('(2)', () {
+        final doc = YamlEditor('a: 1');
+        doc.update(['b'], 2);
+        expect(doc.toString(), equals('''a: 1
+b: 2
+'''));
+        expectYamlBuilderValue(doc, {'a': 1, 'b': 2});
+      });
+
+      test('(3)', () {
+        final doc = YamlEditor('''
+a:
+  aa: 1
+  zz: 1
+''');
+        doc.update([
+          'a',
+          'bb'
+        ], {
+          'aaa': {'dddd': 'c'},
+          'bbb': [0, 1, 2]
+        });
+
+        expect(doc.toString(), equals('''
+a:
+  aa: 1
+  bb:
+    aaa:
+      dddd: c
+    bbb:
+      - 0
+      - 1
+      - 2
+  zz: 1
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': {
+            'aa': 1,
+            'bb': {
+              'aaa': {'dddd': 'c'},
+              'bbb': [0, 1, 2]
+            },
+            'zz': 1
+          }
+        });
+      });
+
+      test('(4)', () {
+        final doc = YamlEditor('''
+a:
+  aa: 1
+  zz: 1
+''');
+        doc.update([
+          'a',
+          'bb'
+        ], [
+          0,
+          [1, 2],
+          {'aaa': 'b', 'bbb': 'c'}
+        ]);
+
+        expect(doc.toString(), equals('''
+a:
+  aa: 1
+  bb:
+    - 0
+    - - 1
+      - 2
+    - aaa: b
+      bbb: c
+  zz: 1
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': {
+            'aa': 1,
+            'bb': [
+              0,
+              [1, 2],
+              {'aaa': 'b', 'bbb': 'c'}
+            ],
+            'zz': 1
+          }
+        });
+      });
+
+      test('with complex keys', () {
+        final doc = YamlEditor('''
+? Sammy Sosa
+? Ken Griff''');
+        doc.update(['Mark McGwire'], null);
+        expect(doc.toString(), equals('''
+? Sammy Sosa
+? Ken Griff
+Mark McGwire: null
+'''));
+        expectYamlBuilderValue(
+            doc, {'Sammy Sosa': null, 'Ken Griff': null, 'Mark McGwire': null});
+      });
+
+      test('with trailing newline', () {
+        final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+
+
+''');
+        doc.update(['d'], 4);
+        expect(doc.toString(), equals('''
+a: 1
+b: 2
+c: 3
+d: 4
+
+
+'''));
+        expectYamlBuilderValue(doc, {'a': 1, 'b': 2, 'c': 3, 'd': 4});
+      });
+
+      test('adds an empty map properly', () {
+        final doc = YamlEditor('a: b');
+        doc.update(['key'], {});
+        expectYamlBuilderValue(doc, {'a': 'b', 'key': {}});
+      });
+
+      test('adds an empty map properly (2)', () {
+        final doc = YamlEditor('a: b');
+        doc.update(['a'], {'key': {}});
+        expectYamlBuilderValue(doc, {
+          'a': {'key': {}}
+        });
+      });
+
+      test('adds and preserves key order (ascending)', () {
+        final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+
+
+''');
+
+        doc.update(['d'], 4);
+        expect(doc.toString(), equals('''
+a: 1
+b: 2
+c: 3
+d: 4
+
+
+'''));
+      });
+
+      test('adds at the end when no key order is present', () {
+        final doc = YamlEditor('''
+a: 1
+c: 2
+b: 3
+''');
+
+        doc.update(['d'], 4);
+        expect(doc.toString(), equals('''
+a: 1
+c: 2
+b: 3
+d: 4
+'''));
+      });
+    });
+
+    group('empty starting document', () {
+      test('empty map', () {
+        final doc = YamlEditor('');
+        doc.update([], {'key': {}});
+        expectYamlBuilderValue(doc, {'key': {}});
+      });
+
+      test('empty map (2)', () {
+        final doc = YamlEditor('');
+        doc.update([], {});
+        expectYamlBuilderValue(doc, {});
+      });
+
+      test('empty map (3)', () {
+        final doc = YamlEditor('');
+        doc.update(
+          [],
+          wrapAsYamlNode(
+            {'key': {}},
+            collectionStyle: CollectionStyle.BLOCK,
+          ),
+        );
+        expectYamlBuilderValue(doc, {'key': {}});
+      });
+
+      test('empty map (4)', () {
+        final doc = YamlEditor('');
+        doc.update(
+          [],
+          wrapAsYamlNode(
+            {},
+            collectionStyle: CollectionStyle.BLOCK,
+          ),
+        );
+        expectYamlBuilderValue(doc, {});
+      });
+
+      test('empty list', () {
+        final doc = YamlEditor('');
+        doc.update([], {'key': []});
+        expectYamlBuilderValue(doc, {'key': []});
+      });
+
+      test('empty list (2)', () {
+        final doc = YamlEditor('');
+        doc.update([], []);
+        expectYamlBuilderValue(doc, []);
+      });
+
+      test('empty list (3)', () {
+        final doc = YamlEditor('');
+        doc.update(
+          [],
+          wrapAsYamlNode(
+            {'key': []},
+            collectionStyle: CollectionStyle.BLOCK,
+          ),
+        );
+        expectYamlBuilderValue(doc, {'key': []});
+      });
+
+      test('empty map (4)', () {
+        final doc = YamlEditor('');
+        doc.update(
+          [],
+          wrapAsYamlNode(
+            [],
+            collectionStyle: CollectionStyle.BLOCK,
+          ),
+        );
+        expectYamlBuilderValue(doc, []);
+      });
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/utils_test.dart b/pkgs/yaml_edit/test/utils_test.dart
new file mode 100644
index 0000000..57f0b2a
--- /dev/null
+++ b/pkgs/yaml_edit/test/utils_test.dart
@@ -0,0 +1,448 @@
+// 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:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/src/utils.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('indentation', () {
+    test('returns 2 for empty strings', () {
+      final doc = YamlEditor('');
+      expect(getIndentation(doc), equals(2));
+    });
+
+    test('returns 2 for strings consisting only scalars', () {
+      final doc = YamlEditor('foo');
+      expect(getIndentation(doc), equals(2));
+    });
+
+    test('returns 2 if only top-level elements are present', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+- 3''');
+      expect(getIndentation(doc), equals(2));
+    });
+
+    test('detects the indentation used in nested list', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+- 
+   - 3
+   - 4''');
+      expect(getIndentation(doc), equals(3));
+    });
+
+    test('detects the indentation used in nested map', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c:
+   d: 4
+   e: 5''');
+      expect(getIndentation(doc), equals(3));
+    });
+
+    test('detects the indentation used in nested map in list', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+- 
+    d: 4
+    e: 5''');
+      expect(getIndentation(doc), equals(4));
+    });
+
+    test('detects the indentation used in nested map in list with complex keys',
+        () {
+      final doc = YamlEditor('''
+- 1
+- 2
+- 
+    ? d
+    : 4''');
+      expect(getIndentation(doc), equals(4));
+    });
+
+    test('detects the indentation used in nested list in map', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c:
+  - 4
+  - 5''');
+      expect(getIndentation(doc), equals(2));
+    });
+  });
+
+  group('styling options', () {
+    group('update', () {
+      test('flow map with style', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(['YAML'],
+            wrapAsYamlNode('hi', scalarStyle: ScalarStyle.DOUBLE_QUOTED));
+
+        expect(doc.toString(), equals('{YAML: "hi"}'));
+        expectYamlBuilderValue(doc, {'YAML': 'hi'});
+      });
+
+      test('prevents block scalars in flow map', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(
+            ['YAML'], wrapAsYamlNode('test', scalarStyle: ScalarStyle.FOLDED));
+
+        expect(doc.toString(), equals('{YAML: test}'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('wraps string in double-quotes if it contains dangerous characters',
+          () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(
+            ['YAML'], wrapAsYamlNode('> test', scalarStyle: ScalarStyle.PLAIN));
+
+        expect(doc.toString(), equals('{YAML: "> test"}'));
+        expectYamlBuilderValue(doc, {'YAML': '> test'});
+      });
+
+      test('list in map', () {
+        final doc = YamlEditor('''YAML: YAML Ain't Markup Language''');
+        doc.update(['YAML'],
+            wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW));
+
+        expect(doc.toString(), equals('YAML: [1, 2, 3]'));
+        expectYamlBuilderValue(doc, {
+          'YAML': [1, 2, 3]
+        });
+      });
+
+      test('nested map', () {
+        final doc = YamlEditor('''YAML: YAML Ain't Markup Language''');
+        doc.update(
+            ['YAML'],
+            wrapAsYamlNode({'YAML': "YAML Ain't Markup Language"},
+                collectionStyle: CollectionStyle.FLOW));
+
+        expect(
+            doc.toString(), equals("YAML: {YAML: YAML Ain't Markup Language}"));
+        expectYamlBuilderValue(doc, {
+          'YAML': {'YAML': "YAML Ain't Markup Language"}
+        });
+      });
+
+      test('nested list', () {
+        final doc = YamlEditor('- 0');
+        doc.update(
+            [0],
+            wrapAsYamlNode([
+              1,
+              2,
+              wrapAsYamlNode([3, 4], collectionStyle: CollectionStyle.FLOW),
+              5
+            ]));
+
+        expect(doc.toString(), equals('''
+- - 1
+  - 2
+  - [3, 4]
+  - 5'''));
+        expectYamlBuilderValue(doc, [
+          [
+            1,
+            2,
+            [3, 4],
+            5
+          ]
+        ]);
+      });
+
+      test('different scalars in block list!', () {
+        final doc = YamlEditor('- 0');
+        doc.update(
+            [0],
+            wrapAsYamlNode([
+              wrapAsYamlNode('plain string', scalarStyle: ScalarStyle.PLAIN),
+              wrapAsYamlNode('folded string', scalarStyle: ScalarStyle.FOLDED),
+              wrapAsYamlNode('single-quoted string',
+                  scalarStyle: ScalarStyle.SINGLE_QUOTED),
+              wrapAsYamlNode('literal string',
+                  scalarStyle: ScalarStyle.LITERAL),
+              wrapAsYamlNode('double-quoted string',
+                  scalarStyle: ScalarStyle.DOUBLE_QUOTED),
+            ]));
+
+        expect(doc.toString(), equals('''
+- - plain string
+  - >-
+      folded string
+  - 'single-quoted string'
+  - |-
+      literal string
+  - "double-quoted string"'''));
+        expectYamlBuilderValue(doc, [
+          [
+            'plain string',
+            'folded string',
+            'single-quoted string',
+            'literal string',
+            'double-quoted string',
+          ]
+        ]);
+      });
+
+      test('different scalars in block map!', () {
+        final doc = YamlEditor('strings: strings');
+        doc.update(
+            ['strings'],
+            wrapAsYamlNode({
+              'plain': wrapAsYamlNode('string', scalarStyle: ScalarStyle.PLAIN),
+              'folded':
+                  wrapAsYamlNode('string', scalarStyle: ScalarStyle.FOLDED),
+              'single-quoted': wrapAsYamlNode('string',
+                  scalarStyle: ScalarStyle.SINGLE_QUOTED),
+              'literal':
+                  wrapAsYamlNode('string', scalarStyle: ScalarStyle.LITERAL),
+              'double-quoted': wrapAsYamlNode('string',
+                  scalarStyle: ScalarStyle.DOUBLE_QUOTED),
+            }));
+
+        expect(doc.toString(), equals('''
+strings:
+  plain: string
+  folded: >-
+      string
+  single-quoted: 'string'
+  literal: |-
+      string
+  double-quoted: "string"'''));
+        expectYamlBuilderValue(doc, {
+          'strings': {
+            'plain': 'string',
+            'folded': 'string',
+            'single-quoted': 'string',
+            'literal': 'string',
+            'double-quoted': 'string',
+          }
+        });
+      });
+
+      test('different scalars in flow list!', () {
+        final doc = YamlEditor('[0]');
+        doc.update(
+            [0],
+            wrapAsYamlNode([
+              wrapAsYamlNode('plain string', scalarStyle: ScalarStyle.PLAIN),
+              wrapAsYamlNode('folded string', scalarStyle: ScalarStyle.FOLDED),
+              wrapAsYamlNode('single-quoted string',
+                  scalarStyle: ScalarStyle.SINGLE_QUOTED),
+              wrapAsYamlNode('literal string',
+                  scalarStyle: ScalarStyle.LITERAL),
+              wrapAsYamlNode('double-quoted string',
+                  scalarStyle: ScalarStyle.DOUBLE_QUOTED),
+            ]));
+
+        expect(
+          doc.toString(),
+          equals(
+            '[[plain string, folded string, \'single-quoted string\', '
+            'literal string, "double-quoted string"]]',
+          ),
+        );
+        expectYamlBuilderValue(doc, [
+          [
+            'plain string',
+            'folded string',
+            'single-quoted string',
+            'literal string',
+            'double-quoted string',
+          ]
+        ]);
+      });
+
+      test('wraps non-printable strings in double-quotes in flow context', () {
+        final doc = YamlEditor('[0]');
+        doc.update([0], '\x00\x07\x08\x0b\x0c\x0d\x1b\x85\xa0\u2028\u2029"');
+        expect(
+            doc.toString(), equals('["\\0\\a\\b\\v\\f\\r\\e\\N\\_\\L\\P\\""]'));
+        expectYamlBuilderValue(
+            doc, ['\x00\x07\x08\x0b\x0c\x0d\x1b\x85\xa0\u2028\u2029"']);
+      });
+
+      test('wraps non-printable strings in double-quotes in block context', () {
+        final doc = YamlEditor('- 0');
+        doc.update([0], '\x00\x07\x08\x0b\x0c\x0d\x1b\x85\xa0\u2028\u2029"');
+        expect(
+            doc.toString(), equals('- "\\0\\a\\b\\v\\f\\r\\e\\N\\_\\L\\P\\""'));
+        expectYamlBuilderValue(
+            doc, ['\x00\x07\x08\x0b\x0c\x0d\x1b\x85\xa0\u2028\u2029"']);
+      });
+
+      test('generates folded strings properly', () {
+        final doc = YamlEditor('');
+        doc.update(
+            [], wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.FOLDED));
+        expect(doc.toString(), equals('>-\n  test\n\n  test'));
+      });
+
+      test('rewrites folded strings properly', () {
+        final doc = YamlEditor('''
+- >
+    folded string
+''');
+        doc.update(
+            [0], wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.FOLDED));
+        expect(doc.toString(), equals('''
+- >-
+    test
+
+    test
+'''));
+      });
+
+      test('rewrites folded strings properly (1)', () {
+        final doc = YamlEditor('''
+- >
+    folded string''');
+        doc.update(
+            [0], wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.FOLDED));
+        expect(doc.toString(), equals('''
+- >-
+    test
+
+    test'''));
+      });
+
+      test('generates literal strings properly', () {
+        final doc = YamlEditor('');
+        doc.update(
+            [], wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.LITERAL));
+        expect(doc.toString(), equals('|-\n  test\n  test'));
+      });
+
+      test('rewrites literal strings properly', () {
+        final doc = YamlEditor('''
+- |
+    literal string
+''');
+        doc.update([0],
+            wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.LITERAL));
+        expect(doc.toString(), equals('''
+- |-
+    test
+    test
+'''));
+      });
+
+      test('prevents literal strings in flow maps, even if nested', () {
+        final doc = YamlEditor('''
+{1: 1}
+''');
+        doc.update([
+          1
+        ], [
+          wrapAsYamlNode('d9]zH`FoYC/>]', scalarStyle: ScalarStyle.LITERAL)
+        ]);
+
+        expect(doc.toString(), equals('''
+{1: ["d9]zH`FoYC\\/>]"]}
+'''));
+        expect((doc.parseAt([1, 0]) as YamlScalar).style,
+            equals(ScalarStyle.DOUBLE_QUOTED));
+      });
+
+      test('prevents literal empty strings', () {
+        final doc = YamlEditor('''
+a:
+  c: 1
+''');
+        doc.update([
+          'a'
+        ], {
+          'f': wrapAsYamlNode('', scalarStyle: ScalarStyle.LITERAL),
+          'g': 1
+        });
+
+        expect(doc.toString(), equals('''
+a:
+  f: ""
+  g: 1
+'''));
+      });
+
+      test('prevents literal strings with leading spaces', () {
+        final doc = YamlEditor('''
+a:
+  c: 1
+''');
+        doc.update([
+          'a'
+        ], {
+          'f': wrapAsYamlNode(' a', scalarStyle: ScalarStyle.LITERAL),
+          'g': 1
+        });
+
+        expect(doc.toString(), equals('''
+a:
+  f: " a"
+  g: 1
+'''));
+      });
+
+      test(
+          'flow collection structure does not get substringed when added to '
+          'block structure', () {
+        final doc = YamlEditor('''
+a:
+  - false
+''');
+        doc.prependToList(['a'],
+            wrapAsYamlNode([1234], collectionStyle: CollectionStyle.FLOW));
+        expect(doc.toString(), equals('''
+a:
+  - [1234]
+  - false
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': [
+            [1234],
+            false
+          ]
+        });
+      });
+    });
+  });
+
+  group('assertValidScalar', () {
+    test('does nothing with a boolean', () {
+      expect(() => assertValidScalar(true), returnsNormally);
+    });
+
+    test('does nothing with a number', () {
+      expect(() => assertValidScalar(1.12), returnsNormally);
+    });
+    test('does nothing with infinity', () {
+      expect(() => assertValidScalar(double.infinity), returnsNormally);
+    });
+    test('does nothing with a String', () {
+      expect(() => assertValidScalar('test'), returnsNormally);
+    });
+
+    test('does nothing with null', () {
+      expect(() => assertValidScalar(null), returnsNormally);
+    });
+
+    test('throws on map', () {
+      expect(() => assertValidScalar({'a': 1}), throwsArgumentError);
+    });
+
+    test('throws on list', () {
+      expect(() => assertValidScalar([1]), throwsArgumentError);
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/windows_test.dart b/pkgs/yaml_edit/test/windows_test.dart
new file mode 100644
index 0000000..50f79e7
--- /dev/null
+++ b/pkgs/yaml_edit/test/windows_test.dart
@@ -0,0 +1,228 @@
+// 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:test/test.dart';
+import 'package:yaml_edit/src/utils.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('windows line ending detection', () {
+    test('empty string gives not windows', () {
+      final doc = YamlEditor('');
+      expect(getLineEnding(doc.toString()), equals('\n'));
+    });
+
+    test('accurately detects windows documents', () {
+      final doc = YamlEditor('\r\n');
+      expect(getLineEnding(doc.toString()), equals('\r\n'));
+    });
+
+    test('accurately detects windows documents (2)', () {
+      final doc = YamlEditor('''
+a:\r
+  b:\r
+    - 1\r
+    - 2\r
+c: 3\r
+''');
+      expect(getLineEnding(doc.toString()), equals('\r\n'));
+    });
+  });
+
+  group('modification with windows line endings', () {
+    test('append element to simple block list ', () {
+      final doc = YamlEditor('''
+- 0\r
+- 1\r
+- 2\r
+- 3\r
+''');
+      doc.appendToList([], [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- 0\r
+- 1\r
+- 2\r
+- 3\r
+- - 4\r
+  - 5\r
+  - 6\r
+'''));
+      expectYamlBuilderValue(doc, [
+        0,
+        1,
+        2,
+        3,
+        [4, 5, 6]
+      ]);
+    });
+
+    test('update nested scalar -> flow list', () {
+      final doc = YamlEditor('''
+a: 1\r
+b:\r
+  d: 4\r
+  e: 5\r
+c: 3\r
+''');
+      doc.update(['b', 'e'], [1, 2, 3]);
+
+      expect(doc.toString(), equals('''
+a: 1\r
+b:\r
+  d: 4\r
+  e:\r
+    - 1\r
+    - 2\r
+    - 3\r
+c: 3\r
+'''));
+      expectYamlBuilderValue(doc, {
+        'a': 1,
+        'b': {
+          'd': 4,
+          'e': [1, 2, 3]
+        },
+        'c': 3
+      });
+    });
+
+    test('update in nested list flow map -> scalar', () {
+      final doc = YamlEditor('''
+- 0\r
+- {a: 1, b: 2}\r
+- 2\r
+- 3\r
+''');
+      doc.update([1], 4);
+      expect(doc.toString(), equals('''
+- 0\r
+- 4\r
+- 2\r
+- 3\r
+'''));
+      expectYamlBuilderValue(doc, [0, 4, 2, 3]);
+    });
+
+    test('insert into a list with comments', () {
+      final doc = YamlEditor('''
+- 0 # comment a\r
+- 2 # comment b\r
+''');
+      doc.insertIntoList([], 1, 1);
+      expect(doc.toString(), equals('''
+- 0 # comment a\r
+- 1\r
+- 2 # comment b\r
+'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('prepend into a list', () {
+      final doc = YamlEditor('''
+- 1\r
+- 2\r
+''');
+      doc.prependToList([], [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- - 4\r
+  - 5\r
+  - 6\r
+- 1\r
+- 2\r
+'''));
+      expectYamlBuilderValue(doc, [
+        [4, 5, 6],
+        1,
+        2
+      ]);
+    });
+
+    test('remove from block list ', () {
+      final doc = YamlEditor('''
+- 0\r
+- 1\r
+- 2\r
+- 3\r
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0\r
+- 2\r
+- 3\r
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('remove from block list (2)', () {
+      final doc = YamlEditor('''
+- 0\r
+''');
+      doc.remove([0]);
+      expect(doc.toString(), equals('''
+[]\r
+'''));
+      expectYamlBuilderValue(doc, []);
+    });
+
+    test('inserted nested map', () {
+      final doc = YamlEditor('''
+a:\r
+  b:\r
+''');
+      doc.update(
+        ['a', 'b'],
+        {
+          'c': {'d': 'e'}
+        },
+      );
+      expect(doc.toString(), equals('''
+a:\r
+  b:\r
+    c:\r
+      d: e\r
+'''));
+    });
+
+    test('remove from block map', () {
+      final doc = YamlEditor('''
+a: 1\r
+b: 2\r
+c: 3\r
+''');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('''
+a: 1\r
+c: 3\r
+'''));
+    });
+
+    test('remove from block map (2)', () {
+      final doc = YamlEditor('''
+a: 1\r
+''');
+      doc.remove(['a']);
+      expect(doc.toString(), equals('''
+{}\r
+'''));
+      expectYamlBuilderValue(doc, {});
+    });
+
+    test('splice block list', () {
+      final doc = YamlEditor('''
+- 0\r
+- 0\r
+''');
+      final nodes = doc.spliceList([], 0, 2, [0, 1, 2]);
+      expect(doc.toString(), equals('''
+- 0\r
+- 1\r
+- 2\r
+'''));
+
+      expectDeepEquals(nodes.toList(), [0, 0]);
+    });
+  });
+}
diff --git a/pkgs/yaml_edit/test/wrap_test.dart b/pkgs/yaml_edit/test/wrap_test.dart
new file mode 100644
index 0000000..60237b9
--- /dev/null
+++ b/pkgs/yaml_edit/test/wrap_test.dart
@@ -0,0 +1,373 @@
+// 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.
+
+// ignore_for_file: avoid_dynamic_calls
+
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/src/equality.dart';
+import 'package:yaml_edit/src/wrap.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('wrapAsYamlNode', () {
+    group('checks for invalid scalars', () {
+      test('fails to wrap an invalid scalar', () {
+        expect(() => wrapAsYamlNode(File('test.dart')), throwsArgumentError);
+      });
+
+      test('fails to wrap an invalid map', () {
+        expect(() => wrapAsYamlNode({'a': File('test.dart')}),
+            throwsArgumentError);
+      });
+
+      test('fails to wrap an invalid list', () {
+        expect(
+            () => wrapAsYamlNode([
+                  'a',
+                  [File('test.dart')]
+                ]),
+            throwsArgumentError);
+      });
+
+      test('checks YamlScalar for invalid scalar value', () {
+        expect(() => wrapAsYamlNode(YamlScalar.wrap(File('test.dart'))),
+            throwsArgumentError);
+      });
+
+      test('checks YamlMap for deep invalid scalar value', () {
+        expect(
+            () => wrapAsYamlNode(YamlMap.wrap({
+                  'a': {'b': File('test.dart')}
+                })),
+            throwsArgumentError);
+      });
+
+      test('checks YamlList for deep invalid scalar value', () {
+        expect(
+            () => wrapAsYamlNode(YamlList.wrap([
+                  'a',
+                  [File('test.dart')]
+                ])),
+            throwsArgumentError);
+      });
+    });
+
+    test('wraps scalars', () {
+      final scalar = wrapAsYamlNode('foo');
+
+      expect((scalar as YamlScalar).style, equals(ScalarStyle.ANY));
+      expect(scalar.value, equals('foo'));
+    });
+
+    test('wraps scalars with style', () {
+      final scalar =
+          wrapAsYamlNode('foo', scalarStyle: ScalarStyle.DOUBLE_QUOTED);
+
+      expect((scalar as YamlScalar).style, equals(ScalarStyle.DOUBLE_QUOTED));
+      expect(scalar.value, equals('foo'));
+    });
+
+    test('wraps lists', () {
+      final list = wrapAsYamlNode([
+        [1, 2, 3],
+        {
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        },
+        'value'
+      ]);
+
+      expect(
+          list,
+          equals([
+            [1, 2, 3],
+            {
+              'foo': 'bar',
+              'nested': [4, 5, 6]
+            },
+            'value'
+          ]));
+      expect((list as YamlList).style, equals(CollectionStyle.ANY));
+      expect(list[0].style, equals(CollectionStyle.ANY));
+      expect(list[1].style, equals(CollectionStyle.ANY));
+    });
+
+    test('wraps lists with collectionStyle', () {
+      final list = wrapAsYamlNode([
+        [1, 2, 3],
+        {
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        },
+        'value'
+      ], collectionStyle: CollectionStyle.BLOCK);
+
+      expect((list as YamlList).style, equals(CollectionStyle.BLOCK));
+      expect(list[0].style, equals(CollectionStyle.BLOCK));
+      expect(list[1].style, equals(CollectionStyle.BLOCK));
+    });
+
+    test('wraps nested lists while preserving style', () {
+      final list = wrapAsYamlNode([
+        wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW),
+        wrapAsYamlNode({
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        }, collectionStyle: CollectionStyle.FLOW),
+        'value'
+      ], collectionStyle: CollectionStyle.BLOCK);
+
+      expect((list as YamlList).style, equals(CollectionStyle.BLOCK));
+      expect(list[0].style, equals(CollectionStyle.FLOW));
+      expect(list[1].style, equals(CollectionStyle.FLOW));
+    });
+
+    test('wraps maps', () {
+      final map = wrapAsYamlNode({
+        'list': [1, 2, 3],
+        'map': {
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        },
+        'scalar': 'value'
+      });
+
+      expect(
+          map,
+          equals({
+            'list': [1, 2, 3],
+            'map': {
+              'foo': 'bar',
+              'nested': [4, 5, 6]
+            },
+            'scalar': 'value'
+          }));
+
+      expect((map as YamlMap).style, equals(CollectionStyle.ANY));
+    });
+
+    test('wraps maps with collectionStyle', () {
+      final map = wrapAsYamlNode({
+        'list': [1, 2, 3],
+        'map': {
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        },
+        'scalar': 'value'
+      }, collectionStyle: CollectionStyle.BLOCK);
+
+      expect((map as YamlMap).style, equals(CollectionStyle.BLOCK));
+    });
+
+    test('wraps nested maps while preserving style', () {
+      final map = wrapAsYamlNode({
+        'list':
+            wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW),
+        'map': wrapAsYamlNode({
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        }, collectionStyle: CollectionStyle.BLOCK),
+        'scalar': 'value'
+      }, collectionStyle: CollectionStyle.BLOCK);
+
+      expect((map as YamlMap).style, equals(CollectionStyle.BLOCK));
+      expect(map['list'].style, equals(CollectionStyle.FLOW));
+      expect(map['map'].style, equals(CollectionStyle.BLOCK));
+    });
+
+    test('works with YamlMap.wrap', () {
+      final map = wrapAsYamlNode({
+        'list':
+            wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW),
+        'map': YamlMap.wrap({
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        }),
+      }, collectionStyle: CollectionStyle.BLOCK);
+
+      expect((map as YamlMap).style, equals(CollectionStyle.BLOCK));
+      expect(map['list'].style, equals(CollectionStyle.FLOW));
+      expect(map['map'].style, equals(CollectionStyle.ANY));
+    });
+  });
+
+  test('applies collectionStyle recursively', () {
+    final list = wrapAsYamlNode([
+      [1, 2, 3],
+      {
+        'foo': 'bar',
+        'nested': [4, 5, 6]
+      },
+    ], collectionStyle: CollectionStyle.BLOCK);
+
+    expect((list as YamlList).style, equals(CollectionStyle.BLOCK));
+    expect(list[0].style, equals(CollectionStyle.BLOCK));
+    expect(list[1].style, equals(CollectionStyle.BLOCK));
+    expect(list[1]['nested'].style, equals(CollectionStyle.BLOCK));
+  });
+
+  test('applies scalarStyle recursively', () {
+    final list = wrapAsYamlNode([
+      ['a', 'b', 'c'],
+      {
+        'foo': 'bar',
+      },
+      'hello',
+    ], scalarStyle: ScalarStyle.SINGLE_QUOTED);
+
+    expect((list as YamlList).style, equals(CollectionStyle.ANY));
+    final item1 = list.nodes[0] as YamlList;
+    final item2 = list.nodes[1] as YamlMap;
+    final item3 = list.nodes[2] as YamlScalar;
+    expect(item1.style, equals(CollectionStyle.ANY));
+    expect(item2.style, equals(CollectionStyle.ANY));
+    expect(item3.style, equals(ScalarStyle.SINGLE_QUOTED));
+
+    final item1entry1 = item1.nodes[0] as YamlScalar;
+    expect(item1entry1.style, equals(ScalarStyle.SINGLE_QUOTED));
+
+    final item2foo = item2.nodes['foo'] as YamlScalar;
+    expect(item2foo.style, equals(ScalarStyle.SINGLE_QUOTED));
+  });
+
+  group('deepHashCode', () {
+    test('returns the same result for scalar and its value', () {
+      final hashCode1 = deepHashCode('foo');
+      final hashCode2 = deepHashCode(wrapAsYamlNode('foo'));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns different results for different values', () {
+      final hashCode1 = deepHashCode('foo');
+      final hashCode2 = deepHashCode(wrapAsYamlNode('bar'));
+
+      expect(hashCode1, notEquals(hashCode2));
+    });
+
+    test('returns the same result for YamlScalar with style and its value', () {
+      final hashCode1 = deepHashCode('foo');
+      final hashCode2 =
+          deepHashCode(wrapAsYamlNode('foo', scalarStyle: ScalarStyle.LITERAL));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test(
+        'returns the same result for two YamlScalars with same value but '
+        'different styles', () {
+      final hashCode1 =
+          deepHashCode(wrapAsYamlNode('foo', scalarStyle: ScalarStyle.PLAIN));
+      final hashCode2 =
+          deepHashCode(wrapAsYamlNode('foo', scalarStyle: ScalarStyle.LITERAL));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the same result for list and its value', () {
+      final hashCode1 = deepHashCode([1, 2, 3]);
+      final hashCode2 = deepHashCode(wrapAsYamlNode([1, 2, 3]));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the same result for list and the YamlList.wrap() value', () {
+      final hashCode1 = deepHashCode([
+        1,
+        [1, 2],
+        3
+      ]);
+      final hashCode2 = deepHashCode(YamlList.wrap([
+        1,
+        YamlList.wrap([1, 2]),
+        3
+      ]));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the different results for different lists', () {
+      final hashCode1 = deepHashCode([1, 2, 3]);
+      final hashCode2 = deepHashCode([1, 2, 4]);
+      final hashCode3 = deepHashCode([1, 2, 3, 4]);
+
+      expect(hashCode1, notEquals(hashCode2));
+      expect(hashCode2, notEquals(hashCode3));
+      expect(hashCode3, notEquals(hashCode1));
+    });
+
+    test('returns the same result for YamlList with style and its value', () {
+      final hashCode1 = deepHashCode([1, 2, 3]);
+      final hashCode2 = deepHashCode(
+          wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test(
+        'returns the same result for two YamlLists with same value but '
+        'different styles', () {
+      final hashCode1 = deepHashCode(
+          wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.BLOCK));
+      final hashCode2 = deepHashCode(wrapAsYamlNode([1, 2, 3]));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the same result for a map and its value', () {
+      final hashCode1 = deepHashCode({'a': 1, 'b': 2});
+      final hashCode2 = deepHashCode(wrapAsYamlNode({'a': 1, 'b': 2}));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the same result for list and the YamlList.wrap() value', () {
+      final hashCode1 = deepHashCode({
+        'a': 1,
+        'b': 2,
+        'c': {'d': 4, 'e': 5}
+      });
+      final hashCode2 = deepHashCode(YamlMap.wrap({
+        'a': 1,
+        'b': 2,
+        'c': YamlMap.wrap({'d': 4, 'e': 5})
+      }));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the different results for different maps', () {
+      final hashCode1 = deepHashCode({'a': 1, 'b': 2});
+      final hashCode2 = deepHashCode({'a': 1, 'b': 3});
+      final hashCode3 = deepHashCode({'a': 1, 'b': 2, 'c': 3});
+
+      expect(hashCode1, notEquals(hashCode2));
+      expect(hashCode2, notEquals(hashCode3));
+      expect(hashCode3, notEquals(hashCode1));
+    });
+
+    test('returns the same result for YamlMap with style and its value', () {
+      final hashCode1 = deepHashCode({'a': 1, 'b': 2});
+      final hashCode2 = deepHashCode(wrapAsYamlNode({'a': 1, 'b': 2},
+          collectionStyle: CollectionStyle.FLOW));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test(
+        'returns the same result for two YamlMaps with same value but '
+        'different styles', () {
+      final hashCode1 = deepHashCode(wrapAsYamlNode({'a': 1, 'b': 2},
+          collectionStyle: CollectionStyle.BLOCK));
+      final hashCode2 = deepHashCode(wrapAsYamlNode({'a': 1, 'b': 2},
+          collectionStyle: CollectionStyle.FLOW));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+  });
+}