[ Widget Preview ] Add initial support for communications over the Dart Tooling Daemon (DTD) (#166698)

This will eventually be used as the main communication channel between
the widget preview scaffold, the Flutter tool, and other developer
tooling (e.g., IDEs).

Fixes #166417
This commit is contained in:
Ben Konyi 2025-04-08 14:10:50 -04:00 committed by GitHub
parent 9bf18f0971
commit 30e53b0d9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 305 additions and 3 deletions

View File

@ -258,6 +258,8 @@ List<FlutterCommand> generateCommands({required bool verboseHelp, required bool
platform: globals.platform,
shutdownHooks: globals.shutdownHooks,
os: globals.os,
processManager: globals.processManager,
artifacts: globals.artifacts!,
),
UpgradeCommand(verboseHelp: verboseHelp),
SymbolizeCommand(stdio: globals.stdio, fileSystem: globals.fs),

View File

@ -5,7 +5,9 @@
import 'package:args/args.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:process/process.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/deferred_component.dart';
import '../base/file_system.dart';
@ -24,6 +26,8 @@ import '../linux/build_linux.dart';
import '../macos/build_macos.dart';
import '../project.dart';
import '../runner/flutter_command.dart';
import '../runner/flutter_command_runner.dart';
import '../widget_preview/dtd_services.dart';
import '../widget_preview/preview_code_generator.dart';
import '../widget_preview/preview_detector.dart';
import '../widget_preview/preview_manifest.dart';
@ -41,6 +45,8 @@ class WidgetPreviewCommand extends FlutterCommand {
required Platform platform,
required ShutdownHooks shutdownHooks,
required OperatingSystemUtils os,
required ProcessManager processManager,
required Artifacts artifacts,
}) {
addSubcommand(
WidgetPreviewStartCommand(
@ -52,6 +58,8 @@ class WidgetPreviewCommand extends FlutterCommand {
platform: platform,
shutdownHooks: shutdownHooks,
os: os,
processManager: processManager,
artifacts: artifacts,
),
);
addSubcommand(
@ -118,6 +126,8 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
required this.platform,
required this.shutdownHooks,
required this.os,
required this.processManager,
required this.artifacts,
}) {
addPubOptions();
argParser
@ -152,6 +162,9 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
static const String kHeadlessWeb = 'headless-web';
static const String kWidgetPreviewScaffoldOutputDir = 'scaffold-output-dir';
/// Environment variable used to pass the DTD URI to the widget preview scaffold.
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
// Ensure the Flutter Web SDK is installed.
@ -185,6 +198,10 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
final OperatingSystemUtils os;
final ProcessManager processManager;
final Artifacts artifacts;
late final FlutterProject rootProject = getRootProject();
late final PreviewDetector _previewDetector = PreviewDetector(
@ -203,6 +220,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
cache: cache,
);
late final WidgetPreviewDtdServices _dtdService = WidgetPreviewDtdServices(
logger: logger,
shutdownHooks: shutdownHooks,
dtdLauncher: DtdLauncher(logger: logger, artifacts: artifacts, processManager: processManager),
);
/// The currently running instance of the widget preview scaffold.
AppInstance? _widgetPreviewApp;
@ -284,6 +307,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
shutdownHooks.addShutdownHook(() async {
await _widgetPreviewApp?.stop();
});
await configureDtd();
_widgetPreviewApp = await runPreviewEnvironment(
widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
);
@ -309,6 +333,31 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
_populatePreviewPubspec(rootProject: rootProject);
}
/// Configures the Dart Tooling Daemon connection.
///
/// If --dtd-uri is provided, the existing DTD instance will be used. If the tool fails to
/// connect to this URI, it will start its own DTD instance.
///
/// If --dtd-uri is not provided, a DTD instance managed by the tool will be started.
Future<void> configureDtd() async {
final String? existingDtdUriStr = stringArg(FlutterGlobalOptions.kDtdUrl, global: true);
Uri? existingDtdUri;
try {
if (existingDtdUriStr != null) {
existingDtdUri = Uri.parse(existingDtdUriStr);
}
} on FormatException {
logger.printWarning('Failed to parse value of --dtd-uri: $existingDtdUriStr.');
}
if (existingDtdUri == null) {
logger.printTrace('Launching a fresh DTD instance...');
await _dtdService.launchAndConnect();
} else {
logger.printTrace('Connecting to existing DTD instance at: $existingDtdUri...');
await _dtdService.connect(dtdWsUri: existingDtdUri);
}
}
/// Builds the application binary for the widget preview scaffold the first
/// time the widget preview command is run.
///
@ -457,6 +506,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
BuildMode.debug,
null,
treeShakeIcons: false,
// Provide the DTD connection information directly to the preview scaffold.
// This could, in theory, be provided via a follow up call to a service extension
// registered by the preview scaffold, but there's some uncertainty around how service
// extensions will work with Flutter web embedded in VSCode without a Chrome debugger
// connection.
dartDefines: <String>['$kWidgetPreviewDtdUriEnvVar=${_dtdService.dtdUri}'],
extraFrontEndOptions:
isWeb ? <String>['--dartdevc-canary', '--dartdevc-module-format=ddc'] : null,
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
@ -599,6 +654,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
if (offline) '--offline',
'--directory',
widgetPreviewScaffoldProject.directory.path,
'dtd',
'flutter_lints',
'stack_trace',
],

View File

@ -36,6 +36,7 @@ abstract final class FlutterGlobalOptions {
static const String kMachineFlag = 'machine';
static const String kPackagesOption = 'packages';
static const String kPrefixedErrorsFlag = 'prefixed-errors';
static const String kDtdUrl = 'dtd-url';
static const String kPrintDtd = 'print-dtd';
static const String kQuietFlag = 'quiet';
static const String kShowTestDeviceFlag = 'show-test-device';
@ -151,6 +152,12 @@ class FlutterCommandRunner extends CommandRunner<void> {
hide: !verboseHelp,
help: 'Path to your "package_config.json" file.',
);
argParser.addOption(
FlutterGlobalOptions.kDtdUrl,
help:
'The address of an existing Dart Tooling Daemon instance to be used by the Flutter CLI.',
hide: !verboseHelp,
);
argParser.addFlag(
FlutterGlobalOptions.kPrintDtd,
negatable: false,

View File

@ -0,0 +1,101 @@
// Copyright 2014 The Flutter 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:async';
import 'package:dtd/dtd.dart';
import 'package:process/process.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../convert.dart';
/// Provides services, streams, and RPC invocations to interact with the Widget Preview Scaffold.
class WidgetPreviewDtdServices {
WidgetPreviewDtdServices({
required this.logger,
required this.shutdownHooks,
required this.dtdLauncher,
}) {
shutdownHooks.addShutdownHook(() async {
await _dtd?.close();
await dtdLauncher.dispose();
});
}
final Logger logger;
final ShutdownHooks shutdownHooks;
final DtdLauncher dtdLauncher;
DartToolingDaemon? _dtd;
/// The [Uri] pointing to the currently connected DTD instance.
///
/// Returns `null` if there is no DTD connection.
Uri? get dtdUri => _dtdUri;
Uri? _dtdUri;
/// Starts DTD in a child process before invoking [connect] with a [Uri] pointing to the new
/// DTD instance.
Future<void> launchAndConnect() async {
// Connect to the new DTD instance.
await connect(dtdWsUri: await dtdLauncher.launch());
}
/// Connects to an existing DTD instance and registers any relevant services.
Future<void> connect({required Uri dtdWsUri}) async {
_dtdUri = dtdWsUri;
_dtd = await DartToolingDaemon.connect(dtdWsUri);
// TODO(bkonyi): register services.
logger.printTrace('Connected to DTD and registered services.');
}
}
/// Manages the lifecycle of a Dart Tooling Daemon (DTD) instance.
class DtdLauncher {
DtdLauncher({required this.logger, required this.artifacts, required this.processManager});
/// Starts a new DTD instance and returns the web socket URI it's available on.
Future<Uri> launch() async {
if (_dtdProcess != null) {
throw StateError('Attempted to launch DTD twice.');
}
// Start DTD.
_dtdProcess = await processManager.start(<Object>[
artifacts.getArtifactPath(Artifact.engineDartBinary),
'tooling-daemon',
'--machine',
]);
// Wait for the DTD connection information.
final Completer<Uri> dtdUri = Completer<Uri>();
late final StreamSubscription<String> sub;
sub = _dtdProcess!.stdout.transform(const Utf8Decoder()).listen((String data) async {
await sub.cancel();
final Map<String, Object?> jsonData = json.decode(data) as Map<String, Object?>;
if (jsonData case {'tooling_daemon_details': {'uri': final String dtdUriString}}) {
dtdUri.complete(Uri.parse(dtdUriString));
} else {
throwToolExit('Unable to start the Dart Tooling Daemon.');
}
});
return dtdUri.future;
}
/// Kills the spawned DTD instance.
Future<void> dispose() async {
_dtdProcess?.kill();
_dtdProcess = null;
}
final Logger logger;
final Artifacts artifacts;
final ProcessManager processManager;
Process? _dtdProcess;
}

View File

@ -356,6 +356,7 @@
"templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/controls.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/dtd_services.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/generated_preview.dart.tmpl",
"templates/widget_preview_scaffold/lib/src/utils.dart.tmpl",
"templates/widget_preview_scaffold/pubspec.yaml.tmpl",

View File

@ -0,0 +1,33 @@
// Copyright 2014 The Flutter 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:async';
import 'package:dtd/dtd.dart';
/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling.
class WidgetPreviewScaffoldDtdServices {
/// Environment variable for the DTD URI.
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
/// Connects to the Dart Tooling Daemon (DTD) specified by the Flutter tool.
///
/// If the connection is successful, the Widget Preview Scaffold will register services and
/// subscribe to various streams to interact directly with other tooling (e.g., IDEs).
Future<void> connect() async {
final Uri dtdWsUri = Uri.parse(
const String.fromEnvironment(kWidgetPreviewDtdUriEnvVar),
);
_dtd = await DartToolingDaemon.connect(dtdWsUri);
unawaited(
_dtd.postEvent(
'WidgetPreviewScaffold',
'Connected',
const <String, Object?>{},
),
);
}
late final DartToolingDaemon _dtd;
}

View File

@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
import 'controls.dart';
import 'dtd_services.dart';
import 'generated_preview.dart';
import 'utils.dart';
import 'widget_preview.dart';
@ -410,6 +411,8 @@ class PreviewAssetBundle extends PlatformAssetBundle {
/// the preview scaffold project which prevents us from being able to use hot
/// restart to iterate on this file.
Future<void> mainImpl() async {
// TODO(bkonyi): store somewhere.
await WidgetPreviewScaffoldDtdServices().connect();
runApp(_WidgetPreviewScaffold());
}

View File

@ -12,6 +12,7 @@ dependencies:
flutter_test:
sdk: flutter
# These will be replaced with proper constraints after the template is hydrated.
dtd: any
flutter_lints: any
stack_trace: any

View File

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
@ -47,6 +48,8 @@ void main() {
platform: platform,
processManager: processManager,
),
processManager: FakeProcessManager.any(),
artifacts: Artifacts.test(fileSystem: fileSystem),
);
rootProject = FakeFlutterProject(
projectRoot: 'some_project',

View File

@ -6,6 +6,7 @@ import 'dart:io' as io show IOOverrides;
import 'package:args/command_runner.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/bot_detector.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
@ -80,6 +81,8 @@ void main() {
logger: logger,
platform: platform,
),
artifacts: Artifacts.test(),
processManager: FakeProcessManager.any(),
),
);
await runner.run(<String>['widget-preview', ...arguments]);

View File

@ -5,12 +5,18 @@
import 'dart:async';
import 'dart:convert';
import 'package:dtd/dtd.dart';
import 'package:file/file.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/commands/widget_preview.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
import 'package:flutter_tools/src/widget_preview/dtd_services.dart';
import 'package:process/process.dart';
import '../src/common.dart';
import '../src/context.dart';
import 'test_data/basic_project.dart';
import 'test_utils.dart';
@ -43,10 +49,13 @@ const List<String> subsequentLaunchMessagesWeb = <String>[
void main() {
late Directory tempDir;
Process? process;
Logger? logger;
DtdLauncher? dtdLauncher;
final BasicProject project = BasicProject();
const ProcessManager processManager = LocalProcessManager();
setUp(() async {
logger = BufferLogger.test();
tempDir = createResolvedTempDirectorySync('widget_preview_test.');
await project.setUpIn(tempDir);
});
@ -54,12 +63,15 @@ void main() {
tearDown(() async {
process?.kill();
process = null;
await dtdLauncher?.dispose();
dtdLauncher = null;
tryToDelete(tempDir);
});
Future<void> runWidgetPreview({
required List<String> expectedMessages,
bool useWeb = false,
Uri? dtdUri,
}) async {
expect(expectedMessages, isNotEmpty);
int i = 0;
@ -72,6 +84,7 @@ void main() {
'--${WidgetPreviewStartCommand.kHeadlessWeb}'
else
'--${WidgetPreviewStartCommand.kUseFlutterDesktop}',
if (dtdUri != null) '--${FlutterGlobalOptions.kDtdUrl}=$dtdUri',
], workingDirectory: tempDir.path);
final Completer<void> completer = Completer<void>();
@ -103,8 +116,6 @@ void main() {
}),
);
await completer.future;
process!.kill();
process = null;
}
group('flutter widget-preview start', () {
@ -132,5 +143,38 @@ void main() {
// We shouldn't regenerate the scaffold after the initial run.
await runWidgetPreview(expectedMessages: subsequentLaunchMessagesWeb, useWeb: true);
});
testUsingContext('can connect to an existing DTD instance', () async {
dtdLauncher = DtdLauncher(
logger: logger!,
artifacts: globals.artifacts!,
processManager: globals.processManager,
);
// Start a DTD instance.
final Uri dtdUri = await dtdLauncher!.launch();
// Connect to it and listen to the WidgetPreviewScaffold stream.
//
// The preview scaffold will send a 'Connected' event on this stream once it has initialized
// and is ready.
final DartToolingDaemon dtdConnection = await DartToolingDaemon.connect(dtdUri);
const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold';
final Completer<void> completer = Completer<void>();
dtdConnection.onEvent(kWidgetPreviewScaffoldStream).listen((DTDEvent event) {
expect(event.stream, kWidgetPreviewScaffoldStream);
expect(event.kind, 'Connected');
completer.complete();
});
await dtdConnection.streamListen(kWidgetPreviewScaffoldStream);
// Start the widget preview and wait for the 'Connected' event.
await runWidgetPreview(
expectedMessages: firstLaunchMessagesWeb,
useWeb: true,
dtdUri: dtdUri,
);
await completer.future;
});
});
}

