From 8219b54b37c891d90f8b3c5b07eefac7aed1db3a Mon Sep 17 00:00:00 2001
From: Sigurd Meldgaard <sigurdm@google.com>
Date: Tue, 22 Oct 2024 09:27:04 +0000
Subject: [PATCH 1/3] Prefer installation as a dev-dependency

---
 example/README.md                          |  12 +-
 example/pubspec.yaml                       |   6 +
 webdev/CHANGELOG.md                        |   4 +
 webdev/README.md                           |  26 +--
 webdev/bin/webdev.dart                     |  13 --
 webdev/lib/src/command/build_command.dart  |  11 +-
 webdev/lib/src/command/daemon_command.dart |   8 -
 webdev/lib/src/command/serve_command.dart  |   8 -
 webdev/lib/src/command/shared.dart         |   8 -
 webdev/lib/src/pubspec.dart                | 220 --------------------
 webdev/lib/src/webdev_command_runner.dart  |   1 -
 webdev/test/e2e_test.dart                  |   5 -
 webdev/test/installation_test.dart         | 230 ++++++---------------
 13 files changed, 79 insertions(+), 473 deletions(-)
 delete mode 100644 webdev/lib/src/pubspec.dart

diff --git a/example/README.md b/example/README.md
index 5c29b42b3..b8f3e6d77 100644
--- a/example/README.md
+++ b/example/README.md
@@ -1,13 +1,9 @@
 # Running the example app
 
