Merge pull request #1658 from dart-lang/merge-string_scanner-package

Merge `package:string_scanner`
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/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/labeler.yml b/.github/labeler.yml
index 1cc4b20..bfef316 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -108,6 +108,26 @@
   - changed-files:
     - any-glob-to-any-file: 'pkgs/sse/**'
 
+'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/**'
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/clock.yaml b/.github/workflows/clock.yaml
index aef0895..a09a601 100644
--- a/.github/workflows/clock.yaml
+++ b/.github/workflows/clock.yaml
@@ -5,12 +5,12 @@
   push:
     branches: [ main ]
     paths:
-      - '.github/workflows/clock.yml'
+      - '.github/workflows/clock.yaml'
       - 'pkgs/clock/**'
   pull_request:
     branches: [ main ]
     paths:
-      - '.github/workflows/clock.yml'
+      - '.github/workflows/clock.yaml'
       - 'pkgs/clock/**'
   schedule:
     - cron: "0 0 * * 0"
diff --git a/.github/workflows/stream_transform.yaml b/.github/workflows/stream_transform.yaml
new file mode 100644
index 0000000..a36a776
--- /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.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 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/README.md b/README.md
index 79d1dde..d1a1d04 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,12 @@
 | [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) |
+| [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) |
 
 ## 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/source_maps/CHANGELOG.md b/pkgs/source_maps/CHANGELOG.md
index ae7711e..b06ac72 100644
--- a/pkgs/source_maps/CHANGELOG.md
+++ b/pkgs/source_maps/CHANGELOG.md
@@ -1,3 +1,5 @@
+## 0.10.14-wip
+
 ## 0.10.13
 
 * Require Dart 3.3
diff --git a/pkgs/source_maps/lib/builder.dart b/pkgs/source_maps/lib/builder.dart
index 54ba743..9043c63 100644
--- a/pkgs/source_maps/lib/builder.dart
+++ b/pkgs/source_maps/lib/builder.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Contains a builder object useful for creating source maps programatically.
-library source_maps.builder;
+library;
 
 // TODO(sigmund): add a builder for multi-section mappings.
 
diff --git a/pkgs/source_maps/lib/parser.dart b/pkgs/source_maps/lib/parser.dart
index b699ac7..590dfc6 100644
--- a/pkgs/source_maps/lib/parser.dart
+++ b/pkgs/source_maps/lib/parser.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Contains the top-level function to parse source maps version 3.
-library source_maps.parser;
+library;
 
 import 'dart:convert';
 
diff --git a/pkgs/source_maps/lib/printer.dart b/pkgs/source_maps/lib/printer.dart
index 17733cd..32523d6 100644
--- a/pkgs/source_maps/lib/printer.dart
+++ b/pkgs/source_maps/lib/printer.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Contains a code printer that generates code by recording the source maps.
-library source_maps.printer;
+library;
 
 import 'package:source_span/source_span.dart';
 
diff --git a/pkgs/source_maps/lib/refactor.dart b/pkgs/source_maps/lib/refactor.dart
index 98e0c93..a518a0c 100644
--- a/pkgs/source_maps/lib/refactor.dart
+++ b/pkgs/source_maps/lib/refactor.dart
@@ -6,7 +6,7 @@
 ///
 /// [TextEditTransaction] supports making a series of changes to a text buffer.
 /// [guessIndent] helps to guess the appropriate indentiation for the new code.
-library source_maps.refactor;
+library;
 
 import 'package:source_span/source_span.dart';
 
diff --git a/pkgs/source_maps/lib/source_maps.dart b/pkgs/source_maps/lib/source_maps.dart
index 58f805a..244dee7 100644
--- a/pkgs/source_maps/lib/source_maps.dart
+++ b/pkgs/source_maps/lib/source_maps.dart
@@ -24,7 +24,7 @@
 /// var mapping = parse(json);
 /// mapping.spanFor(outputSpan1.line, outputSpan1.column)
 /// ```
-library source_maps;
+library;
 
 import 'package:source_span/source_span.dart';
 
diff --git a/pkgs/source_maps/lib/src/utils.dart b/pkgs/source_maps/lib/src/utils.dart
index f70531e..ba04fbb 100644
--- a/pkgs/source_maps/lib/src/utils.dart
+++ b/pkgs/source_maps/lib/src/utils.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Utilities that shouldn't be in this package.
-library source_maps.utils;
+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,
diff --git a/pkgs/source_maps/lib/src/vlq.dart b/pkgs/source_maps/lib/src/vlq.dart
index 61b4768..3b0562d 100644
--- a/pkgs/source_maps/lib/src/vlq.dart
+++ b/pkgs/source_maps/lib/src/vlq.dart
@@ -10,7 +10,7 @@
 /// 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 source_maps.src.vlq;