View File

@ -0,0 +1,33 @@
// Copyright 2014 The Flutter 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:async';
import 'package:dtd/dtd.dart';
/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling.
class WidgetPreviewScaffoldDtdServices {
/// Environment variable for the DTD URI.
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
/// Connects to the Dart Tooling Daemon (DTD) specified by the Flutter tool.
///
/// If the connection is successful, the Widget Preview Scaffold will register services and
/// subscribe to various streams to interact directly with other tooling (e.g., IDEs).
Future<void> connect() async {
final Uri dtdWsUri = Uri.parse(
const String.fromEnvironment(kWidgetPreviewDtdUriEnvVar),
);
_dtd = await DartToolingDaemon.connect(dtdWsUri);
unawaited(
_dtd.postEvent(
'WidgetPreviewScaffold',
'Connected',
const <String, Object?>{},
),
);
}
late final DartToolingDaemon _dtd;
}

View File

@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
import 'controls.dart';
import 'dtd_services.dart';
import 'generated_preview.dart';
import 'utils.dart';
import 'widget_preview.dart';
@ -410,6 +411,8 @@ class PreviewAssetBundle extends PlatformAssetBundle {
/// the preview scaffold project which prevents us from being able to use hot
/// restart to iterate on this file.
Future<void> mainImpl() async {
// TODO(bkonyi): store somewhere.
await WidgetPreviewScaffoldDtdServices().connect();
runApp(_WidgetPreviewScaffold());
}

View File

@ -12,6 +12,7 @@ dependencies:
flutter_test:
sdk: flutter
# These will be replaced with proper constraints after the template is hydrated.
dtd: 2.5.0
flutter_lints: 5.0.0
stack_trace: 1.12.1
@ -20,7 +21,13 @@ dependencies:
characters: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
clock: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
collection: 1.19.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
convert: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
crypto: 3.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
fake_async: 1.3.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
file: 7.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_parser: 4.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
json_rpc_2: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
leak_tracker: 10.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
leak_tracker_flutter_testing: 3.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
leak_tracker_testing: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
@ -34,10 +41,15 @@ dependencies:
string_scanner: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
term_glyph: 1.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_api: 0.7.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
typed_data: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
unified_analytics: 7.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vector_math: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vm_service: 15.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web_socket: 0.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web_socket_channel: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
flutter:
uses-material-design: true
# PUBSPEC CHECKSUM: 367e
# PUBSPEC CHECKSUM: 4b12