From eb6456def182adc0cf9c7b9d53876583c539c0b3 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, 179 insertions(+), 5 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 979b0ff2d03..e3cc8f9b314 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -15,7 +15,9 @@ To learn more about DevTools, check out the ## General updates -TODO: Remove this section if there are not any updates. +* 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) ## Inspector updates diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index 577b5c24924..489e7690f0d 100644 --- a/packages/devtools_app_shared/CHANGELOG.md +++ b/packages/devtools_app_shared/CHANGELOG.md @@ -4,6 +4,11 @@ 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. --> ## 0.5.2-wip +* 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 d1a20ca45683144f2ac15f23280246b9686f29b5 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 --- .../release_notes/NEXT_RELEASE_NOTES.md | 2 +- pubspec.lock | 20 ++++++------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index e3cc8f9b314..81bbfda5d45 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -17,7 +17,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) ## Inspector updates diff --git a/pubspec.lock b/pubspec.lock index b1b1b8f86a0..18b8251cc4e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -523,14 +523,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" - listen: - dependency: transitive - description: - name: listen - sha256: "47501a08016a43fcad79252439d723f50f14f88fa7bfd8a177e0a417e5c9e1f2" - url: "https://pub.dev" - source: hosted - version: "1.0.1" logging: dependency: transitive description: @@ -899,26 +891,26 @@ packages: dependency: transitive description: name: test - sha256: de5d145b0afff7921e5e788a880f52d7e5f3ae24068a202f6fd3b58e4ba26323 + sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f url: "https://pub.dev" source: hosted - version: "1.32.0" + version: "1.31.1" test_api: dependency: transitive description: name: test_api - sha256: "0a10344e901e5b2e63819567951cb6a06673ed6b84f40462188ff5a0c41f371f" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.14" + version: "0.7.12" test_core: dependency: transitive description: name: test_core - sha256: "80f3fb49087454e07e7e07c67578cfdd156c8c3a5227d8b3f47c7b2d019c2e93" + sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 url: "https://pub.dev" source: hosted - version: "0.6.20" + version: "0.6.18" typed_data: dependency: transitive description: From 852c9c04f04114bac5df8d52f89dc98d230d2057 Mon Sep 17 00:00:00 2001 From: hkarmoush Date: Sat, 17 Oct 2026 23:34:18 +0300 Subject: [PATCH 3/4] Revert unrelated pubspec.lock changes --- pubspec.lock | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 18b8251cc4e..b1b1b8f86a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -523,6 +523,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + listen: + dependency: transitive + description: + name: listen + sha256: "47501a08016a43fcad79252439d723f50f14f88fa7bfd8a177e0a417e5c9e1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.1" logging: dependency: transitive description: @@ -891,26 +899,26 @@ packages: dependency: transitive description: name: test - sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f + sha256: de5d145b0afff7921e5e788a880f52d7e5f3ae24068a202f6fd3b58e4ba26323 url: "https://pub.dev" source: hosted - version: "1.31.1" + version: "1.32.0" test_api: dependency: transitive description: name: test_api - sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + sha256: "0a10344e901e5b2e63819567951cb6a06673ed6b84f40462188ff5a0c41f371f" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.14" test_core: dependency: transitive description: name: test_core - sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 + sha256: "80f3fb49087454e07e7e07c67578cfdd156c8c3a5227d8b3f47c7b2d019c2e93" url: "https://pub.dev" source: hosted - version: "0.6.18" + version: "0.6.20" typed_data: dependency: transitive description: From 90c62a720c936e3bcf643d3d702cfd291e30a51f Mon Sep 17 00:00:00 2001 From: hkarmoush Date: Tue, 20 Oct 2026 00:21:35 +0300 Subject: [PATCH 4/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(