[DWDS] Can't set breakpoint in file with circular dependency
See https://github.com/flutter/flutter/issues/106727 for details.
How to reproduce:
- Download the minimal example
- Rename
lib/main_dev.darttomain.dart - Follow instructions to debug DWDS with
flutter_toolsusing the example app - Try to set a breakpoint on line 21 of
account_presentation/controllers/sign_in_controller.dart
Note:
- When the import that introduces a circular dependency (
package:router/router.dart) is commented out, themoduleForSourcemethod correctly returnspackages/account_presentation/controllers/sign_in_controller.dart. - With the import is included, it incorrectly returns
packages/home/pages/welcome_page.dart.
Looked at this a bit more. With the circular dependency, account_presentation/controllers/sign_in_controller.dart gets listed as a library for the packages/home/pages/welcome_page.dart module.
ModuleMetadata for packages/home/pages/welcome_page.dart:
With circular dependency
{
"version":1.0.2,
"name":"packages/home/pages/welcome_page.dart",
"closureName":"load__packages__home__pages__welcome_page_dart",
"sourceMapUri":"/packages/home/pages/welcome_page.dart.lib.js.map",
"moduleUri":"/packages/home/pages/welcome_page.dart.lib.js",
"libraries":[
{
"name":"welcome_page",
"importUri":"package":"home/pages/welcome_page.dart",
"partUris":[
]
},
{
"name":"unknown_page",
"importUri":"package":"home/pages/unknown_page.dart",
"partUris":[
]
},
{
"name":"splash_page",
"importUri":"package":"home/pages/splash_page.dart",
"partUris":[
]
},
{
"name":"main_layout_page",
"importUri":"package":"home/layouts/main_layout_page.dart",
"partUris":[
]
},
{
"name":"unknown_controller",
"importUri":"package":"home/controllers/unknown_controller.dart",
"partUris":[
]
},
{
"name":"splash_controller",
"importUri":"package":"home/controllers/splash_controller.dart",
"partUris":[
]
},
{
"name":"sign_in_page",
"importUri":"package":"account_presentation/pages/sign_in_page.dart",
"partUris":[
]
},
{
"name":"sign_in_controller",
"importUri":"package":"account_presentation/controllers/sign_in_controller.dart",
"partUris":[
]
},
{
"name":"sign_in_binding",
"importUri":"package":"account_presentation/bindings/sign_in_binding.dart",
"partUris":[
]
},
{
"name":"account_presentation_library",
"importUri":"package":"account_presentation/account_presentation_library.dart",
"partUris":[
]
},
{
"name":"router",
"importUri":"package":"router/router.dart",
"partUris":[
]
},
{
"name":"router_library",
"importUri":"package":"router/router_library.dart",
"partUris":[
]
},
{
"name":"main_layout_controller",
"importUri":"package":"home/controllers/main_layout_controller.dart",
"partUris":[
]
},
{
"name":"welcome_binding",
"importUri":"package":"home/bindings/welcome_binding.dart",
"partUris":[
]
},
{
"name":"unknown_binding",
"importUri":"package":"home/bindings/unknown_binding.dart",
"partUris":[
]
},
{
"name":"splash_binding",
"importUri":"package":"home/bindings/splash_binding.dart",
"partUris":[
]
},
{
"name":"home_library",
"importUri":"package":"home/home_library.dart",
"partUris":[
]
}
],
"soundNullSafety":true
}
Without circular dependency
{
"version":1.0.2,
"name":"packages/home/pages/welcome_page.dart",
"closureName":"load__packages__home__pages__welcome_page_dart",
"sourceMapUri":"/packages/home/pages/welcome_page.dart.lib.js.map",
"moduleUri":"/packages/home/pages/welcome_page.dart.lib.js",
"libraries":[
{
"name":"welcome_page",
"importUri":"package":"home/pages/welcome_page.dart",
"partUris":[
]
},
{
"name":"unknown_page",
"importUri":"package":"home/pages/unknown_page.dart",
"partUris":[
]
},
{
"name":"splash_page",
"importUri":"package":"home/pages/splash_page.dart",
"partUris":[
]
},
{
"name":"main_layout_page",
"importUri":"package":"home/layouts/main_layout_page.dart",
"partUris":[
]
},
{
"name":"unknown_controller",
"importUri":"package":"home/controllers/unknown_controller.dart",
"partUris":[
]
},
{
"name":"splash_controller",
"importUri":"package":"home/controllers/splash_controller.dart",
"partUris":[
]
},
{
"name":"router",
"importUri":"package":"router/router.dart",
"partUris":[
]
},
{
"name":"router_library",
"importUri":"package":"router/router_library.dart",
"partUris":[
]
},
{
"name":"main_layout_controller",
"importUri":"package":"home/controllers/main_layout_controller.dart",
"partUris":[
]
},
{
"name":"welcome_binding",
"importUri":"package":"home/bindings/welcome_binding.dart",
"partUris":[
]
},
{
"name":"unknown_binding",
"importUri":"package":"home/bindings/unknown_binding.dart",
"partUris":[
]
},
{
"name":"splash_binding",
"importUri":"package":"home/bindings/splash_binding.dart",
"partUris":[
]
},
{
"name":"home_library",
"importUri":"package":"home/home_library.dart",
"partUris":[
]
}
],
"soundNullSafety":true
}
Adding @annagrin who has more context. How should ModuleMetadata / breakpoints handle circular dependencies?
Circular dependencies result in JS modules that include all JS code for the dart included, so the module change is expected. Not sure what causes the issue, will look into it.
Hi @annagrin, do you have any news about this? Thanks in advance!
Investigated this - the problem is caused by disagreements between serving paths in dwds and relative paths in source maps:
Example
source directory tree
<current directory>
a/
lib/
a.dart
pubspec.yaml
b/
lib/
b.dart
pubspec.yaml
where
package:a/a.dart imports package:b/b.dartpackage:b/b.dart imports package:a/a.dart
DDC output
-
Module:
packages/a/a.lib.jsthat contains code for both packages -
Source map:
packages/a/a.lib.js.mapthat contains paths relative to directorya/lib(where .js file is physically located):a.dart../../b/lib/b.dart(meaningless relative to the serving pathpackages/a/a.lib.js- it should be../b/b.dartto resolve correctly).
We can solve this by making relative paths in modules uris and relative paths in source maps agree, ie making both match either the actual directory structure or the structure of module uris (which are created from package: urls I believe)
This needs some experimentation and thinking, so not a trivial bug fix unfortunately. I suspect the fix should be in the frontend server since it writes out module metadata (that contains module uris) and source maps (that contain relative source paths), or flutter tools (that reads everything from the frontend server and starts dwds to serve the files).
@jkydevelopment To work around the issue, is it possible to break the circular dependency by either putting all the mutual dependent code in one package, or pulling the code both need to a third package and making the two depend on it instead?
@annagrin thank you for your response. I have just tried it by moving some common packages to a new common package, which now contains core, locator and router.
In this case it's not possible to fix the circular dependency because of the dependency injection:
locatorfolder (now contained inside the newcommonpackage) usesget_it, so it needs to import some other packages to register eachfactoryorsingleton(ieaccount_domain).- In the other hand any other package which uses
locator(ieaccount_domain) also needs to inject some elements which are retrieved fromlocator, which is insidecommonpackage, so the circular dependency it's always present.
The only way I could fix this issue is by only using the main lib folder and not creating any other package, but all the libraries would be contained in the main pubspec.yaml, and it would not be possible to apply a clean architecture to the project.
Any ideas about how could I structure this to avoid the circular dependency?
I am working on the solution but not sure when it is going to be available (really depends where the change needs to be made, I am experimenting with a few ideas).
As for the package dependencies, looks like the account_domain is special because it creates a circular dependency. Can the part of account_domain (the part that uses locator and common) be moved to the common package so common does not need to depend on account_domain anymore? Another option is to unify common and account_domain into one package, if this makes more sense for the project.
Proposed fix
make server paths follow directory structure:packages/<package_directory>/<path from package root to the file on disk>
Fix plan
- Frontend server and expression compiler worker (DDC):
- update the module uris in metadata files to match the scheme above, under a new
debugger-module-urisflag
- update the module uris in metadata files to match the scheme above, under a new
- Dwds:
- provide a way to map
package:uris to server paths, ie a new classPackageUriMapper - pass the
debugger-module-uristo the expression compiler worker
- provide a way to map
- flutter tools:
- use
PackageUriMapperdwds API to resolvepackage:uris inWebAssetServer - pass the
debugger-module-uristo the frontend server
- use
- cleanup:
- set
debugger-module-uristo true by default - after all the changes propagated to dart and flutter stable versions
- remove passing of the flag by tools
- remove the flag
- set
We should verify whether or not this is still an issue.
@annagrin @bkonyi We can confirm this is still an issue on flutter 3.32. With the release of pub workspaces, we decided to transition our growing codebase to a modular project structure, with feature packages just as @jkydevelopment described. First, we couldn't debug at all cause of https://github.com/dart-lang/webdev/issues/2575. Today, still a lot of our features cannot be debugged on web. I finally found this old issue now, which describes exactly what we experience and also explains why some breakpoints can be set and others not. This is the error:
addBreakpointWithScriptUri: (102) The VM is unable to add a breakpoint bp/3302#40:0 at the specified line or function: (3302:40:0): cannot find Dart location.
@srujzs @elliette Is there anything we could do to support resolving this issue?
Hi, we are refactoring some code in our repository and we introduced circular dependencies between packages to speed up the work. But by doing so we hit this issue in all of our Flutter web apps, so yes this is still an issue.
Update: The changes that were landed in 2022 but never enabled do fix this issue by updating the directory layout of the files at serve time to match what is encoded in the source maps. Unfortunately, those changes also introduce new problems related to source maps and stack traces.
There are a lot of places throughout the Dart web dev infrastructure that assume a very specific server directory layout. Changing that layout will require staging more changes and plumbing more flags to roll it out. There are some longstanding goals to holistically redesign how we produce source maps and stack traces but that is a much larger task. My goal is to find a solution that targets just this problem that will be faster to roll out.
I've been exploring some potential fixes to resolve this issue and I have some ideas to work with. I'll be away from work for a few weeks but I'm planning on taking on this task when I return.
I landed a fix for this issue in the Dart SDK https://github.com/dart-lang/sdk/commit/1c0c66f680f53d0a2b7d1c933fe244c0978beeaa. It has been rolled into the Flutter SDK and is now available on the main channel.
~~The fix has also been published in the latest Flutter beta release: 3.38.0-0.2.pre if you want to try it there.~~
I'm wrong about that. I'll look into getting the fix in.