| // Copyright 2024 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. | |
| 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.extensionAssetsPathKey: '', | |
| DevToolsExtensionConfig.devtoolsOptionsUriKey: '', | |
| DevToolsExtensionConfig.isPubliclyHostedKey: 'false', | |
| DevToolsExtensionConfig.detectedFromStaticContextKey: 'false', | |
| }); | |
| // 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'); | |
| } |