+library;
 
 import 'dart:math';
 
diff --git a/pkgs/source_maps/pubspec.yaml b/pkgs/source_maps/pubspec.yaml
index 8518fa7..32cbf4f 100644
--- a/pkgs/source_maps/pubspec.yaml
+++ b/pkgs/source_maps/pubspec.yaml
@@ -1,5 +1,5 @@
 name: source_maps
-version: 0.10.13
+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
 
@@ -10,6 +10,6 @@
   source_span: ^1.8.0
 
 dev_dependencies:
-  dart_flutter_team_lints: ^2.0.0
+  dart_flutter_team_lints: ^3.0.0
   term_glyph: ^1.2.0
   test: ^1.16.0
diff --git a/pkgs/source_maps/test/common.dart b/pkgs/source_maps/test/common.dart
index f6139de..e225ff5 100644
--- a/pkgs/source_maps/test/common.dart
+++ b/pkgs/source_maps/test/common.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Common input/output used by builder, parser and end2end tests
-library test.common;
+library;
 
 import 'package:source_maps/source_maps.dart';
 import 'package:source_span/source_span.dart';
diff --git a/pkgs/source_maps/test/utils_test.dart b/pkgs/source_maps/test/utils_test.dart
index 4abdce2..2516d1e 100644
--- a/pkgs/source_maps/test/utils_test.dart
+++ b/pkgs/source_maps/test/utils_test.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Tests for the binary search utility algorithm.
-library test.utils_test;
+library;
 
 import 'package:source_maps/src/utils.dart';
 import 'package:test/test.dart';
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..a71b2fb
--- /dev/null
+++ b/pkgs/stream_transform/CHANGELOG.md
@@ -0,0 +1,185 @@
+## 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..70b3e7f
--- /dev/null
+++ b/pkgs/stream_transform/example/main.dart
@@ -0,0 +1,26 @@
+// 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:html';
+
+import 'package:stream_transform/stream_transform.dart';
+
+void main() {
+  var firstInput = document.querySelector('#first_input') as InputElement;
+  var secondInput = document.querySelector('#second_input') as InputElement;
+  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(InputElement 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..c239220
--- /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(_) {}
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..1e2298a
--- /dev/null
+++ b/pkgs/stream_transform/pubspec.yaml
@@ -0,0 +1,13 @@
+name: stream_transform
+version: 2.1.1
+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.1.0
+
+dev_dependencies:
+  async: ^2.5.0
+  dart_flutter_team_lints: ^2.0.0
+  fake_async: ^1.3.0
+  test: ^1.16.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/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..b7359cf
--- /dev/null
+++ b/pkgs/term_glyph/CHANGELOG.md
@@ -0,0 +1,31 @@
+## 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..7c97d7f
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/ascii_glyph_set.dart
@@ -0,0 +1,137 @@
+// 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.
+
+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..be1a668
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/glyph_set.dart
@@ -0,0 +1,227 @@
+// 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.
+
+/// 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..925903e
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/top_level.dart
@@ -0,0 +1,383 @@
+// 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.
+
+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..1ddd165
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/unicode_glyph_set.dart
@@ -0,0 +1,137 @@
+// 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.
+
+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..c429307
--- /dev/null
+++ b/pkgs/term_glyph/pubspec.yaml
@@ -0,0 +1,13 @@
+name: term_glyph
+version: 1.2.2
+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
+  dart_style: ^2.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..f5cdade
--- /dev/null
+++ b/pkgs/term_glyph/tool/generate.dart
@@ -0,0 +1,153 @@
+// 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('lib/src/generated').createSync(recursive: true);
+
+  _writeGlyphSetInterface(data);
+  _writeGlyphSet(data, ascii: false);
+  _writeGlyphSet(data, ascii: true);
+  _writeTopLevel(data);
+
+  final result = Process.runSync(
+      'pub', ['run', 'dart_style:format', '-w', 'lib/src/generated']);
+  print(result.stderr);
+  exit(result.exitCode);
+}
+
+/// Writes `lib/src/generated/glyph_set.dart`.
+void _writeGlyphSetInterface(List<List<dynamic>> data) {
+  final file =
+      File('lib/src/generated/glyph_set.dart').openSync(mode: FileMode.write);
+  file.writeStringSync(r'''
+    // 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.
+
+    /// 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('lib/src/generated/${ascii ? "ascii" : "unicode"}_glyph_set.dart')
+          .openSync(mode: FileMode.write);
+
+  final className = '${ascii ? "Ascii" : "Unicode"}GlyphSet';
+  file.writeStringSync('''
+    // 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.
+
+    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('lib/src/generated/top_level.dart').openSync(mode: FileMode.write);
+
+  file.writeStringSync('''
+    // 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.
+
+    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'";
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;
+}