Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ To learn more about DevTools, check out the
evaluate expressions and a debug-only screen is unavailable.
[#4091](https://github.com/flutter/devtools/issues/4091)

* Fixed an issue where Dart and Flutter web apps compiled with WebAssembly
(Wasm) or modern web libraries (such as `package:web`) were not recognized as
web applications. [#10045](https://github.com/flutter/devtools/pull/10045)

TODO: Remove this section if there are not any updates.

* Improved the readability of ANSI-colored text in the console and logging
Expand Down
4 changes: 4 additions & 0 deletions packages/devtools_app_shared/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ found in the LICENSE file or at https://developers.google.com/open-source/licens
-->
## 0.5.2-wip
* Removed redundant argument values.
* Detect Dart web apps by the presence of `dart:js_interop` in addition to
`dart:html`, so dart2wasm and `package:web` apps are recognized without
dropping support for older `dart:html` apps. Adds the
`dartJsInteropLibraryUri` and `dartWebLibraryUris` constants.
* Fix `forEachIsolateHelper` to rethrow isolate callback exceptions directly
using `Future.wait` instead of wrapping them in a `ParallelWaitError`.
* Fix a `RangeError` thrown by `SplitPane` when the number of children
Expand Down
38 changes: 34 additions & 4 deletions packages/devtools_app_shared/lib/src/service/connected_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,28 @@ final _log = Logger('connected_app');

const flutterLibraryUri = 'package:flutter/src/widgets/binding.dart';

// TODO(kenz): if we want to support debugging dart2wasm web apps, we will need
// to check for the presence of a different library.
/// Present in older Dart and Flutter web apps, including google3.
///
/// Modern apps may use `package:web` / [dartJsInteropLibraryUri] instead.
const dartHtmlLibraryUri = 'dart:html';

/// Present in Dart web apps compiled to JavaScript or Wasm.
///
/// `package:web` is built on this library, so it is the reliable signal for
/// modern and dart2wasm apps that no longer import [dartHtmlLibraryUri].
/// Library URIs are matched by prefix, so this also covers
/// `dart:js_interop_unsafe`.
const dartJsInteropLibraryUri = 'dart:js_interop';

/// Libraries whose presence indicates the connected app is a Dart web app.
///
/// [dartHtmlLibraryUri] is kept so older apps (including google3) are still
/// detected.
const dartWebLibraryUris = [
dartHtmlLibraryUri,
dartJsInteropLibraryUri,
];

// TODO(https://github.com/flutter/devtools/issues/6239): try to remove this.
@sealed
class ConnectedApp {
Expand Down Expand Up @@ -85,8 +103,15 @@ class ConnectedApp {
bool? _isProfileBuild;

// TODO(kenz): investigate if we can use `libraryUriAvailableNow` instead.
Future<bool> get isDartWebApp async => _isDartWebApp ??= await serviceManager!
.libraryUriAvailable(dartHtmlLibraryUri);
Future<bool> get isDartWebApp async {
if (_isDartWebApp != null) return _isDartWebApp!;
for (final uri in dartWebLibraryUris) {
if (await serviceManager!.libraryUriAvailable(uri)) {
return _isDartWebApp = true;
}
}
return _isDartWebApp = false;
}
Comment thread
srawlins marked this conversation as resolved.
Resolved

bool? get isDartWebAppNow {
assert(_isDartWebApp != null);
Expand Down Expand Up @@ -230,6 +255,11 @@ final class OfflineConnectedApp extends ConnectedApp {
@override
final bool? isDartWebAppNow;

/// There is no [serviceManager] to query for an offline app, so this
/// returns the value that was recorded when the app was exported.
@override
Future<bool> get isDartWebApp async => isDartWebAppNow ?? false;

@override
final bool? isRunningOnDartVM;

Expand Down
127 changes: 127 additions & 0 deletions packages/devtools_app_shared/test/service/connected_app_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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 'package:devtools_app_shared/service.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('ConnectedApp', () {
test('isDartWebApp is true when dart:html is available', () async {
final serviceManager = _FakeServiceManager(
availableLibraries: ['dart:html'],
);
final app = ConnectedApp(serviceManager);
expect(await app.isDartWebApp, isTrue);
expect(app.isDartWebAppNow, isTrue);
});

test('isDartWebApp is true when dart:js_interop is available', () async {
final serviceManager = _FakeServiceManager(
availableLibraries: ['dart:js_interop'],
);
final app = ConnectedApp(serviceManager);
expect(await app.isDartWebApp, isTrue);
expect(app.isDartWebAppNow, isTrue);
});

test('isDartWebApp is false when no web libraries are available', () async {
final serviceManager = _FakeServiceManager(
availableLibraries: [
'dart:io',
'package:flutter/src/widgets/binding.dart',
],
);
final app = ConnectedApp(serviceManager);
expect(await app.isDartWebApp, isFalse);
expect(app.isDartWebAppNow, isFalse);
});

test('isDartWebApp short-circuits on first matching library', () async {
final queriedLibraries = <String>[];
final serviceManager = _FakeServiceManager(
availableLibraries: ['dart:html', 'dart:js_interop'],
onLibraryUriAvailable: queriedLibraries.add,
);
final app = ConnectedApp(serviceManager);

expect(await app.isDartWebApp, isTrue);
expect(queriedLibraries, equals(['dart:html']));
});

test('isDartWebApp caches results', () async {
var callCount = 0;
final serviceManager = _FakeServiceManager(
availableLibraries: ['dart:js_interop'],
onLibraryUriAvailable: (_) => callCount++,
);
final app = ConnectedApp(serviceManager);

expect(await app.isDartWebApp, isTrue);
final callsAfterFirst = callCount;
expect(callsAfterFirst, greaterThan(0));

expect(await app.isDartWebApp, isTrue);
expect(callCount, equals(callsAfterFirst));
});

test('isDartWebAppNow asserts when not initialized', () {
final serviceManager = _FakeServiceManager();
final app = ConnectedApp(serviceManager);
expect(() => app.isDartWebAppNow, throwsAssertionError);
});

test(
'isFlutterWebAppNow is true and isFlutterNativeAppNow is false for modern Flutter web apps',
() async {
final serviceManager = _FakeServiceManager(
availableLibraries: [flutterLibraryUri, 'dart:js_interop'],
);
final app = ConnectedApp(serviceManager);
expect(await app.isFlutterApp, isTrue);
expect(await app.isDartWebApp, isTrue);
expect(app.isFlutterWebAppNow, isTrue);
expect(app.isFlutterNativeAppNow, isFalse);
},
);
});

group('OfflineConnectedApp', () {
test(
'isDartWebApp returns the recorded value without a service manager',
() async {
expect(
await OfflineConnectedApp(isDartWebAppNow: true).isDartWebApp,
isTrue,
);
expect(
await OfflineConnectedApp(isDartWebAppNow: false).isDartWebApp,
isFalse,
);
expect(await OfflineConnectedApp().isDartWebApp, isFalse);
},
);
});
}

class _FakeServiceManager extends Fake implements ServiceManager {
_FakeServiceManager({
this.availableLibraries = const [],
this.onLibraryUriAvailable,
});

final List<String> availableLibraries;
final void Function(String uri)? onLibraryUriAvailable;

@override
bool libraryUriAvailableNow(String? uri) {
if (uri == null) return false;
onLibraryUriAvailable?.call(uri);
return availableLibraries.any((u) => u.startsWith(uri));
}

@override
Future<bool> libraryUriAvailable(String uri) async {
return libraryUriAvailableNow(uri);
}
}
Loading