diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index f1a3750171d..6c76807f860 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -4,12 +4,12 @@ import 'dart:async'; +import 'package:devtools_shared/devtools_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'example/conditional_screen.dart'; -import 'extensions/extension_model.dart'; import 'extensions/extension_screen.dart'; import 'framework/framework_core.dart'; import 'framework/home_screen.dart'; diff --git a/packages/devtools_app/lib/src/extensions/embedded/controller.dart b/packages/devtools_app/lib/src/extensions/embedded/controller.dart index df9d47b57d3..acd8a5e1d60 100644 --- a/packages/devtools_app/lib/src/extensions/embedded/controller.dart +++ b/packages/devtools_app/lib/src/extensions/embedded/controller.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'package:devtools_extensions/api.dart'; +import 'package:devtools_shared/devtools_extensions.dart'; import '../../shared/primitives/auto_dispose.dart'; -import '../extension_model.dart'; import '_controller_desktop.dart' if (dart.library.html) '_controller_web.dart'; EmbeddedExtensionControllerImpl createEmbeddedExtensionController( diff --git a/packages/devtools_app/lib/src/extensions/extension_model.dart b/packages/devtools_app/lib/src/extensions/extension_model.dart deleted file mode 100644 index a852b2cbd1d..00000000000 --- a/packages/devtools_app/lib/src/extensions/extension_model.dart +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023 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 'package:flutter/material.dart'; - -// TODO(kenz): share this with devtools_server so that we do not duplicate. - -/// Describes an extension that can be dynamically loaded into a custom screen -/// in DevTools. -class DevToolsExtensionConfig { - DevToolsExtensionConfig._({ - required this.name, - required this.path, - required this.issueTrackerLink, - required this.version, - required this.materialIconCodePoint, - }); - - factory DevToolsExtensionConfig.parse(Map json) { - // Defaults to the code point for [Icons.extensions_outlined] if null. - final codePoint = json[materialIconCodePointKey] as int? ?? 0xf03f; - return DevToolsExtensionConfig._( - name: json[nameKey]! as String, - path: json[pathKey]! as String, - issueTrackerLink: json[issueTrackerKey]! as String, - version: json[versionKey]! as String, - materialIconCodePoint: codePoint, - ); - } - - static const nameKey = 'name'; - static const pathKey = 'path'; - static const issueTrackerKey = 'issueTracker'; - static const versionKey = 'version'; - static const materialIconCodePointKey = 'materialIconCodePoint'; - - final String name; - final String path; - final String issueTrackerLink; - final String version; - final int materialIconCodePoint; - - Map toJson() => { - nameKey: name, - pathKey: path, - issueTrackerKey: issueTrackerLink, - versionKey: version, - materialIconCodePointKey: materialIconCodePoint, - }; -} - -extension ExtensionConfigExtension on DevToolsExtensionConfig { - IconData get icon => IconData( - materialIconCodePoint, - fontFamily: 'MaterialIcons', - ); -} - -// TODO(kenz): remove these once the DevTools extensions feature has shipped. -final List debugExtensions = [ - DevToolsExtensionConfig.parse({ - DevToolsExtensionConfig.nameKey: 'foo', - DevToolsExtensionConfig.issueTrackerKey: 'www.google.com', - DevToolsExtensionConfig.versionKey: '1.0.0', - DevToolsExtensionConfig.pathKey: '/path/to/foo', - }), - DevToolsExtensionConfig.parse({ - DevToolsExtensionConfig.nameKey: 'bar', - DevToolsExtensionConfig.issueTrackerKey: 'www.google.com', - DevToolsExtensionConfig.versionKey: '2.0.0', - DevToolsExtensionConfig.materialIconCodePointKey: 0xe638, - DevToolsExtensionConfig.pathKey: '/path/to/bar', - }), - DevToolsExtensionConfig.parse({ - DevToolsExtensionConfig.nameKey: 'provider', - DevToolsExtensionConfig.issueTrackerKey: - 'https://github.com/rrousselGit/provider/issues', - DevToolsExtensionConfig.versionKey: '3.0.0', - DevToolsExtensionConfig.materialIconCodePointKey: 0xe50a, - DevToolsExtensionConfig.pathKey: '/path/to/provider', - }), -]; diff --git a/packages/devtools_app/lib/src/extensions/extension_screen.dart b/packages/devtools_app/lib/src/extensions/extension_screen.dart index 1dd17ff84e5..d10040bfbeb 100644 --- a/packages/devtools_app/lib/src/extensions/extension_screen.dart +++ b/packages/devtools_app/lib/src/extensions/extension_screen.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:devtools_shared/devtools_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,6 @@ import '../shared/screen.dart'; import '../shared/theme.dart'; import 'embedded/controller.dart'; import 'embedded/view.dart'; -import 'extension_model.dart'; class ExtensionScreen extends Screen { ExtensionScreen(this.extensionConfig) @@ -155,3 +155,10 @@ class EmbeddedExtensionHeader extends StatelessWidget { ); } } + +extension ExtensionConfigExtension on DevToolsExtensionConfig { + IconData get icon => IconData( + materialIconCodePoint, + fontFamily: 'MaterialIcons', + ); +} diff --git a/packages/devtools_app/lib/src/extensions/extension_service.dart b/packages/devtools_app/lib/src/extensions/extension_service.dart index 26db4825ced..a3472128c4d 100644 --- a/packages/devtools_app/lib/src/extensions/extension_service.dart +++ b/packages/devtools_app/lib/src/extensions/extension_service.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:devtools_shared/devtools_extensions.dart'; import 'package:flutter/foundation.dart'; import '../shared/globals.dart'; import '../shared/primitives/auto_dispose.dart'; -import 'extension_model.dart'; class ExtensionService extends DisposableController with AutoDisposeControllerMixin { @@ -28,3 +28,28 @@ class ExtensionService extends DisposableController debugExtensions.sublist(0, _count++ % (debugExtensions.length + 1)); } } + +// TODO(kenz): remove these once the DevTools extensions feature has shipped. +final List debugExtensions = [ + DevToolsExtensionConfig.parse({ + DevToolsExtensionConfig.nameKey: 'foo', + DevToolsExtensionConfig.issueTrackerKey: 'www.google.com', + DevToolsExtensionConfig.versionKey: '1.0.0', + DevToolsExtensionConfig.pathKey: '/path/to/foo', + }), + DevToolsExtensionConfig.parse({ + DevToolsExtensionConfig.nameKey: 'bar', + DevToolsExtensionConfig.issueTrackerKey: 'www.google.com', + DevToolsExtensionConfig.versionKey: '2.0.0', + DevToolsExtensionConfig.materialIconCodePointKey: 0xe638, + DevToolsExtensionConfig.pathKey: '/path/to/bar', + }), + DevToolsExtensionConfig.parse({ + DevToolsExtensionConfig.nameKey: 'provider', + DevToolsExtensionConfig.issueTrackerKey: + 'https://github.com/rrousselGit/provider/issues', + DevToolsExtensionConfig.versionKey: '3.0.0', + DevToolsExtensionConfig.materialIconCodePointKey: 0xe50a, + DevToolsExtensionConfig.pathKey: '/path/to/provider', + }), +]; diff --git a/packages/devtools_app/lib/src/shared/config_specific/server/_server_stub.dart b/packages/devtools_app/lib/src/shared/config_specific/server/_server_stub.dart index c9c5686952a..a2737661bbc 100644 --- a/packages/devtools_app/lib/src/shared/config_specific/server/_server_stub.dart +++ b/packages/devtools_app/lib/src/shared/config_specific/server/_server_stub.dart @@ -6,6 +6,8 @@ // ignore_for_file: avoid-unused-parameters import 'dart:async'; +import 'package:devtools_shared/devtools_extensions.dart'; + import '../../primitives/utils.dart'; const unsupportedMessage = @@ -75,6 +77,12 @@ Future requestTestAppSizeFile(String path) async { throw Exception(unsupportedMessage); } +Future> refreshAvailableExtensions( + String? rootPath, +) async { + return []; +} + void logWarning() { throw Exception(unsupportedMessage); } diff --git a/packages/devtools_app/lib/src/shared/config_specific/server/_server_web.dart b/packages/devtools_app/lib/src/shared/config_specific/server/_server_web.dart index da81b141cbe..50abddfbc5c 100644 --- a/packages/devtools_app/lib/src/shared/config_specific/server/_server_web.dart +++ b/packages/devtools_app/lib/src/shared/config_specific/server/_server_web.dart @@ -8,6 +8,8 @@ import 'dart:convert'; // ignore: avoid_web_libraries_in_flutter, as designed import 'dart:html'; +import 'package:collection/collection.dart'; +import 'package:devtools_shared/devtools_extensions.dart'; import 'package:devtools_shared/devtools_shared.dart'; import 'package:logging/logging.dart'; @@ -337,6 +339,32 @@ DevToolsJsonFile _devToolsJsonFileFromResponse( ); } +Future> refreshAvailableExtensions( + String? rootPath, +) async { + if (isDevToolsServerAvailable) { + final uri = Uri( + path: apiServeAvailableExtensions, + queryParameters: {extensionRootPathPropertyName: rootPath}, + ); + final resp = await request(uri.toString()); + if (resp?.status == HttpStatus.ok) { + final parsedResult = json.decode(resp!.responseText!); + final extensionsAsJson = + (parsedResult[extensionsResultPropertyName]! as List) + .whereNotNull() + .cast>(); + return extensionsAsJson + .map((p) => DevToolsExtensionConfig.parse(p)) + .toList(); + } else { + logWarning(resp, apiServeAvailableExtensions); + return []; + } + } + return []; +} + void logWarning(HttpRequest? response, String apiType, [String? respText]) { _log.warning( 'HttpRequest $apiType failed status = ${response?.status}' diff --git a/packages/devtools_app/test/extensions/extension_screen_test.dart b/packages/devtools_app/test/extensions/extension_screen_test.dart index cc5fc29dde5..cd4da457a4f 100644 --- a/packages/devtools_app/test/extensions/extension_screen_test.dart +++ b/packages/devtools_app/test/extensions/extension_screen_test.dart @@ -4,8 +4,8 @@ import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/extensions/embedded/view.dart'; -import 'package:devtools_app/src/extensions/extension_model.dart'; import 'package:devtools_app/src/extensions/extension_screen.dart'; +import 'package:devtools_shared/src/extensions/extension_model.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/devtools_shared/lib/devtools_extensions.dart b/packages/devtools_shared/lib/devtools_extensions.dart new file mode 100644 index 00000000000..564e9bd6731 --- /dev/null +++ b/packages/devtools_shared/lib/devtools_extensions.dart @@ -0,0 +1,6 @@ +// Copyright 2023 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. + +export 'src/extensions/extension_manager.dart'; +export 'src/extensions/extension_model.dart'; diff --git a/packages/devtools_shared/lib/src/devtools_api.dart b/packages/devtools_shared/lib/src/devtools_api.dart index a95ecca738a..aeb2ee66719 100644 --- a/packages/devtools_shared/lib/src/devtools_api.dart +++ b/packages/devtools_shared/lib/src/devtools_api.dart @@ -3,64 +3,77 @@ // found in the LICENSE file. /// All server APIs prefix: -const String apiPrefix = 'api/'; +const apiPrefix = 'api/'; /// Flutter GA properties APIs: -const String apiGetFlutterGAEnabled = '${apiPrefix}getFlutterGAEnabled'; -const String apiGetFlutterGAClientId = '${apiPrefix}getFlutterGAClientId'; +const apiGetFlutterGAEnabled = '${apiPrefix}getFlutterGAEnabled'; +const apiGetFlutterGAClientId = '${apiPrefix}getFlutterGAClientId'; /// DevTools GA properties APIs: -const String apiResetDevTools = '${apiPrefix}resetDevTools'; -const String apiGetDevToolsFirstRun = '${apiPrefix}getDevToolsFirstRun'; -const String apiGetDevToolsEnabled = '${apiPrefix}getDevToolsEnabled'; -const String apiSetDevToolsEnabled = '${apiPrefix}setDevToolsEnabled'; +const apiResetDevTools = '${apiPrefix}resetDevTools'; +const apiGetDevToolsFirstRun = '${apiPrefix}getDevToolsFirstRun'; +const apiGetDevToolsEnabled = '${apiPrefix}getDevToolsEnabled'; +const apiSetDevToolsEnabled = '${apiPrefix}setDevToolsEnabled'; /// Property name to apiSetDevToolsEnabled the DevToolsEnabled is the name used /// in queryParameter: -const String devToolsEnabledPropertyName = 'enabled'; +const devToolsEnabledPropertyName = 'enabled'; /// Survey properties APIs: /// setActiveSurvey sets the survey property to fetch and save JSON values e.g., Q1-2020 -const String apiSetActiveSurvey = '${apiPrefix}setActiveSurvey'; +const apiSetActiveSurvey = '${apiPrefix}setActiveSurvey'; /// Survey name passed in apiSetActiveSurvey, the activeSurveyName is the property name /// passed as a queryParameter and is the property in ~/.devtools too. -const String activeSurveyName = 'activeSurveyName'; +const activeSurveyName = 'activeSurveyName'; /// Returns the surveyActionTaken of the activeSurvey (apiSetActiveSurvey). -const String apiGetSurveyActionTaken = '${apiPrefix}getSurveyActionTaken'; +const apiGetSurveyActionTaken = '${apiPrefix}getSurveyActionTaken'; /// Sets the surveyActionTaken of the of the activeSurvey (apiSetActiveSurvey). -const String apiSetSurveyActionTaken = '${apiPrefix}setSurveyActionTaken'; +const apiSetSurveyActionTaken = '${apiPrefix}setSurveyActionTaken'; /// Property name to apiSetSurveyActionTaken the surveyActionTaken is the name /// passed in queryParameter: -const String surveyActionTakenPropertyName = 'surveyActionTaken'; +const surveyActionTakenPropertyName = 'surveyActionTaken'; /// Returns the surveyShownCount of the of the activeSurvey (apiSetActiveSurvey). -const String apiGetSurveyShownCount = '${apiPrefix}getSurveyShownCount'; +const apiGetSurveyShownCount = '${apiPrefix}getSurveyShownCount'; /// Increments the surveyShownCount of the of the activeSurvey (apiSetActiveSurvey). -const String apiIncrementSurveyShownCount = +const apiIncrementSurveyShownCount = '${apiPrefix}incrementSurveyShownCount'; -const String lastReleaseNotesVersionPropertyName = 'lastReleaseNotesVersion'; +const lastReleaseNotesVersionPropertyName = 'lastReleaseNotesVersion'; /// Returns the last DevTools version for which we have shown release notes. -const String apiGetLastReleaseNotesVersion = +const apiGetLastReleaseNotesVersion = '${apiPrefix}getLastReleaseNotesVersion'; /// Sets the last DevTools version for which we have shown release notes. -const String apiSetLastReleaseNotesVersion = +const apiSetLastReleaseNotesVersion = '${apiPrefix}setLastReleaseNotesVersion'; /// Returns the base app size file, if present. -const String apiGetBaseAppSizeFile = '${apiPrefix}getBaseAppSizeFile'; +const apiGetBaseAppSizeFile = '${apiPrefix}getBaseAppSizeFile'; /// Returns the test app size file used for comparing two files in a diff, if /// present. -const String apiGetTestAppSizeFile = '${apiPrefix}getTestAppSizeFile'; +const apiGetTestAppSizeFile = '${apiPrefix}getTestAppSizeFile'; -const String baseAppSizeFilePropertyName = 'appSizeBase'; +const baseAppSizeFilePropertyName = 'appSizeBase'; -const String testAppSizeFilePropertyName = 'appSizeTest'; +const testAppSizeFilePropertyName = 'appSizeTest'; + +/// Serves any available extensions and returns a list of their configurations +/// to DevTools. +const apiServeAvailableExtensions = + '${apiPrefix}serveAvailableExtensions'; + +/// The property name for the query parameter passed along with +/// [apiServeAvailableExtensions] requests to the server. +const extensionRootPathPropertyName = 'rootPath'; + +/// The property name for the response that the server sends back upon +/// receiving a [apiServeAvailableExtensions] request. +const extensionsResultPropertyName = 'extensions'; diff --git a/packages/devtools_shared/lib/src/extensions/extension_manager.dart b/packages/devtools_shared/lib/src/extensions/extension_manager.dart new file mode 100644 index 00000000000..0567cc532d4 --- /dev/null +++ b/packages/devtools_shared/lib/src/extensions/extension_manager.dart @@ -0,0 +1,150 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:io/io.dart'; +import 'package:path/path.dart' as path; + +import 'extension_model.dart'; + +/// Location where DevTools extension assets will be served, relative to where +/// DevTools assets are served (build/). +const extensionRequestPath = 'devtools_extensions'; + +/// The location for the DevTools extension, relative to the parent package's +/// root. +const extensionLocation = 'extension/devtools'; + +/// The default location for the DevTools extension, relative to +/// `/extension/devtools/`. +const extensionBuildDefault = 'build'; + +/// Responsible for storing the available DevTools extensions and managing the +/// content that DevTools server will serve at `build/devtools_extensions`. +/// +/// When [serveAvailableExtensions] is called, the available extensions will be +/// looked up using package:extension_discovery, and the available extension's +/// assets will be copied to the `build/devtools_extensions` directory that +/// DevTools server is serving. +class ExtensionsManager { + ExtensionsManager({required this.buildDir}); + + /// The build directory of DevTools that is being served by the DevTools + /// server. + final String buildDir; + + /// The directory path where DevTools extensions are being served by the + /// DevTools server. + String get _servedExtensionsPath => path.join(buildDir, extensionRequestPath); + + /// The list of available DevTools extensions that are being served by the + /// DevTools server. + /// + /// This list will be cleared and re-populated each time + /// [serveAvailableExtensions] is called. + final devtoolsExtensions = []; + + /// Serves any available DevTools extensions for the given [rootPath], where + /// [rootPath] is the root for a Dart or Flutter project containing the + /// `.dart_tool/` directory. + /// + /// This method first looks up the available extensions using + /// package:extension_discovery, and the available extension's + /// assets will be copied to the `build/devtools_extensions` directory that + /// DevTools server is serving. + Future serveAvailableExtensions(String? rootPath) async { + devtoolsExtensions.clear(); + + if (rootPath != null) { + // TODO(kenz): use 'findExtensions' from package:extension_discovery once it + // is published. + // final extensions = findExtensions( + // 'devtools', + // packageConfig: '$rootPath/.dart_tool/package_config.json', + // ); + final extensions = <_Extension>[]; + for (final extension in extensions) { + final config = extension.config; + if (config is! Map) { + // Fail gracefully. Invalid content in the extension's config.json. + continue; + } + final configAsMap = config as Map; + + // This should be relative to the 'extension/devtools/' directory and + // defaults to 'build'; + final relativeExtensionLocation = + configAsMap['buildLocation'] as String? ?? 'build'; + + final location = path.join( + extension.rootUri.toFilePath(), + extensionLocation, + relativeExtensionLocation, + ); + + try { + final pluginConfig = DevToolsExtensionConfig.parse({ + ...configAsMap, + DevToolsExtensionConfig.pathKey: location, + }); + devtoolsExtensions.add(pluginConfig); + } on StateError catch (e) { + print(e.message); + continue; + } + } + } + + _resetServedPluginsDir(); + await Future.wait([ + for (final extension in devtoolsExtensions) + _moveToServedExtensionsDir(extension.name, extension.path), + ]); + } + + void _resetServedPluginsDir() { + final buildDirectory = Directory(buildDir); + if (!buildDirectory.existsSync()) { + throw const FileSystemException('The build directory does not exist.'); + } + + // Destroy and recreate the 'devtools_extensions' directory where extension + // assets are served. + final servedExtensionsDir = Directory(_servedExtensionsPath); + if (servedExtensionsDir.existsSync()) { + servedExtensionsDir.deleteSync(recursive: true); + } + servedExtensionsDir.createSync(); + } + + Future _moveToServedExtensionsDir( + String extensionPackageName, + String extensionPath, + ) async { + final newExtensionPath = path.join( + _servedExtensionsPath, + extensionPackageName, + ); + await copyPath(extensionPath, newExtensionPath); + } +} + +/// TODO(kenz): remove this class. This is copied from +/// package:extension_discovery, which is drafed here: +/// https://github.com/dart-lang/tools/pull/129. Remove this temporary copy once +/// package:extension_discovery is published. +class _Extension { + _Extension._({ + required this.package, + required this.rootUri, + required this.packageUri, + required this.config, + }); + + final String package; + final Uri rootUri; + final Uri packageUri; + final Object? config; +} diff --git a/packages/devtools_shared/lib/src/extensions/extension_model.dart b/packages/devtools_shared/lib/src/extensions/extension_model.dart new file mode 100644 index 00000000000..b8e5f11cc87 --- /dev/null +++ b/packages/devtools_shared/lib/src/extensions/extension_model.dart @@ -0,0 +1,99 @@ +// Copyright 2023 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. + +/// Describes an extension that can be dynamically loaded into a custom screen +/// in DevTools. +class DevToolsExtensionConfig { + DevToolsExtensionConfig._({ + required this.name, + required this.path, + required this.issueTrackerLink, + required this.version, + required this.materialIconCodePoint, + }); + + factory DevToolsExtensionConfig.parse(Map json) { + // Defaults to the code point for [Icons.extensions_outlined] if null. + late int codePoint; + final codePointFromJson = json[materialIconCodePointKey]; + const defaultCodePoint = 0xf03f; + if (codePointFromJson is String?) { + codePoint = + int.tryParse(codePointFromJson ?? '0xf03f') ?? defaultCodePoint; + } else { + codePoint = codePointFromJson as int? ?? defaultCodePoint; + } + + final name = json[nameKey] as String?; + final path = json[pathKey] as String?; + final issueTrackerLink = json[issueTrackerKey] as String?; + final version = json[versionKey] as String?; + + final nullFields = [ + if (name == null) nameKey, + if (path == null) pathKey, + if (issueTrackerLink == null) issueTrackerKey, + if (version == null) versionKey, + ]; + if (nullFields.isNotEmpty) { + throw StateError( + 'missing required fields ${nullFields.toString()} in the extension ' + 'config.json', + ); + } + + return DevToolsExtensionConfig._( + name: name!, + path: path!, + issueTrackerLink: issueTrackerLink!, + version: version!, + materialIconCodePoint: codePoint, + ); + } + + static const nameKey = 'name'; + static const pathKey = 'path'; + static const issueTrackerKey = 'issueTracker'; + static const versionKey = 'version'; + static const materialIconCodePointKey = 'materialIconCodePoint'; + + /// The package name that this extension is for. + final String name; + + /// The path that this extension's assets live at. + /// + /// This location will be in the user's pub cache. + final String path; + + // TODO(kenz): we might want to add validation to these issue tracker + // links to ensure they don't point to the DevTools repo or flutter repo. + // If an invalid issue tracker link is provided, we can default to + // 'pub.dev/packages/$name'. + /// The link to the issue tracker for this DevTools extension. + /// + /// This should not point to the flutter/devtools or flutter/flutter issue + /// trackers, but rather to the issue tracker for the package that provides + /// the extension, or to the repo where the extension is developed. + final String issueTrackerLink; + + /// The version for the DevTools extension. + /// + /// This may match the version of the parent package or use a different + /// versioning system as decided by the extension author. + final String version; + + /// The code point for the material icon that will parsed by Flutter's + /// [IconData] class for displaying in DevTools. + /// + /// This code point should be part of the 'MaterialIcons' font family. + final int materialIconCodePoint; + + Map toJson() => { + nameKey: name, + pathKey: path, + issueTrackerKey: issueTrackerLink, + versionKey: version, + materialIconCodePointKey: materialIconCodePoint, + }; +} diff --git a/packages/devtools_shared/lib/src/server/server_api.dart b/packages/devtools_shared/lib/src/server/server_api.dart index fa035101be0..ef43e809bc5 100644 --- a/packages/devtools_shared/lib/src/server/server_api.dart +++ b/packages/devtools_shared/lib/src/server/server_api.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:shelf/shelf.dart' as shelf; import '../devtools_api.dart'; +import '../extensions/extension_manager.dart'; import 'file_system.dart'; import 'usage.dart'; @@ -27,7 +28,8 @@ class ServerApi { /// /// To override an API call, pass in a subclass of [ServerApi]. static FutureOr handle( - shelf.Request request, [ + shelf.Request request, + ExtensionsManager extensionsManager, [ ServerApi? api, ]) { api ??= ServerApi(); @@ -153,6 +155,9 @@ class ServerApi { return api.getCompleted( json.encode(_devToolsUsage.surveyShownCount), ); + + // ----- Release notes api. ----- + case apiGetLastReleaseNotesVersion: return api.getCompleted( json.encode(_devToolsUsage.lastReleaseNotesVersion), @@ -166,6 +171,9 @@ class ServerApi { return api.getCompleted( json.encode(_devToolsUsage.lastReleaseNotesVersion), ); + + // ----- App size api. ----- + case apiGetBaseAppSizeFile: final queryParams = request.requestedUri.queryParameters; if (queryParams.containsKey(baseAppSizeFilePropertyName)) { @@ -196,6 +204,26 @@ class ServerApi { 'contain a query parameter with the expected key: ' '$testAppSizeFilePropertyName', ); + + // ----- Extensions api. ----- + + case apiServeAvailableExtensions: + final queryParams = request.requestedUri.queryParameters; + final rootPath = queryParams[extensionRootPathPropertyName]; + if (rootPath == null) { + return api.badRequest( + '$extensionRootPathPropertyName query parameter required', + ); + } + + return extensionsManager.serveAvailableExtensions(rootPath).then((_) { + final extensions = extensionsManager.devtoolsExtensions + .map((p) => p.toJson()) + .toList(); + final result = jsonEncode({extensionsResultPropertyName: extensions}); + return api!.getCompleted(result); + }); + default: return api.notImplemented(); }