Add a `devtools_extensions validate` command (#7257)
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 95e563c..62898f0 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -74,6 +74,7 @@ subdirectories. - [#7174](https://github.com/flutter/devtools/pull/7174) * Added an example of creating an extension for a pure Dart package. - [#7196](https://github.com/flutter/devtools/pull/7196) * Updated the `example/README.md` with more complete documentation. - [#7237](https://github.com/flutter/devtools/pull/7237) +* Added a `devtools_extensions validate` command to validate extension requirements during development. - [#7257](https://github.com/flutter/devtools/pull/7257) ## Full commit history
diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index 95bd6a2..10161c0 100644 --- a/packages/devtools_app_shared/CHANGELOG.md +++ b/packages/devtools_app_shared/CHANGELOG.md
@@ -1,4 +1,4 @@ -## 0.0.10-wip +## 0.0.10 * Add `DTDManager` class and export from `service.dart`. * Add `showDevToolsDialog` helper method. * Add `FlexSplitColumn` and `BlankHeader` common widgets.
diff --git a/packages/devtools_app_shared/pubspec.yaml b/packages/devtools_app_shared/pubspec.yaml index 1b133ca..75bf29a 100644 --- a/packages/devtools_app_shared/pubspec.yaml +++ b/packages/devtools_app_shared/pubspec.yaml
@@ -1,6 +1,6 @@ name: devtools_app_shared description: Package of Dart & Flutter structures shared between devtools_app and devtools extensions. -version: 0.0.10-wip +version: 0.0.10 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_app_shared environment:
diff --git a/packages/devtools_extensions/CHANGELOG.md b/packages/devtools_extensions/CHANGELOG.md index 7cab68a..e08adb9 100644 --- a/packages/devtools_extensions/CHANGELOG.md +++ b/packages/devtools_extensions/CHANGELOG.md
@@ -1,4 +1,4 @@ -## 0.0.14-wip +## 0.0.14 * Add a global `dtdManager` for interacting with the Dart Tooling Daemon. * Add support for connecting to the Dart Tooling Daemon from the simulated DevTools environment. @@ -8,6 +8,7 @@ * Refactor `example` directory to support more package examples. * Add an example of providing an extension from a pure Dart package. * Update the `example/README.md`. +* Add a `devtools_extensions validate` for validating extension requirements. ## 0.0.13 * Bump `package:web` to `^0.4.1`.
diff --git a/packages/devtools_extensions/README.md b/packages/devtools_extensions/README.md index 578f2c4..4c11b04 100644 --- a/packages/devtools_extensions/README.md +++ b/packages/devtools_extensions/README.md
@@ -198,6 +198,16 @@ dart run devtools_extensions build_and_copy --source=. --dest=../foo/extension/devtools ``` +To ensure that your extension is setup properly for loading in DevTools, run the +`validate` command from `package:devtools_extensions`. The `--package` argument +should point to the root of the Dart package that this extension will be published +with. +```sh +cd your_extension_web_app; +flutter pub get; +dart run devtools_extensions validate --package=../foo +``` + 2. Prepare and run a test application that depends on your pub package. You'll need to change the `pubspec.yaml` dependency to be a [path](https://dart.dev/tools/pub/dependencies#path-packages) dependency that points to your local pub package source code. Once you have done this,
diff --git a/packages/devtools_extensions/analysis_options.yaml b/packages/devtools_extensions/analysis_options.yaml index 35d2ba4..83f90ee 100644 --- a/packages/devtools_extensions/analysis_options.yaml +++ b/packages/devtools_extensions/analysis_options.yaml
@@ -2,5 +2,4 @@ analyzer: exclude: - - bin/** - example/**
diff --git a/packages/devtools_extensions/bin/_build_and_copy.dart b/packages/devtools_extensions/bin/_build_and_copy.dart index 40f70c0..8f90941 100644 --- a/packages/devtools_extensions/bin/_build_and_copy.dart +++ b/packages/devtools_extensions/bin/_build_and_copy.dart
@@ -102,7 +102,7 @@ if (destinationDirectory.existsSync()) { destinationDirectory.deleteSync(recursive: true); } - Directory(destinationBuildPath)..createSync(recursive: true); + Directory(destinationBuildPath).createSync(recursive: true); await copyPath( sourceBuildPath, @@ -116,7 +116,7 @@ ); } - void _log(String message) => print('[$name] $message'); + void _log(String message) => stdout.writeln('[$name] $message'); Future<void> _runProcess( ProcessManager processManager,
diff --git a/packages/devtools_extensions/bin/_validate.dart b/packages/devtools_extensions/bin/_validate.dart new file mode 100644 index 0000000..e0c5f80 --- /dev/null +++ b/packages/devtools_extensions/bin/_validate.dart
@@ -0,0 +1,138 @@ +// Copyright 2024 The Chromium Authors. 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:args/command_runner.dart'; +import 'package:devtools_shared/devtools_extensions.dart'; +import 'package:devtools_shared/devtools_extensions_io.dart'; +import 'package:path/path.dart' as path; +import 'package:yaml/yaml.dart'; + +/// Command that validates that a DevTools extension meets the requirements for +/// loading successfully in DevTools. +/// +/// Example usage: +/// +/// dart run devtools_extensions validate --package=../your_pub_package +class ValidateExtensionCommand extends Command { + ValidateExtensionCommand() { + argParser.addOption( + _packageKey, + help: 'The location of the package that this extension is published with', + abbr: 'p', + valueHelp: 'path/to/foo/packages/foo', + mandatory: true, + ); + } + + static const _packageKey = 'package'; + + @override + String get name => 'validate'; + + @override + String get description => + 'Command that validates that a DevTools extension meets the ' + 'requirements for loading successfully in DevTools.'; + + static const docUrl = 'https://docs.flutter.dev/tools/devtools/extensions'; + + @override + Future<void> run() async { + final packagePath = argResults?[_packageKey]! as String; + try { + // TODO(kenz): try to use the the existing pub validator for this check. See + // https://github.com/dart-lang/pub/blob/master/lib/src/validator/devtools_extension.dart. + _validateDirectoryContents(packagePath); + + // Try to parse the config.yaml file. This will throw an exception if there + // are parsing errors. + DevToolsExtensionConfig.parse( + { + ..._configAsMap(packagePath), + // These are generated on the DevTools server, so pass in stubbed + // values for the sake of validation. + DevToolsExtensionConfig.isPubliclyHostedKey: 'false', + DevToolsExtensionConfig.pathKey: '', + }, + ); + + // If there are no exceptions at this point, the extension has successfully + // been validated. + stdout.writeln('Extension validation successful'); + } on StateError catch (e) { + _logError(e.message); + } on FileSystemException catch (e) { + _logError(e.message); + } catch (e) { + _logError(e.toString()); + } + } +} + +void _validateDirectoryContents(String packagePath) { + final packageDirectory = Directory(packagePath); + if (!packageDirectory.existsSync()) { + throw FileSystemException('${packageDirectory.path} directory not found'); + } + + final devtoolsExtensionDir = Directory( + path.join(packageDirectory.path, 'extension', 'devtools'), + ); + if (!devtoolsExtensionDir.existsSync()) { + throw const FileSystemException( + ''' +An extension/devtools directory is required, but none was found. +See ${ValidateExtensionCommand.docUrl}. +''', + ); + } + + final buildDir = Directory(path.join(devtoolsExtensionDir.path, 'build')); + if (!buildDir.existsSync()) { + throw const FileSystemException( + ''' +An extension/devtools/build directory is required, but none was found. +See ${ValidateExtensionCommand.docUrl}. +''', + ); + } + if (buildDir.listSync().isEmpty) { + throw const FileSystemException( + ''' +A non-empty extension/devtools/build directory is required, but the directory is empty. +See ${ValidateExtensionCommand.docUrl}. +''', + ); + } + + final configFile = _lookupConfigFile(packagePath); + if (!configFile.existsSync()) { + throw const FileSystemException( + ''' +An extension/devtools/config.yaml file is required, but none was found. +See ${ValidateExtensionCommand.docUrl}. +''', + ); + } +} + +Map<String, Object?> _configAsMap(String packagePath) { + final configFile = _lookupConfigFile(packagePath); + // At this point, we know the config.yaml file exists. + assert(configFile.existsSync()); + final yamlMap = loadYaml(configFile.readAsStringSync()) as YamlMap; + return yamlMap.toDartMap(); +} + +File _lookupConfigFile(String packagePath) { + return File( + path.join(packagePath, 'extension', 'devtools', 'config.yaml'), + ); +} + +void _logError(String error) { + stderr.writeln('Validation error: $error'); +}
diff --git a/packages/devtools_extensions/bin/devtools_extensions.dart b/packages/devtools_extensions/bin/devtools_extensions.dart index 853b7e7..4acf18a 100644 --- a/packages/devtools_extensions/bin/devtools_extensions.dart +++ b/packages/devtools_extensions/bin/devtools_extensions.dart
@@ -6,10 +6,12 @@ import 'package:io/io.dart'; import '_build_and_copy.dart'; +import '_validate.dart'; void main(List<String> arguments) async { final command = BuildExtensionCommand(); final runner = CommandRunner('devtools_extensions', command.description) - ..addCommand(BuildExtensionCommand()); + ..addCommand(BuildExtensionCommand()) + ..addCommand(ValidateExtensionCommand()); await runner.run(arguments).whenComplete(sharedStdIn.terminate); }
diff --git a/packages/devtools_extensions/pubspec.yaml b/packages/devtools_extensions/pubspec.yaml index 3e5f538..09cd7d0 100644 --- a/packages/devtools_extensions/pubspec.yaml +++ b/packages/devtools_extensions/pubspec.yaml
@@ -1,6 +1,6 @@ name: devtools_extensions description: A package for building and supporting extensions for Dart DevTools. -version: 0.0.14-wip +version: 0.0.14 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_extensions @@ -21,7 +21,8 @@ path: ^1.8.0 logging: ^1.1.1 vm_service: ^14.0.0 - web: ^0.4.1 + web: ^0.4.1 + yaml: ^3.1.2 dev_dependencies: flutter_driver:
diff --git a/packages/devtools_shared/CHANGELOG.md b/packages/devtools_shared/CHANGELOG.md index b5f0da5..ec1efbf 100644 --- a/packages/devtools_shared/CHANGELOG.md +++ b/packages/devtools_shared/CHANGELOG.md
@@ -1,3 +1,6 @@ +# 7.0.1-wip +* Refactor yaml extension methods. + # 7.0.0 * **Breaking change:** remove the `ServerApi.setCompleted` method that was a duplicate of `ServerApi.getCompleted`.
diff --git a/packages/devtools_shared/lib/devtools_extensions_io.dart b/packages/devtools_shared/lib/devtools_extensions_io.dart index 37ef521..985717b 100644 --- a/packages/devtools_shared/lib/devtools_extensions_io.dart +++ b/packages/devtools_shared/lib/devtools_extensions_io.dart
@@ -4,3 +4,4 @@ export 'src/extensions/extension_enablement.dart'; export 'src/extensions/extension_manager.dart'; +export 'src/extensions/yaml_utils.dart';
diff --git a/packages/devtools_shared/lib/src/extensions/extension_enablement.dart b/packages/devtools_shared/lib/src/extensions/extension_enablement.dart index 721f781..95d1b56 100644 --- a/packages/devtools_shared/lib/src/extensions/extension_enablement.dart +++ b/packages/devtools_shared/lib/src/extensions/extension_enablement.dart
@@ -10,6 +10,7 @@ import 'package:yaml_edit/yaml_edit.dart'; import 'extension_model.dart'; +import 'yaml_utils.dart'; /// Manages the `devtools_options.yaml` file and allows read / write access. class DevToolsOptions { @@ -144,34 +145,3 @@ } } } - -extension YamlExtension on YamlMap { - Map<String, Object?> toDartMap() { - final map = <String, Object?>{}; - for (final entry in nodes.entries) { - map[entry.key.toString()] = entry.value.convertToDartType(); - } - return map; - } -} - -extension YamlListExtension on YamlList { - List<Object?> toDartList() { - final list = <Object>[]; - for (final e in nodes) { - final element = e.convertToDartType(); - if (element != null) list.add(element); - } - return list; - } -} - -extension YamlNodeExtension on YamlNode { - Object? convertToDartType() { - return switch (this) { - YamlMap() => (this as YamlMap).toDartMap(), - YamlList() => (this as YamlList).toDartList(), - _ => value, - }; - } -}
diff --git a/packages/devtools_shared/lib/src/extensions/yaml_utils.dart b/packages/devtools_shared/lib/src/extensions/yaml_utils.dart new file mode 100644 index 0000000..b000569 --- /dev/null +++ b/packages/devtools_shared/lib/src/extensions/yaml_utils.dart
@@ -0,0 +1,36 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:yaml/yaml.dart'; + +extension YamlExtension on YamlMap { + Map<String, Object?> toDartMap() { + final map = <String, Object?>{}; + for (final entry in nodes.entries) { + map[entry.key.toString()] = entry.value.convertToDartType(); + } + return map; + } +} + +extension YamlListExtension on YamlList { + List<Object?> toDartList() { + final list = <Object>[]; + for (final e in nodes) { + final element = e.convertToDartType(); + if (element != null) list.add(element); + } + return list; + } +} + +extension YamlNodeExtension on YamlNode { + Object? convertToDartType() { + return switch (this) { + YamlMap() => (this as YamlMap).toDartMap(), + YamlList() => (this as YamlList).toDartList(), + _ => value, + }; + } +}
diff --git a/packages/devtools_shared/pubspec.yaml b/packages/devtools_shared/pubspec.yaml index b15883f..1a7de1b 100644 --- a/packages/devtools_shared/pubspec.yaml +++ b/packages/devtools_shared/pubspec.yaml
@@ -1,7 +1,7 @@ name: devtools_shared description: Package of shared Dart structures between devtools_app, dds, and other tools. -version: 7.0.0 +version: 7.0.1-wip repository: https://github.com/flutter/devtools/tree/master/packages/devtools_shared