From 8d11d77102db1ab2493de6f4b7aa200f6f2c9f57 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 | 9 ++ 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, 185 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 7e9c5050340..0a64044c343 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -17,6 +17,15 @@ To learn more about DevTools, check out the * Remove the legacy Provider screen, which was a placeholder to point users to the `package:provider` DevTools extension. [#10046](https://github.com/flutter/devtools/pull/10046) +* 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) +* Fixed unreadable text in the release notes panel, where blockquotes were + drawn on a hard coded light blue background in the dark theme. + [#9957](https://github.com/flutter/devtools/pull/9957) +* Added support for proxying the VM service connection over a `MessagePort` + using `?uri=messageport:`. + [#9964](https://github.com/flutter/devtools/pull/9964) ## Inspector updates diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index 683bc2bdaf9..231da6a74db 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 a `RangeError` thrown by `SplitPane` when the number of children changes between rebuilds. * Fix garbage collection and timeout issues with the result list in `asyncEval` on both native VM and web. 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 7fe39d20cc447f2e2c2c82ec59ff55f8b4478c93 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 | 8 +------- pubspec.lock | 20 ++++++------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 0a64044c343..34c0610d550 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -19,13 +19,7 @@ To learn more about DevTools, check out the the `package:provider` DevTools extension. [#10046](https://github.com/flutter/devtools/pull/10046) * 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) -* Fixed unreadable text in the release notes panel, where blockquotes were - drawn on a hard coded light blue background in the dark theme. - [#9957](https://github.com/flutter/devtools/pull/9957) -* Added support for proxying the VM service connection over a `MessagePort` - using `?uri=messageport:`. - [#9964](https://github.com/flutter/devtools/pull/9964) + web applications. [#10045](https://github.com/flutter/devtools/pull/10045) ## Inspector updates diff --git a/pubspec.lock b/pubspec.lock index e12fee18dd7..31b595daaa5 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: @@ -891,26 +883,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 1904e3abed2aa04f9f7a027c03512c5ee64790e5 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 31b595daaa5..e12fee18dd7 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: @@ -883,26 +891,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 190f2c5e5faa7364cca27c112d4ed5a536b77c71 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(