Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions packages/devtools_app/lib/src/framework/framework_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:vm_service/vm_service.dart';

import '../extensions/extension_service.dart';
import '../screens/debugger/breakpoint_manager.dart';
import '../service/post_message_service.dart';
import '../service/service_manager.dart';
import '../service/vm_service_wrapper.dart';
import '../shared/analytics/analytics.dart' as ga;
Expand Down Expand Up @@ -140,29 +141,34 @@ extension FrameworkCore on Never {
final finishedCompleter = Completer<void>();

try {
final service = await connect<VmServiceWrapper>(
uri: uri,
finishedCompleter: finishedCompleter,
serviceFactory:
({
// ignore: avoid-dynamic, mirrors types of [VmServiceFactory].
required Stream<dynamic> /*String|List<int>*/ inStream,
required void Function(String message) writeMessage,
Log? log,
DisposeHandler? disposeHandler,
Future? streamClosed,
String? wsUri,
bool trackFutures = false,
}) => VmServiceWrapper.defaultFactory(
inStream: inStream,
writeMessage: writeMessage,
log: log,
disposeHandler: disposeHandler,
streamClosed: streamClosed,
wsUri: wsUri,
trackFutures: integrationTestMode,
),
);
final service = isPostMessageUri(uri)
? await connectWithPostMessage(
uri: uri,
finishedCompleter: finishedCompleter,
)
: await connect<VmServiceWrapper>(
uri: uri,
finishedCompleter: finishedCompleter,
serviceFactory:
({
// ignore: avoid-dynamic, mirrors types of [VmServiceFactory].
required Stream<dynamic> /*String|List<int>*/ inStream,
required void Function(String message) writeMessage,
Log? log,
DisposeHandler? disposeHandler,
Future? streamClosed,
String? wsUri,
bool trackFutures = false,
}) => VmServiceWrapper.defaultFactory(
inStream: inStream,
writeMessage: writeMessage,
log: log,
disposeHandler: disposeHandler,
streamClosed: streamClosed,
wsUri: wsUri,
trackFutures: integrationTestMode,
),
);

await serviceConnection.serviceManager.vmServiceOpened(
service,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright 2026 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:async';

import 'vm_service_wrapper.dart';

/// Connects to a VM Service using a `MessagePort` transferred via postMessage.
Future<VmServiceWrapper> connectWithPostMessage({
required Uri uri,
required Completer<void> finishedCompleter,
}) => throw UnsupportedError('unsupported platform');
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2026 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:async';
import 'dart:convert';
import 'dart:js_interop';

import 'package:web/web.dart' as web;

import 'vm_service_wrapper.dart';

/// Extracts the target origin for window.postMessage from [uri].
///
/// The URI is expected in the form `post-message:<origin>` (e.g.
/// `post-message:https://dartpad.dev` or `post-message:*`).
String _extractTargetOrigin(Uri uri) {
if (uri.path.isEmpty || uri.path == '*') {
return '*';
}
final target = Uri.tryParse(uri.path);
if (target != null && target.hasScheme && target.hasAuthority) {
return target.origin;
}
return uri.path;
}

/// Connects to a Dart VM service via a browser `MessagePort`.
///
/// Creates a [web.MessageChannel], transfers `port1` to `window.parent`
/// via a postMessage with payload `{'vmServicePort': port1}`, and binds
/// `port2` to a [VmServiceWrapper].
Future<VmServiceWrapper> connectWithPostMessage({
required Uri uri,
required Completer<void> finishedCompleter,
}) async {
final targetOrigin = _extractTargetOrigin(uri);
final channel = web.MessageChannel();
// ignore: avoid-dynamic, mirrors types of [Stream<dynamic> inStream] in [VmService].
final inStreamController = StreamController<dynamic>();

final message = {'vmServicePort': channel.port1}.jsify();
web.window.parent?.postMessage(
message,
web.WindowPostMessageOptions(
targetOrigin: targetOrigin,
transfer: [channel.port1].toJS,
),
);

channel.port2.onmessage = ((web.MessageEvent event) {
final data = event.data;
if (data != null) {
if (data.isA<JSString>()) {
inStreamController.add((data as JSString).toDart);
} else if (data.isA<JSUint8Array>()) {
inStreamController.add((data as JSUint8Array).toDart);
} else if (data.isA<JSArrayBuffer>()) {
inStreamController.add((data as JSArrayBuffer).toDart.asUint8List());
} else {
inStreamController.add(jsonEncode(data.dartify()));
}
}
}).toJS;
channel.port2.start();

final service = VmServiceWrapper.defaultFactory(
inStream: inStreamController.stream,
writeMessage: (String msg) {
channel.port2.postMessage(msg.toJS);
},
streamClosed: finishedCompleter.future,
wsUri: uri.toString(),
);

unawaited(
finishedCompleter.future.then((_) async {
channel.port2.close();
await inStreamController.close();
await service.dispose();
}),
);

// Validate the connection with a getVersion call.
await service.getVersion();

return service;
}
14 changes: 14 additions & 0 deletions packages/devtools_app/lib/src/service/post_message_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2026 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.

export '_post_message_service_stub.dart'
if (dart.library.js_interop) '_post_message_service_web.dart';

/// Returns whether [uri] uses the `post-message:` or `postmessage:` scheme.
///
/// This scheme is used when DevTools is embedded in an `<iframe>` and
/// communicates with the target Dart/Flutter application via a transferred
/// `MessagePort` instead of a WebSocket or SSE connection.
bool isPostMessageUri(Uri uri) =>
uri.scheme == 'post-message' || uri.scheme == 'postmessage';
Loading