-1. In the root directory of `webdev`, run
-   `pub global activate --source path webdev`
-1. Uncomment the dwds dependency override in `/webdev/webdev/pubspec.yaml`, then
-   run `dart run build_runner build` from `/webdev/webdev` directory
-   - *Note: You will have to comment and build, and then uncomment and build,
-     each time you need to pick up new changes*
-1. From `/webdev/example`, run `webdev serve --debug --verbose` (Note: all
-   options can be found by running `webdev help serve`)
+1. Run `dart run build_runner build` from `webdev/` directory to update build
+   artifacts.
+1. From `example/`, run `dart run webdev serve --debug --verbose` (Note: all
+   options can be found by running `dart run webdev help serve`)
 1. Type opt/alt-d in the browser. This is required to start the VM.
 1. \[OPTIONAL\] If you need to connect a locally running DevTools (instructions
    for running
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index ceaaeb2bf..d13599eab 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -9,3 +9,9 @@ environment:
 dev_dependencies:
   build_runner: ^2.4.0
   build_web_compilers: ^4.0.4
+  webdev:
+    path: ../webdev
+
+dependency_overrides:
+  dwds:
+    path: ../dwds
\ No newline at end of file
diff --git a/webdev/CHANGELOG.md b/webdev/CHANGELOG.md
index 113a96acf..e87b6fd65 100644
--- a/webdev/CHANGELOG.md
+++ b/webdev/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## 3.7.0-wip
 
+- Suggest using as a dev-dependency instead of as a globally activated package.
+  The global flow will still work - but there will be no compatibility
+  checking of dependencies.
+
 ## 3.6.0
 
 - Update `dwds` constraint to `24.1.0`.
diff --git a/webdev/README.md b/webdev/README.md
index 2117507f9..21c25c4cf 100644
--- a/webdev/README.md
+++ b/webdev/README.md
@@ -4,32 +4,18 @@ A command-line tool for developing and deploying web applications with Dart.
 
 The latest release of `webdev` requires Dart SDK `2.18.0` or later.
 
-To use `webdev` with a package, make sure you have entries in `pubspec.yaml`
-similar to:
-
-```yaml
-...
-dev_dependencies:
-  build_runner: ^2.4.0
-  build_web_compilers: ^4.0.4
-...
-```
-
-## Installation
-
-`webdev` is not meant to be used as a dependency. Instead, it should be
-["activated"][activating].
+To use `webdev` with a package, add it to the `dev_dependencies` in `pubspec.yaml`:
 
 ```console
-$ dart pub global activate webdev
+$ dart pub add dev:webdev
 ```
 
-Learn more about activating and using packages [here][pub global].
-
 ## Usage
 
 `webdev` provides two commands: `serve` and `build`.
 
+Run them with `dart run webdev serve` and `dart run webdev build`.
+
 ### `webdev serve`
 
 ```
@@ -148,8 +134,4 @@ Usage: webdev build [arguments]
 -v, --verbose                              Enables verbose logging.
 
 Run "webdev help" to see global options.
-
 ```
-
-[activating]: https://dart.dev/tools/pub/cmd/pub-global#activating-a-package
-[pub global]: https://dart.dev/tools/pub/cmd/pub-global
diff --git a/webdev/bin/webdev.dart b/webdev/bin/webdev.dart
index 42d8d6c64..35152d20f 100644
--- a/webdev/bin/webdev.dart
+++ b/webdev/bin/webdev.dart
@@ -26,19 +26,6 @@ Future main(List<String> args) async {
     if (e.path != null) {
       print('  ${e.path}');
     }
-    exitCode = ExitCode.config.code;
-  } on PackageException catch (e) {
-    var withUnsupportedArg =
-        e.unsupportedArgument != null ? ' with --${e.unsupportedArgument}' : '';
-    print(red
-        .wrap('$_boldApp could not run$withUnsupportedArg for this project.'));
-    for (var detail in e.details) {
-      print(yellow.wrap(detail.error));
-      if (detail.description != null) {
-        print(detail.description);
-      }
-    }
-
     exitCode = ExitCode.config.code;
   } on IsolateSpawnException catch (e) {
     print(red.wrap('$_boldApp failed with an unexpected exception.'));
diff --git a/webdev/lib/src/command/build_command.dart b/webdev/lib/src/command/build_command.dart
index b4db27d1c..8e6a016a3 100644
--- a/webdev/lib/src/command/build_command.dart
+++ b/webdev/lib/src/command/build_command.dart
@@ -16,7 +16,6 @@ import 'package:logging/logging.dart' as logging;
 
 import '../daemon_client.dart';
 import '../logging.dart';
-import '../pubspec.dart';
 import 'configuration.dart';
 import 'shared.dart';
 
@@ -50,15 +49,7 @@ class BuildCommand extends Command<int> {
     var configuration = Configuration.fromArgs(argResults);
     configureLogWriter(configuration.verbose);
 
-    List<String> arguments;
-    try {
-      await validatePubspecLock(configuration);
-      arguments = buildRunnerArgs(configuration)..addAll(validExtraArgs);
-    } on PackageException catch (e) {
-      logWriter(logging.Level.SEVERE, 'Pubspec errors: ',
-          error: '${e.details}');
-      rethrow;
-    }
+    final arguments = buildRunnerArgs(configuration)..addAll(validExtraArgs);
 
     try {
       logWriter(logging.Level.INFO, 'Connecting to the build daemon...');
diff --git a/webdev/lib/src/command/daemon_command.dart b/webdev/lib/src/command/daemon_command.dart
index c5b4c34c8..bc086cc3b 100644
--- a/webdev/lib/src/command/daemon_command.dart
+++ b/webdev/lib/src/command/daemon_command.dart
@@ -15,7 +15,6 @@ import '../daemon/app_domain.dart';
 import '../daemon/daemon.dart';
 import '../daemon/daemon_domain.dart';
 import '../logging.dart';
-import '../pubspec.dart';
 import '../serve/dev_workflow.dart';
 import '../serve/utils.dart';
 import 'configuration.dart';
@@ -65,13 +64,6 @@ class DaemonCommand extends Command<int> {
         defaultConfiguration: Configuration(
             launchInChrome: true, debug: true, autoRun: false, release: false));
     configureLogWriter(configuration.verbose);
-    // Validate the pubspec first to ensure we are in a Dart project.
-    try {
-      await validatePubspecLock(configuration);
-    } on PackageException catch (e) {
-      logWriter(Level.SEVERE, 'Pubspec errors: ', error: '${e.details}');
-      rethrow;
-    }
 
     Daemon? daemon;
     DevWorkflow? workflow;
diff --git a/webdev/lib/src/command/serve_command.dart b/webdev/lib/src/command/serve_command.dart
index 127b26e0e..acdde372c 100644
--- a/webdev/lib/src/command/serve_command.dart
+++ b/webdev/lib/src/command/serve_command.dart
@@ -6,10 +6,8 @@ import 'dart:async';
 
 import 'package:args/args.dart';
 import 'package:args/command_runner.dart';
-import 'package:logging/logging.dart';
 
 import '../logging.dart';
-import '../pubspec.dart';
 import '../serve/dev_workflow.dart';
 import 'configuration.dart';
 import 'shared.dart';
@@ -95,12 +93,6 @@ refresh: Performs a full page refresh.
     Configuration configuration;
     configuration = Configuration.fromArgs(argResults);
     configureLogWriter(configuration.verbose);
-    try {
-      await validatePubspecLock(configuration);
-    } on PackageException catch (e) {
-      logWriter(Level.SEVERE, 'Pubspec errors: ', error: '${e.details}');
-      rethrow;
-    }
     // Forward remaining arguments as Build Options to the Daemon.
     // This isn't documented. Should it be advertised?
     var buildOptions = buildRunnerArgs(configuration)
diff --git a/webdev/lib/src/command/shared.dart b/webdev/lib/src/command/shared.dart
index da952ff94..cbc45adb9 100644
--- a/webdev/lib/src/command/shared.dart
+++ b/webdev/lib/src/command/shared.dart
@@ -2,13 +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 'dart:async';
 import 'dart:io';
 
 import 'package:args/args.dart';
 import 'package:path/path.dart' as p;
 
-import '../pubspec.dart';
 import 'configuration.dart';
 
 final lineLength = stdout.hasTerminal ? stdout.terminalColumns : 80;
@@ -102,12 +100,6 @@ List<String> buildRunnerArgs(Configuration configuration) {
   return arguments;
 }
 
-Future<void> validatePubspecLock(Configuration configuration) async {
-  var pubspecLock = await PubspecLock.read();
-  await checkPubspecLock(pubspecLock,
-      requireBuildWebCompilers: configuration.requireBuildWebCompilers);
-}
-
 /// Checks that the normalized form of [path] is a top level directory under
 /// such as `web` or `test`.
 ///
diff --git a/webdev/lib/src/pubspec.dart b/webdev/lib/src/pubspec.dart
deleted file mode 100644
index c2e8eda82..000000000
--- a/webdev/lib/src/pubspec.dart
+++ /dev/null
@@ -1,220 +0,0 @@
-// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'dart:async';
-import 'dart:convert';
-import 'dart:io';
-
-import 'package:http/http.dart';
-import 'package:path/path.dart' as p;
-import 'package:pub_semver/pub_semver.dart';
-import 'package:pubspec_parse/pubspec_parse.dart';
-import 'package:yaml/yaml.dart';
-
-import 'util.dart';
-import 'version.dart';
-
-class PackageException implements Exception {
-  final List<PackageExceptionDetails> details;
-
-  final String? unsupportedArgument;
-
-  PackageException(this.details, {this.unsupportedArgument});
-}
-
-class PackageExceptionDetails {
-  final String error;
-  final String? description;
-  final bool _missingDependency;
-
-  const PackageExceptionDetails._(this.error,
-      {this.description, bool missingDependency = false})
-      : _missingDependency = missingDependency;
-
-  static PackageExceptionDetails missingDep(
-          String pkgName, VersionConstraint constraint) =>
-      PackageExceptionDetails._(
-          'You must have a dependency on `$pkgName` in `pubspec.yaml`.',
-          description: '''
-# pubspec.yaml
-dev_dependencies:
-  $pkgName: $constraint''',
-          missingDependency: true);
-
-  @override
-  String toString() => [error, description].join('\n');
-}
-
-Future _runPubDeps() async {
-  var result = Process.runSync(dartPath, ['pub', 'deps']);
-
-  if (result.exitCode == 65 || result.exitCode == 66) {
-    throw PackageException(
-        [PackageExceptionDetails._((result.stderr as String).trim())]);
-  }
-
-  if (result.exitCode != 0) {
-    throw ProcessException(
-        dartPath,
-        ['pub', 'deps'],
-        '***OUT***\n${result.stdout}\n***ERR***\n${result.stderr}\n***',
-        exitCode);
-  }
-}
-
-class PubspecLock {
-  final YamlMap? _packages;
-
-  PubspecLock(this._packages);
-
-  static Future<PubspecLock> read() async {
-    await _runPubDeps();
-    var dir = p.absolute(p.current);
-    while (true) {
-      final candidate = p.join(
-        dir,
-        '.dart_tool',
-        'package_config.json',
-      );
-      if (File(candidate).existsSync()) break;
-      final next = p.dirname(dir);
-      if (next == dir) {
-        // Give up.
-        dir = p.current;
-        break;
-      }
-      dir = next;
-    }
-
-    var pubspecLock = loadYaml(
-            await File(p.relative(p.join(dir, 'pubspec.lock'))).readAsString())
-        as YamlMap;
-
-    var packages = pubspecLock['packages'] as YamlMap?;
-    return PubspecLock(packages);
-  }
-
-  List<PackageExceptionDetails> checkPackage(
-      String pkgName, VersionConstraint constraint,
-      {String? forArgument}) {
-    var issues = <PackageExceptionDetails>[];
-    var missingDetails =
-        PackageExceptionDetails.missingDep(pkgName, constraint);
-
-    var pkgDataMap =
-        (_packages == null) ? null : _packages[pkgName] as YamlMap?;
-    if (pkgDataMap == null) {
-      issues.add(missingDetails);
-    } else {
-      var source = pkgDataMap['source'] as String?;
-      if (source == 'hosted') {
-        // NOTE: pkgDataMap['description'] should be:
-        //           `{url: https://pub.dev, name: [pkgName]}`
-        //       If a user is playing around here, they are on their own.
-
-        var version = pkgDataMap['version'] as String;
-        var pkgVersion = Version.parse(version);
-        if (!constraint.allows(pkgVersion)) {
-          var error = 'The `$pkgName` version – $pkgVersion – is not '
-              'within the allowed constraint – $constraint.';
-          issues.add(PackageExceptionDetails._(error));
-        }
-      } else {
-        // NOTE: Intentionally not checking non-hosted dependencies: git, path
-        //       If a user is playing around here, they are on their own.
-      }
-    }
-    return issues;
-  }
-}
-
-Future<List<PackageExceptionDetails>> _validateBuildDaemonVersion(
-    PubspecLock pubspecLock) async {
-  var buildDaemonConstraint = '^4.0.0';
-
-  var issues = <PackageExceptionDetails>[];
-
-  var buildDaemonIssues = pubspecLock.checkPackage(
-    'build_daemon',
-    VersionConstraint.parse(buildDaemonConstraint),
-  );
-
-  // Only warn of build_daemon issues if they have a dependency on the package.
-  if (buildDaemonIssues.any((issue) => !issue._missingDependency)) {
-    var info = await _latestPackageInfo();
-    var issuePreamble =
-        'This version of webdev does not support the `build_daemon` '
-        'protocol used by your version of `build_runner`.';
-    // Check if the newer version supports the `build_daemon` transitive version
-    // used by their application.
-    if (info.isNewer &&
-        pubspecLock
-            .checkPackage('build_daemon', info.buildDaemonConstraint)
-            .isEmpty) {
-      issues.add(PackageExceptionDetails._('$issuePreamble\n'
-          'A newer version of webdev is available which supports '
-          'your version of the `build_daemon`. Please update.'));
-    } else {
-      issues.add(PackageExceptionDetails._('$issuePreamble\n'
-          'Please add a dev dependency on `build_daemon` with constraint: '
-          '$buildDaemonConstraint'));
-    }
-  }
-  return issues;
-}
-
-final buildRunnerConstraint = VersionConstraint.parse('^2.4.0');
-final buildWebCompilersConstraint = VersionConstraint.parse('^4.0.4');
-
-// Note the minimum versions should never be dev versions as users will not
-// get them by default.
-Future<void> checkPubspecLock(PubspecLock pubspecLock,
-    {required bool requireBuildWebCompilers}) async {
-  var issues = <PackageExceptionDetails>[];
-  var buildRunnerIssues =
-      pubspecLock.checkPackage('build_runner', buildRunnerConstraint);
-
-  issues.addAll(buildRunnerIssues);
-
-  if (requireBuildWebCompilers) {
-    issues.addAll(pubspecLock.checkPackage(
-        'build_web_compilers', buildWebCompilersConstraint));
-  }
-
-  if (buildRunnerIssues.isEmpty) {
-    issues.addAll(await _validateBuildDaemonVersion(pubspecLock));
-  }
-
-  if (issues.isNotEmpty) {
-    throw PackageException(issues);
-  }
-}
-
-class _PackageInfo {
-  final Version? version;
-  final VersionConstraint buildDaemonConstraint;
-  final bool isNewer;
-  _PackageInfo(this.version, this.buildDaemonConstraint, this.isNewer);
-}
-
-/// Returns the package info for the latest webdev release.
-Future<_PackageInfo> _latestPackageInfo() async {
-  var response = await get(Uri.parse('https://pub.dev/api/packages/webdev'),
-      headers: {HttpHeaders.userAgentHeader: 'webdev $packageVersion'});
-  var responseObj = json.decode(response.body);
-  var pubspec = Pubspec.fromJson(
-      responseObj['latest']['pubspec'] as Map<String, dynamic>);
-  var buildDaemonDependency = pubspec.dependencies['build_daemon'];
-  // This should never be satisfied.
-  var buildDaemonConstraint = VersionConstraint.parse('0.0.0');
-  if (buildDaemonDependency is HostedDependency) {
-    buildDaemonConstraint = buildDaemonDependency.version;
-  }
-  var currentVersion = Version.parse(packageVersion);
-  var pubspecVersion = pubspec.version;
-  var isNewer = (pubspecVersion == null)
-      ? true
-      : currentVersion.compareTo(pubspecVersion) < 0;
-  return _PackageInfo(pubspec.version, buildDaemonConstraint, isNewer);
-}
diff --git a/webdev/lib/src/webdev_command_runner.dart b/webdev/lib/src/webdev_command_runner.dart
index 2f5af323f..3729811a0 100644
--- a/webdev/lib/src/webdev_command_runner.dart
+++ b/webdev/lib/src/webdev_command_runner.dart
@@ -13,7 +13,6 @@ import 'command/serve_command.dart';
 import 'util.dart';
 import 'version.dart';
 
-export 'pubspec.dart' show PackageException;
 export 'util.dart' show appName;
 
 Future<int> run(List<String> args) async => (await _CommandRunner().run(args))!;
diff --git a/webdev/test/e2e_test.dart b/webdev/test/e2e_test.dart
index cb431a9ed..b54c85e0c 100644
--- a/webdev/test/e2e_test.dart
+++ b/webdev/test/e2e_test.dart
@@ -17,7 +17,6 @@ import 'package:test_process/test_process.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:vm_service/vm_service_io.dart';
 import 'package:webdev/src/logging.dart';
-import 'package:webdev/src/pubspec.dart';
 import 'package:webdev/src/serve/utils.dart';
 import 'package:webdev/src/util.dart';
 import 'package:yaml/yaml.dart';
@@ -69,10 +68,6 @@ void main() {
         loadYaml(await File('pubspec.yaml').readAsString()) as YamlMap;
     expect(smokeYaml['environment']['sdk'],
         equals(webdevYaml['environment']['sdk']));
-    expect(smokeYaml['dev_dependencies']['build_runner'],
-        equals(buildRunnerConstraint.toString()));
-    expect(smokeYaml['dev_dependencies']['build_web_compilers'],
-        equals(buildWebCompilersConstraint.toString()));
   });
 
   test('build should fail if targeting an existing directory', () async {
diff --git a/webdev/test/installation_test.dart b/webdev/test/installation_test.dart
index d79674521..bbf9250e2 100644
--- a/webdev/test/installation_test.dart
+++ b/webdev/test/installation_test.dart
@@ -25,180 +25,70 @@ enum StreamType {
 const processTimeout = Duration(minutes: 1);
 
 void main() {
-  Process? createProcess;
-  Process? activateProcess;
-  Process? serveProcess;
-  Directory? tempDir0;
-
-  Future<void> expectStdoutAndCleanExit(Process process,
-      {required String expectedStdout}) async {
-    final stdoutCompleter = _captureOutput(
-      process,
-      streamType: StreamType.stdout,
-      stopCaptureFuture: process.exitCode,
-    );
-    final stderrCompleter = _captureOutput(
-      process,
-      streamType: StreamType.stderr,
-      stopCaptureFuture: process.exitCode,
-    );
-    final exitCode = await _waitForExitOrTimeout(process);
-    final stderrLogs = await stderrCompleter.future;
-    final stdoutLogs = await stdoutCompleter.future;
-    expect(
-      exitCode,
-      equals(0),
-      // Include the stderr and stdout logs if the process does not terminate
-      // cleanly:
-      reason: 'stderr: $stderrLogs, stdout: $stdoutLogs',
-    );
-    expect(
-      stderrLogs,
-      isEmpty,
-    );
-    expect(
-      stdoutLogs,
-      contains(expectedStdout),
-    );
-  }
-
-  Future<void> expectStdoutThenExit(Process process,
-      {required String expectedStdout}) async {
-    final expectedStdoutCompleter = _waitForStdoutOrTimeout(
-      process,
-      expectedStdout: expectedStdout,
-    );
-    final stderrCompleter = _captureOutput(
-      process,
-      streamType: StreamType.stderr,
-      stopCaptureFuture: expectedStdoutCompleter.future,
-    );
-    final stdoutLogs = await expectedStdoutCompleter.future;
-    final stderrLogs = await stderrCompleter.future;
-    expect(
-      stdoutLogs, contains(expectedStdout),
-      // Also include the stderr if the stdout is not expected.
-      reason: 'stderr: $stderrLogs',
-    );
-  }
-
-  setUp(() async {
-    tempDir0 = Directory.systemTemp.createTempSync('installation_test');
-
-    await Process.run(
-      'dart',
-      ['pub', 'global', 'deactivate', 'webdev'],
-    );
-  });
-
-  tearDown(() async {
-    // Kill any stale processes:
-    if (createProcess != null) {
-      Process.killPid(createProcess!.pid, ProcessSignal.sigint);
-      createProcess = null;
-    }
-    if (activateProcess != null) {
-      Process.killPid(activateProcess!.pid, ProcessSignal.sigint);
-      activateProcess = null;
-    }
-    if (serveProcess != null) {
-      Process.killPid(serveProcess!.pid, ProcessSignal.sigint);
-      serveProcess = null;
-    }
-  });
-
   test('can activate and serve webdev', () async {
-    final tempDir = tempDir0!;
-    final tempPath = tempDir.path;
-
-    // Verify that we can create a new Dart app:
-    createProcess = await Process.start(
-      'dart',
-      ['create', '--template', 'web', 'temp_app'],
-      workingDirectory: tempPath,
-    );
-    await expectStdoutAndCleanExit(
-      createProcess!,
-      expectedStdout: 'Created project temp_app in temp_app!',
-    );
-    final appPath = p.join(tempPath, 'temp_app');
-    expect(await Directory(appPath).exists(), isTrue);
-
-    // Verify that `dart pub global activate` works:
-    activateProcess = await Process.start(
-      'dart',
-      ['pub', 'global', 'activate', 'webdev'],
-    );
-    await expectStdoutAndCleanExit(
-      activateProcess!,
-      expectedStdout: 'Activated webdev',
-    );
-
-    // Verify that `webdev serve` works for our new app:
-    serveProcess = await Process.start(
-        'dart', ['pub', 'global', 'run', 'webdev', 'serve'],
-        workingDirectory: appPath);
-    await expectStdoutThenExit(serveProcess!,
-        expectedStdout: 'Serving `web` on');
-  });
-}
-
-Future<int> _waitForExitOrTimeout(Process process) {
-  Timer(processTimeout, () {
-    process.kill(ProcessSignal.sigint);
-  });
-  return process.exitCode;
-}
-
-/// Returns the stdout for the [process] once the [expectedStdout] is found.
-///
-/// Otherwise returns all the stdout up to the [processTimeout].
-Completer<String> _waitForStdoutOrTimeout(Process process,
-    {required String expectedStdout}) {
-  var output = '';
-  final completer = Completer<String>();
-
-  Timer(processTimeout, () {
-    process.kill(ProcessSignal.sigint);
-    if (!completer.isCompleted) {
-      completer.complete(output);
-    }
-  });
-  process.stdout.transform(utf8.decoder).listen((line) {
-    output += line;
-    if (line.contains(expectedStdout)) {
-      process.kill(ProcessSignal.sigint);
-      if (!completer.isCompleted) {
-        completer.complete(output);
+    await withTempDir((tempDir) async {
+      final tempPath = tempDir.path;
+
+      await runCommand(
+        Platform.resolvedExecutable,
+        ['create', '--template', 'web', 'temp_app'],
+        workingDirectory: tempPath,
+        expectedStdout: 'Created project temp_app in temp_app!',
+      );
+
+      final appPath = p.join(tempPath, 'temp_app');
+      expect(await Directory(appPath).exists(), isTrue);
+
+      // Verify that `dart pub add dev:webdev` works:
+      await runCommand(
+        Platform.resolvedExecutable,
+        ['pub', 'add', 'dev:webdev'],
+        expectedStdout: '+ webdev',
+        workingDirectory: appPath,
+      );
+
+      // Verify that `webdev serve` works for our new app:
+      final serveProcess = await Process.start(
+        Platform.resolvedExecutable,
+        ['run', 'webdev', 'serve'],
+        workingDirectory: appPath,
+      );
+      try {
+        serveProcess.stderr.listen((x) => print(utf8.decode(x)));
+        await expectLater(serveProcess.stdout.transform(utf8.decoder),
+            emitsThrough(contains('Serving `web` on')));
+      } finally {
+        serveProcess.kill();
       }
-    }
+    });
   });
+}
 
-  return completer;
+Future<void> runCommand(
+  String executable,
+  List<String> arguments, {
+  required String expectedStdout,
+  int expectedExit = 0,
+  String? workingDirectory,
+}) async {
+  final result = Process.runSync(
+    executable,
+    arguments,
+    workingDirectory: workingDirectory,
+  );
+  printOnFailure('Running `$executable ${arguments.join(' ')}`');
+  printOnFailure(result.stderr);
+  printOnFailure(result.stdout);
+  expect(result.stdout, contains(expectedStdout));
+  expect(result.stderr, isEmpty);
+  expect(result.exitCode, expectedExit);
 }
 
-Completer<String> _captureOutput(
-  Process process, {
-  required StreamType streamType,
-  required Future stopCaptureFuture,
-}) {
-  final stream =
-      streamType == StreamType.stdout ? process.stdout : process.stderr;
-  final completer = Completer<String>();
-  var output = '';
-  stream.transform(utf8.decoder).listen((line) {
-    output += line;
-    if (line.contains('[SEVERE]')) {
-      process.kill(ProcessSignal.sigint);
-      if (!completer.isCompleted) {
-        completer.complete(output);
-      }
-    }
-  });
-  unawaited(stopCaptureFuture.then((_) {
-    if (!completer.isCompleted) {
-      completer.complete(output);
-    }
-  }));
-  return completer;
+Future<T> withTempDir<T>(Future<T> Function(Directory tempDir) callback) async {
+  final tempDir = Directory.systemTemp.createTempSync('installation_test');
+  try {
+    return await callback(tempDir);
+  } finally {
+    tempDir.deleteSync(recursive: true);
+  }
 }

From 992f58d3cff7f7f804166fae7b3f4b74ff694758 Mon Sep 17 00:00:00 2001
From: Sigurd Meldgaard <sigurdm@google.com>
Date: Tue, 19 Nov 2024 14:33:15 +0000
Subject: [PATCH 2/3] remove obsolete tests

---
 webdev/test/integration_test.dart | 257 ------------------------------
 1 file changed, 257 deletions(-)

diff --git a/webdev/test/integration_test.dart b/webdev/test/integration_test.dart
index 93e90e223..cc8fd72e3 100644
--- a/webdev/test/integration_test.dart
+++ b/webdev/test/integration_test.dart
@@ -2,10 +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 'dart:async';
-
 import 'package:test/test.dart';
-import 'package:test_descriptor/test_descriptor.dart' as d;
 
 import 'test_utils.dart';
 
@@ -31,258 +28,4 @@ void main() {
 
     await process.shouldExit(64);
   });
-
-  test('Errors with `build_runner` should not surface `build_daemon` issues',
-      () async {
-    await d.file('pubspec.yaml', _pubspecYaml).create();
-
-    await d
-        .file(
-            'pubspec.lock',
-            _pubspecLock(
-              runnerVersion: '1.2.8',
-              daemonVersion: '0.4.0',
-            ))
-        .create();
-
-    await d.dir('.dart_tool', [d.file('package_config.json', '')]).create();
-    await d.file('.dart_tool/package_config.json', '').create();
-
-    final process =
-        await testRunner.runWebDev(['serve'], workingDirectory: d.sandbox);
-
-    final output = await process.stdout.rest.toList();
-
-    expect(output, isNot(contains(contains('`build_daemon`'))));
-
-    await process.shouldExit(78);
-  });
-
-  final invalidRanges = <String, List<String>>{
-    'build_runner': ['0.8.9', '3.0.0'],
-    'build_web_compilers': ['0.3.5', '5.0.0'],
-    'build_daemon': ['0.3.0', '5.0.0'],
-  };
-
-  for (final command in ['build', 'serve', 'daemon']) {
-    group('`$command` command', () {
-      group('missing dependency on', () {
-        test('`build_runner` should fail', () async {
-          await d.file('pubspec.yaml', _pubspecYaml).create();
-
-          await d
-              .file('pubspec.lock', _pubspecLock(runnerVersion: null))
-              .create();
-
-          await d
-              .dir('.dart_tool', [d.file('package_config.json', '')]).create();
-          await d.file('.dart_tool/package_config.json', '').create();
-
-          final process = await testRunner
-              .runWebDev([command], workingDirectory: d.sandbox);
-
-          await checkProcessStdout(process, ['webdev could not run']);
-          await process.shouldExit(78);
-        });
-
-        test('`build_web_compilers` should fail', () async {
-          await d.file('pubspec.yaml', _pubspecYaml).create();
-
-          await d
-              .file('pubspec.lock', _pubspecLock(webCompilersVersion: null))
-              .create();
-
-          await d
-              .dir('.dart_tool', [d.file('package_config.json', '')]).create();
-          await d.file('.dart_tool/package_config.json', '').create();
-
-          final process = await testRunner
-              .runWebDev(['serve'], workingDirectory: d.sandbox);
-
-          await checkProcessStdout(process, ['webdev could not run']);
-          await process.shouldExit(78);
-        });
-
-        test(
-            '`build_web_compilers` should be ignored with '
-            '--no-build-web-compilers', () async {
-          await d.file('pubspec.yaml', _pubspecYaml).create();
-
-          await d
-              .file('pubspec.lock', _pubspecLock(webCompilersVersion: null))
-              .create();
-
-          await d
-              .dir('.dart_tool', [d.file('package_config.json', '')]).create();
-          await d.file('.dart_tool/package_config.json', '').create();
-
-          // Required for webdev to not complain about nothing to serve.
-          await d.dir('web').create();
-
-          final process = await testRunner.runWebDev(
-              ['serve', '--no-build-web-compilers'],
-              workingDirectory: d.sandbox);
-
-          await process.shouldExit();
-        });
-      });
-
-      for (final entry in invalidRanges.entries) {
-        group('with package `${entry.key}`', () {
-          for (final version in entry.value) {
-            test('at invalid version `$version` should fail', () async {
-              var buildRunnerVersion = _supportedBuildRunnerVersion;
-              var webCompilersVersion = _supportedWebCompilersVersion;
-              var buildDaemonVersion = _supportedBuildDaemonVersion;
-
-              switch (entry.key) {
-                case 'build_runner':
-                  buildRunnerVersion = version;
-                  break;
-                case 'build_web_compilers':
-                  webCompilersVersion = version;
-                  break;
-                case 'build_daemon':
-                  buildDaemonVersion = version;
-              }
-
-              await d.file('pubspec.yaml', _pubspecYaml).create();
-
-              await d
-                  .file(
-                      'pubspec.lock',
-                      _pubspecLock(
-                          runnerVersion: buildRunnerVersion,
-                          webCompilersVersion: webCompilersVersion,
-                          daemonVersion: buildDaemonVersion))
-                  .create();
-
-              await d.dir(
-                  '.dart_tool', [d.file('package_config.json', '')]).create();
-              await d.file('.dart_tool/package_config.json', '').create();
-
-              final process = await testRunner
-                  .runWebDev(['serve'], workingDirectory: d.sandbox);
-
-              await checkProcessStdout(process, ['webdev could not run']);
-
-              await process.shouldExit(78);
-            });
-          }
-        });
-      }
-
-      test('no pubspec.yaml', () async {
-        final process =
-            await testRunner.runWebDev(['serve'], workingDirectory: d.sandbox);
-
-        await checkProcessStdout(process, ['webdev could not run']);
-        await process.shouldExit(78);
-      });
-
-      test(
-        'pubspec.yaml, no pubspec.lock',
-        () async {
-          await d.file('pubspec.yaml', _pubspecYaml).create();
-
-          final process = await testRunner
-              .runWebDev(['serve'], workingDirectory: d.sandbox);
-
-          await checkProcessStdout(process, ['webdev could not run']);
-          await process.shouldExit(78);
-        },
-      );
-
-      test('should fail if there has been a dependency change', () async {
-        await d.file('pubspec.lock', _pubspecLock()).create();
-
-        await d.dir('.dart_tool', [d.file('package_config.json', '')]).create();
-        await d.file('.dart_tool/package_config.json', '').create();
-
-        // Ensure there is a noticeable delta in the creation times
-        await Future.delayed(const Duration(milliseconds: 1100));
-
-        await d.file('pubspec.yaml', '''
-name: sample
-dependencies:
-  args: ^1.0.0
-''').create();
-
-        final process =
-            await testRunner.runWebDev(['serve'], workingDirectory: d.sandbox);
-
-        await checkProcessStdout(process, ['webdev could not run']);
-        await process.shouldExit(78);
-      });
-    });
-  }
-}
-
-const _supportedBuildRunnerVersion = '2.4.0';
-const _supportedWebCompilersVersion = '4.0.4';
-const _supportedBuildDaemonVersion = '4.0.0';
-
-String _pubspecYaml = '''
-  name: sample
-''';
-
-String _pubspecLock(
-    {String? runnerVersion = _supportedBuildRunnerVersion,
-    String? webCompilersVersion = _supportedWebCompilersVersion,
-    String? daemonVersion = _supportedBuildDaemonVersion,
-    List<String> extraPkgs = const []}) {
-  final buffer = StringBuffer('''
-# Copy-pasted from a valid run
-packages:
-''');
-
-  if (runnerVersion != null) {
-    buffer.writeln('''
-  build_runner:
-    dependency: "direct dev"
-    description:
-      name: build_runner
-      url: "https://pub.dev"
-    source: hosted
-    version: "$runnerVersion"
-''');
-  }
-
-  if (webCompilersVersion != null) {
-    buffer.writeln('''
-  build_web_compilers:
-    dependency: "direct dev"
-    description:
-      name: build_web_compilers
-      url: "https://pub.dev"
-    source: hosted
-    version: "$webCompilersVersion"
-''');
-  }
-
-  if (daemonVersion != null) {
-    buffer.writeln('''
-  build_daemon:
-    dependency: "direct transitive"
-    description:
-      name: build_daemon
-      url: "https://pub.dev"
-    source: hosted
-    version: "$daemonVersion"
-''');
-  }
-
-  for (final pkg in extraPkgs) {
-    buffer.writeln('''
-  $pkg:
-    dependency: "direct"
-    description:
-      name: $pkg
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.0"
-''');
-  }
-
-  return buffer.toString();
 }

From 3c05c93bb3db80a720b9cd05418130f13938d707 Mon Sep 17 00:00:00 2001
From: Sigurd Meldgaard <sigurdm@google.com>
Date: Tue, 19 Nov 2024 14:50:46 +0000
Subject: [PATCH 3/3] wait for process to exit in test

---
 webdev/test/installation_test.dart | 1 +
 1 file changed, 1 insertion(+)

diff --git a/webdev/test/installation_test.dart b/webdev/test/installation_test.dart
index bbf9250e2..9d72bfb7d 100644
--- a/webdev/test/installation_test.dart
+++ b/webdev/test/installation_test.dart
@@ -60,6 +60,7 @@ void main() {
       } finally {
         serveProcess.kill();
       }
+      await serveProcess.exitCode;
     });
   });
 }