[dds/devtools_server] Add support for finding VS Code extensions defined in packages

See https://github.com/Dart-Code/Dart-Code/issues/4705

Change-Id: I724c039cd6940dd5939330a6f91f38567db9a179
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/337781
Reviewed-by: Kenzie Davisson <kenzieschmoll@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Kenzie Davisson <kenzieschmoll@google.com>
This commit is contained in:
Danny Tuppeny 2023-12-06 20:56:11 +00:00 committed by Commit Queue
parent 790657f1da
commit 5fde2e1534
10 changed files with 312 additions and 6 deletions

View file

@ -21,7 +21,7 @@ import 'src/devtools/utils.dart';
import 'src/utils/console.dart';
class DevToolsServer {
static const protocolVersion = '1.1.0';
static const protocolVersion = '1.2.0';
static const defaultTryPorts = 10;
static const commandDescription =
'Open DevTools (optionally connecting to an existing application).';

View file

@ -14,6 +14,7 @@ import 'package:vm_service/vm_service.dart';
import '../../devtools_server.dart';
import 'utils.dart';
import 'vs_code.dart';
class MachineModeCommandHandler {
static const launchDevToolsService = 'launchDevTools';
@ -82,6 +83,9 @@ class MachineModeCommandHandler {
case 'devTools.survey':
_handleDevToolsSurvey(id, params);
break;
case 'vscode.extensions.discover':
await _handleVsCodeExtensionsDiscover(id, params);
break;
default:
DevToolsUtils.printOutput(
'Unknown method $method',
@ -197,6 +201,36 @@ class MachineModeCommandHandler {
);
}
Future<void> _handleVsCodeExtensionsDiscover(
dynamic id, Map<String, dynamic> params) async {
if (params case {'rootPaths': List rootPaths}) {
final manager = VsCodeExtensionsManager();
DevToolsUtils.printOutput(
'Extensions',
{
'id': id,
'result': {
for (final rootPath in rootPaths.cast<String>())
rootPath: await manager.findVsCodeExtensions(rootPath),
}
},
machineMode: machineMode,
);
} else {
final errorMessage =
"Invalid input: $params does not contain 'List<String> rootPaths'";
DevToolsUtils.printOutput(
errorMessage,
{
'id': id,
'error': errorMessage,
},
machineMode: machineMode,
);
}
}
void _handleDevToolsSurvey(dynamic id, Map<String, dynamic> params) {
_devToolsUsage ??= DevToolsUsage();
final String surveyRequest = params['surveyRequest'];

View file

@ -0,0 +1,131 @@
// 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 'package:extension_discovery/extension_discovery.dart';
import 'package:path/path.dart' as path;
class VsCodeExtensionsManager {
/// Reads metadata about the VS Code extensions for the packages used by the
/// project at [rootPath].
Future<VsCodeExtensionResults> findVsCodeExtensions(String rootPath) async {
const targetName = 'vs_code';
final packageConfig =
Uri.file(path.join(rootPath, '.dart_tool', 'package_config.json'));
final results = VsCodeExtensionResults();
try {
final extensions = await findExtensions(
targetName,
packageConfig: packageConfig,
);
for (final extension in extensions) {
try {
results.extensions.add(
VsCodeExtensionConfig.parse(extension.package, extension.config),
);
} on Error catch (e) {
results.parseErrors.add(VsCodeExtensionParseError(
packageName: extension.package, error: e.toString()));
}
}
} on PackageConfigException {
// If the package_config doesn't exist or is invalid, we'll just return
// the empty results.
}
return results;
}
}
class VsCodeExtensionResults {
final extensions = <VsCodeExtensionConfig>[];
final parseErrors = <VsCodeExtensionParseError>[];
Map<String, Object?> toJson() => {
'extensions': extensions,
'parseErrors': parseErrors,
};
}
class VsCodeExtensionParseError {
VsCodeExtensionParseError({required this.packageName, required this.error});
final String packageName;
final String error;
Map<String, Object?> toJson() => {
'packageName': packageName,
'error': error,
};
}
class VsCodeExtensionConfig {
VsCodeExtensionConfig._({
required this.packageName,
required this.vsCodeExtensionId,
});
factory VsCodeExtensionConfig.parse(
String packageName,
Map<String, Object?> json,
) {
if (json
case {
vsCodeExtensionIdKey: final String vsCodeExtensionId,
}) {
return VsCodeExtensionConfig._(
packageName: packageName,
vsCodeExtensionId: vsCodeExtensionId,
);
} else {
const requiredKeysFromConfigFile = <String>{
vsCodeExtensionIdKey,
};
final missing = requiredKeysFromConfigFile.difference(json.keys.toSet());
if (missing.isNotEmpty) {
throw StateError(
'Missing required fields $missing in the extension '
'config.yaml.',
);
} else {
// All the required keys are present, but the value types did not match.
final sb = StringBuffer();
for (final entry in json.entries) {
sb.writeln(
' ${entry.key}: ${entry.value} (${entry.value.runtimeType})',
);
}
throw StateError(
'Unexpected value types in the extension config.yaml. Expected all '
'values to be of type String, but one or more had a different type:\n'
'$sb',
);
}
}
}
Map<String, Object?> toJson() => {
'packageName': packageName,
VsCodeExtensionConfig.vsCodeExtensionIdKey: vsCodeExtensionId,
};
/// The YAML key for the ID of the VS Code extension.
///
/// This is 'extension' in the YAML because users have to type it and it's
/// already in a VS-Code-specific file, but in code we use a more descriptive
/// name.
static const vsCodeExtensionIdKey = 'extension';
/// The name of the package promoting the extension.
final String packageName;
/// The ID of the VS Code extension being promoted.
///
/// The format should be 'publisher.extension' matching what's shown inside
/// VS Code and on the market place.
final String vsCodeExtensionId;
}

View file

@ -15,6 +15,7 @@ dependencies:
collection: ^1.15.0
dds_service_extensions: ^1.6.0
dap: ^1.1.0
extension_discovery: ^2.0.0
devtools_shared: ^6.0.0
http_multi_server: ^3.0.0
json_rpc_2: ^3.0.0

View file

@ -0,0 +1,7 @@
name: empty_dart_app
environment:
sdk: '>=3.0.0'
dependencies:
package_with_extensions:
path: ../package_with_extensions

View file

@ -0,0 +1 @@
extension: fake-publisher.fake-extension

View file

@ -0,0 +1,3 @@
name: package_with_extensions
environment:
sdk: '>=3.0.0'

View file

@ -98,6 +98,8 @@ class DevToolsServerDriver {
class DevToolsServerTestController {
static const defaultDelay = Duration(milliseconds: 500);
late Uri emptyDartAppRoot;
late Uri packageWithExtensionsRoot;
late CliAppFixture appFixture;
late DevToolsServerDriver server;
@ -127,7 +129,7 @@ class DevToolsServerTestController {
late StreamSubscription<Map<String, dynamic>?> stdoutSub;
Future<void> setUp() async {
Future<void> setUp({bool runPubGet = false}) async {
serverStartedEvent = Completer<Map<String, dynamic>>();
eventController = StreamController<Map<String, dynamic>>.broadcast();
@ -152,7 +154,7 @@ class DevToolsServerTestController {
});
await serverStartedEvent.future;
await startApp();
await startApp(runPubGet: runPubGet);
}
Future<void> tearDown() async {
@ -200,9 +202,25 @@ class DevToolsServerTestController {
return response['params'];
}
Future<void> startApp() async {
final appUri =
Platform.script.resolveUri(Uri.parse('fixtures/empty_dart_app.dart'));
Future<void> startApp({bool runPubGet = false}) async {
emptyDartAppRoot =
Platform.script.resolveUri(Uri.parse('fixtures/empty_dart_app/'));
packageWithExtensionsRoot = Platform.script
.resolveUri(Uri.parse('fixtures/package_with_extensions/'));
if (runPubGet) {
final pubResult = await Process.run(
Platform.resolvedExecutable, ['pub', 'get'],
workingDirectory: emptyDartAppRoot.toFilePath());
if (pubResult.exitCode != 0) {
throw 'Failed to run "dart pub get" in test fixture:\n'
'${utf8.decode(pubResult.stdout)}\n'
'${utf8.decode(pubResult.stderr)}'
.trim();
}
}
final appUri = emptyDartAppRoot.resolveUri(Uri.parse('bin/main.dart'));
appFixture = await CliAppFixture.create(appUri.toFilePath());
// Track services method names as they're registered.

View file

@ -0,0 +1,111 @@
// 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 'dart:io';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'utils/server_driver.dart';
late final DevToolsServerTestController testController;
void main() {
testController = DevToolsServerTestController();
late String emptyDartAppRoot;
late File extensionConfig;
setUp(() async {
await testController.setUp(runPubGet: true);
emptyDartAppRoot = testController.emptyDartAppRoot.toFilePath();
extensionConfig = File(path.join(
testController.packageWithExtensionsRoot.toFilePath(),
'extension',
'vs_code',
'config.yaml'));
extensionConfig
.writeAsStringSync('extension: fake-publisher.fake-extension');
});
tearDown(() async {
await testController.tearDown();
});
group('Server API - VS Code Extensions', () {
test('can list valid extensions', () async {
final results = await testController.send(
'vscode.extensions.discover',
{
'rootPaths': [emptyDartAppRoot]
},
);
expect(
results,
{
emptyDartAppRoot: {
'extensions': [
{
'packageName': 'package_with_extensions',
'extension': 'fake-publisher.fake-extension'
},
],
'parseErrors': [],
},
},
);
}, timeout: const Timeout.factor(10));
test('returns parse errors for extension/vs_code/config.yaml', () async {
extensionConfig.writeAsStringSync('a: b');
final results = await testController.send(
'vscode.extensions.discover',
{
'rootPaths': [emptyDartAppRoot]
},
);
expect(
results,
{
emptyDartAppRoot: {
'extensions': [],
'parseErrors': [
{
'packageName': 'package_with_extensions',
'error': 'Bad state: Missing required fields {extension} '
'in the extension config.yaml.'
}
]
},
},
);
});
test('does not fail on non-existent or non-project folders', () async {
final notExisting = path.join(emptyDartAppRoot, 'does_not_exist');
final notProject = path.join(emptyDartAppRoot, 'bin');
final results = await testController.send(
'vscode.extensions.discover',
{
'rootPaths': [notExisting, notProject]
},
);
expect(
results,
{
notExisting: {
'extensions': [],
'parseErrors': [],
},
notProject: {
'extensions': [],
'parseErrors': [],
},
},
);
});
}, timeout: const Timeout.factor(10));
}