From ad5969d2e20eb4de415488f33afb61868fa74394 Mon Sep 17 00:00:00 2001 From: hkarmoush Date: Sat, 17 Oct 2026 23:33:32 +0300 Subject: [PATCH 1/4] Detect Dart web apps via dart:js_interop libraries --- .../release_notes/NEXT_RELEASE_NOTES.md | 4 + packages/devtools_app_shared/CHANGELOG.md | 5 + .../lib/src/service/connected_app.dart | 35 ++++- .../test/service/connected_app_test.dart | 135 ++++++++++++++++++ .../lib/src/mocks/fake_service_manager.dart | 5 + 5 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 packages/devtools_app_shared/test/service/connected_app_test.dart diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 1aadb2c50a5..c4e896a7095 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -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. [#TODO](https://github.com/flutter/devtools/pull/TODO) + TODO: Remove this section if there are not any updates. * Improved the readability of ANSI-colored text in the console and logging diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index ee3e1246427..91cb315b138 100644 --- a/packages/devtools_app_shared/CHANGELOG.md +++ b/packages/devtools_app_shared/CHANGELOG.md @@ -5,6 +5,11 @@ 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` or + `dart:js_interop_unsafe` in addition to `dart:html`, so dart2wasm and + `package:web` apps are recognized without dropping support for older + `dart:html` apps. Adds the `dartJsInteropLibraryUri`, + `dartJsInteropUnsafeLibraryUri`, 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 diff --git a/packages/devtools_app_shared/lib/src/service/connected_app.dart b/packages/devtools_app_shared/lib/src/service/connected_app.dart index 5c926ef4b06..1c5d4955a79 100644 --- a/packages/devtools_app_shared/lib/src/service/connected_app.dart +++ b/packages/devtools_app_shared/lib/src/service/connected_app.dart @@ -15,10 +15,30 @@ 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]. +const dartJsInteropLibraryUri = 'dart:js_interop'; + +/// Part of the modern Dart web JS interop library suite. +const dartJsInteropUnsafeLibraryUri = 'dart:js_interop_unsafe'; + +/// 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, + dartJsInteropUnsafeLibraryUri, +]; + // TODO(https://github.com/flutter/devtools/issues/6239): try to remove this. @sealed class ConnectedApp { @@ -85,8 +105,15 @@ class ConnectedApp { bool? _isProfileBuild; // TODO(kenz): investigate if we can use `libraryUriAvailableNow` instead. - Future get isDartWebApp async => _isDartWebApp ??= await serviceManager! - .libraryUriAvailable(dartHtmlLibraryUri); + Future get isDartWebApp async { + if (_isDartWebApp != null) return _isDartWebApp!; + for (final uri in dartWebLibraryUris) { + if (await serviceManager!.libraryUriAvailable(uri)) { + return _isDartWebApp = true; + } + } + return _isDartWebApp = false; + } bool? get isDartWebAppNow { assert(_isDartWebApp != null); diff --git a/packages/devtools_app_shared/test/service/connected_app_test.dart b/packages/devtools_app_shared/test/service/connected_app_test.dart new file mode 100644 index 00000000000..f43a391e82e --- /dev/null +++ b/packages/devtools_app_shared/test/service/connected_app_test.dart @@ -0,0 +1,135 @@ +// 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 true when dart:js_interop_unsafe is available', + () async { + final serviceManager = _FakeServiceManager( + availableLibraries: ['dart:js_interop_unsafe'], + ); + 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 = []; + 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('constants', () { + test('dartWebLibraryUris contains all web library URIs', () { + expect( + dartWebLibraryUris, + containsAll([ + dartHtmlLibraryUri, + dartJsInteropLibraryUri, + dartJsInteropUnsafeLibraryUri, + ]), + ); + }); + }); +} + +class _FakeServiceManager extends Fake implements ServiceManager { + _FakeServiceManager({ + this.availableLibraries = const [], + this.onLibraryUriAvailable, + }); + + final List 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 libraryUriAvailable(String uri) async { + return libraryUriAvailableNow(uri); + } +} diff --git a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart index bb2b821ce0d..cd1cce3b950 100644 --- a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart +++ b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart @@ -234,6 +234,11 @@ class FakeServiceManager extends Fake return availableLibraries.any((u) => u.startsWith(uri)); } + @override + Future libraryUriAvailable(String uri) async { + return libraryUriAvailableNow(uri); + } + @override Future manuallyDisconnect() async { setConnectedState(false, manual: true); From 1d918b8944e9612e929246b05a82eee8784c4821 Mon Sep 17 00:00:00 2001 From: hkarmoush Date: Sat, 17 Oct 2026 23:33:48 +0300 Subject: [PATCH 2/4] Update release note PR link --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index c4e896a7095..c8b38814c06 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -24,7 +24,7 @@ To learn more about DevTools, check out the * 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. [#TODO](https://github.com/flutter/devtools/pull/TODO) + web applications. [#10045](https://github.com/flutter/devtools/pull/10045) TODO: Remove this section if there are not any updates. From d2ae7052c7e732fb4ce395ec9724bd47425116ef Mon Sep 17 00:00:00 2001 From: hkarmoush Date: Tue, 20 Oct 2026 00:21:35 +0300 Subject: [PATCH 3/4] Handle offline apps in ConnectedApp.isDartWebApp --- .../lib/src/service/connected_app.dart | 5 +++++ .../test/service/connected_app_test.dart | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/devtools_app_shared/lib/src/service/connected_app.dart b/packages/devtools_app_shared/lib/src/service/connected_app.dart index 1c5d4955a79..94d7e96f990 100644 --- a/packages/devtools_app_shared/lib/src/service/connected_app.dart +++ b/packages/devtools_app_shared/lib/src/service/connected_app.dart @@ -257,6 +257,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 get isDartWebApp async => isDartWebAppNow ?? false; + @override final bool? isRunningOnDartVM; diff --git a/packages/devtools_app_shared/test/service/connected_app_test.dart b/packages/devtools_app_shared/test/service/connected_app_test.dart index f43a391e82e..b9153d2a82c 100644 --- a/packages/devtools_app_shared/test/service/connected_app_test.dart +++ b/packages/devtools_app_shared/test/service/connected_app_test.dart @@ -98,6 +98,23 @@ void main() { ); }); + 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); + }, + ); + }); + group('constants', () { test('dartWebLibraryUris contains all web library URIs', () { expect( From aca4b526c8d92e25d7c8d7388264088290206401 Mon Sep 17 00:00:00 2001 From: hkarmoush Date: Fri, 13 Nov 2026 15:40:08 +0300 Subject: [PATCH 4/4] Address review: drop redundant js_interop_unsafe check and unused test code --- packages/devtools_app_shared/CHANGELOG.md | 9 +++---- .../lib/src/service/connected_app.dart | 6 ++--- .../test/service/connected_app_test.dart | 25 ------------------- .../lib/src/mocks/fake_service_manager.dart | 5 ---- 4 files changed, 6 insertions(+), 39 deletions(-) diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index 91cb315b138..5b9d827c86a 100644 --- a/packages/devtools_app_shared/CHANGELOG.md +++ b/packages/devtools_app_shared/CHANGELOG.md @@ -5,11 +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` or - `dart:js_interop_unsafe` in addition to `dart:html`, so dart2wasm and - `package:web` apps are recognized without dropping support for older - `dart:html` apps. Adds the `dartJsInteropLibraryUri`, - `dartJsInteropUnsafeLibraryUri`, and `dartWebLibraryUris` constants. +* 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 diff --git a/packages/devtools_app_shared/lib/src/service/connected_app.dart b/packages/devtools_app_shared/lib/src/service/connected_app.dart index 94d7e96f990..da35a76e43c 100644 --- a/packages/devtools_app_shared/lib/src/service/connected_app.dart +++ b/packages/devtools_app_shared/lib/src/service/connected_app.dart @@ -24,11 +24,10 @@ const dartHtmlLibraryUri = 'dart:html'; /// /// `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'; -/// Part of the modern Dart web JS interop library suite. -const dartJsInteropUnsafeLibraryUri = 'dart:js_interop_unsafe'; - /// Libraries whose presence indicates the connected app is a Dart web app. /// /// [dartHtmlLibraryUri] is kept so older apps (including google3) are still @@ -36,7 +35,6 @@ const dartJsInteropUnsafeLibraryUri = 'dart:js_interop_unsafe'; const dartWebLibraryUris = [ dartHtmlLibraryUri, dartJsInteropLibraryUri, - dartJsInteropUnsafeLibraryUri, ]; // TODO(https://github.com/flutter/devtools/issues/6239): try to remove this. diff --git a/packages/devtools_app_shared/test/service/connected_app_test.dart b/packages/devtools_app_shared/test/service/connected_app_test.dart index b9153d2a82c..b0fb5364464 100644 --- a/packages/devtools_app_shared/test/service/connected_app_test.dart +++ b/packages/devtools_app_shared/test/service/connected_app_test.dart @@ -25,18 +25,6 @@ void main() { expect(app.isDartWebAppNow, isTrue); }); - test( - 'isDartWebApp is true when dart:js_interop_unsafe is available', - () async { - final serviceManager = _FakeServiceManager( - availableLibraries: ['dart:js_interop_unsafe'], - ); - 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: [ @@ -114,19 +102,6 @@ void main() { }, ); }); - - group('constants', () { - test('dartWebLibraryUris contains all web library URIs', () { - expect( - dartWebLibraryUris, - containsAll([ - dartHtmlLibraryUri, - dartJsInteropLibraryUri, - dartJsInteropUnsafeLibraryUri, - ]), - ); - }); - }); } class _FakeServiceManager extends Fake implements ServiceManager { diff --git a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart index cd1cce3b950..bb2b821ce0d 100644 --- a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart +++ b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart @@ -234,11 +234,6 @@ class FakeServiceManager extends Fake return availableLibraries.any((u) => u.startsWith(uri)); } - @override - Future libraryUriAvailable(String uri) async { - return libraryUriAvailableNow(uri); - } - @override Future manuallyDisconnect() async { setConnectedState(false, manual: true);