mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 08:44:27 +00:00
[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:
parent
790657f1da
commit
5fde2e1534
10 changed files with 312 additions and 6 deletions
|
@ -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).';
|
||||
|
|
|
@ -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'];
|
||||
|
|
131
pkg/dds/lib/src/devtools/vs_code.dart
Normal file
131
pkg/dds/lib/src/devtools/vs_code.dart
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
name: empty_dart_app
|
||||
environment:
|
||||
sdk: '>=3.0.0'
|
||||
|
||||
dependencies:
|
||||
package_with_extensions:
|
||||
path: ../package_with_extensions
|
|
@ -0,0 +1 @@
|
|||
extension: fake-publisher.fake-extension
|
|
@ -0,0 +1,3 @@
|
|||
name: package_with_extensions
|
||||
environment:
|
||||
sdk: '>=3.0.0'
|
|
@ -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.
|
||||
|
|
111
pkg/dds/test/devtools_server/vs_code_extensions_test.dart
Normal file
111
pkg/dds/test/devtools_server/vs_code_extensions_test.dart
Normal 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));
|
||||
}
|
Loading…
Reference in a new issue