Skip to content

Commit 7dd8b98

Browse files
authored
Switch to using Path URL Strategy (#3585)
* Switch to using Path URL Strategy * Fix import * Update extraction of page from the URL for survey * More extractCurrentPageFromUrl + tests into url_utils * Ensure page URL includes the current tab when there was no existing page set * Allow navigating from /?page=foo to /bar correctly * Restore accidentally deleted line * Fix navigating during initState to fix URL * Change Future.microtask to WidgetsBinding.instance.addPostFrameCallback * Fix analysis errors after rebase on latest * Add a comment about base href being replaced * Handle legacy URLs by redirecting them to new style URLs * Fix require_trailing_comma lints * Move url_strategy to config_specific * Remove redundant code/tests
1 parent 509dc37 commit 7dd8b98

File tree

12 files changed

+204
-46
lines changed

12 files changed

+204
-46
lines changed

packages/devtools_app/lib/main.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,24 @@ import 'src/analytics/analytics_controller.dart';
99
import 'src/app.dart';
1010
import 'src/config_specific/framework_initialize/framework_initialize.dart';
1111
import 'src/config_specific/ide_theme/ide_theme.dart';
12+
import 'src/config_specific/url/url.dart';
13+
import 'src/config_specific/url_strategy/url_strategy.dart';
1214
import 'src/extension_points/extensions_base.dart';
1315
import 'src/extension_points/extensions_external.dart';
16+
import 'src/primitives/url_utils.dart';
1417
import 'src/screens/debugger/syntax_highlighter.dart';
1518
import 'src/screens/provider/riverpod_error_logger_observer.dart';
1619
import 'src/shared/app_error_handling.dart';
1720
import 'src/shared/globals.dart';
1821
import 'src/shared/preferences.dart';
1922

2023
void main() async {
24+
// Before switching to URL path strategy, check if this URL is in the legacy
25+
// fragment format and redirect if necessary.
26+
if (_handleLegacyUrl()) return;
27+
28+
usePathUrlStrategy();
29+
2130
// Initialize the framework before we do anything else, otherwise the
2231
// StorageController won't be initialized and preferences won't be loaded.
2332
await initializeFramework();
@@ -44,3 +53,21 @@ void main() async {
4453
);
4554
});
4655
}
56+
57+
/// Checks if the request is for a legacy URL and if so, redirects to the new
58+
/// equivalent.
59+
///
60+
/// Returns `true` if a redirect was performed, in which case normal app
61+
/// initialization should be skipped.
62+
bool _handleLegacyUrl() {
63+
final url = getWebUrl();
64+
if (url == null) return false;
65+
66+
final newUrl = mapLegacyUrl(url);
67+
if (newUrl != null) {
68+
webRedirect(newUrl);
69+
return true;
70+
}
71+
72+
return false;
73+
}

packages/devtools_app/lib/src/analytics/_analytics_web.dart

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import '../app.dart';
1717
import '../config_specific/logger/logger.dart';
1818
import '../config_specific/server/server.dart' as server;
1919
import '../config_specific/url/url.dart';
20+
import '../primitives/url_utils.dart';
2021
import '../screens/performance/performance_screen.dart';
2122
import '../screens/profiler/profiler_screen.dart';
2223
import '../shared/globals.dart';
@@ -732,29 +733,8 @@ Map<String, dynamic> generateSurveyQueryParameters() {
732733
const fromKey = 'From';
733734
const internalKey = 'Internal';
734735

735-
// TODO(https://github.com/flutter/devtools/issues/2475): fix url structure
736-
// Parsing the url via Uri.parse returns an incorrect value for fragment.
737-
// Grab the fragment value manually. The url will be of the form
738-
// http://127.0.0.1:9100/#/timeline?ide=IntelliJ-IDEA&uri=..., and we want the
739-
// part equal to '/timeline'.
740-
final url = window.location.toString();
741-
const fromValuePrefix = '#/';
742-
final startIndex = url.indexOf(fromValuePrefix);
743-
// Use the last index because the url can be of the form
744-
// 'http://127.0.0.1:9103/?#/?' and we want to be referencing the last '?'
745-
// character.
746-
final endIndex = url.lastIndexOf('?');
747-
var fromPage = '';
748-
try {
749-
fromPage = url.substring(
750-
startIndex + fromValuePrefix.length,
751-
endIndex,
752-
);
753-
} catch (_) {
754-
// Fail gracefully if finding the [fromPage] value throws an exception.
755-
}
756-
757736
final internalValue = (!isExternalBuild).toString();
737+
final fromPage = extractCurrentPageFromUrl(window.location.toString());
758738

759739
return {
760740
ideKey: ideLaunched,

packages/devtools_app/lib/src/config_specific/url/_url_stub.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,13 @@
33
// in the LICENSE file.
44

55
Map<String, String> loadQueryParams() => {};
6+
7+
/// Gets the URL from the browser.
8+
///
9+
/// Returns null for non-web platforms.
10+
String? getWebUrl() => null;
11+
12+
/// Performs a web redirect using window.location.replace().
13+
///
14+
/// No-op for non-web platforms.
15+
void webRedirect(String url) {}

packages/devtools_app/lib/src/config_specific/url/_url_web.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@ import '../../primitives/utils.dart';
99
Map<String, String> loadQueryParams() {
1010
return devToolsQueryParams(window.location.toString());
1111
}
12+
13+
String? getWebUrl() => window.location.toString();
14+
15+
void webRedirect(String url) {
16+
window.location.replace(url);
17+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
void usePathUrlStrategy() {
2+
// No-op. URL Strategies are only supported for web.
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export 'noop.dart' if (dart.library.html) 'web.dart';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
2+
3+
void usePathUrlStrategy() {
4+
setUrlStrategy(PathUrlStrategy());
5+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/// Extracts the current DevTools page from the given [url].
6+
String extractCurrentPageFromUrl(String url) {
7+
// The url can be in one of two forms:
8+
// - /page?uri=xxx
9+
// - /?page=xxx&uri=yyy (original formats IDEs may use)
10+
// Use the path in preference to &page= as it's the one DevTools is updating
11+
final uri = Uri.parse(url);
12+
return uri.path == '/'
13+
? uri.queryParameters['page'] ?? ''
14+
: uri.path.substring(1);
15+
}
16+
17+
/// Maps DevTools URLs in the original fragment format onto the equivalent URLs
18+
/// in the new URL format.
19+
///
20+
/// Returns `null` if [url] is not a legacy URL.
21+
String? mapLegacyUrl(String url) {
22+
final uri = Uri.parse(url);
23+
// Old formats include:
24+
// http://localhost:123/#/inspector?uri=ws://...
25+
// http://localhost:123/#/?page=inspector&uri=ws://...
26+
final isRootRequest = uri.path == '/' || uri.path.endsWith('/devtools/');
27+
if (isRootRequest && uri.fragment.isNotEmpty) {
28+
final basePath = uri.path;
29+
// Convert the URL by removing the fragment separator.
30+
final newUrl = url
31+
// Handle localhost:123/#/inspector?uri=xxx
32+
.replaceFirst('/#/', '/')
33+
// Handle localhost:123/#?page=inspector&uri=xxx
34+
.replaceFirst('/#', '');
35+
36+
// Move page names from the querystring into the path.
37+
var newUri = Uri.parse(newUrl);
38+
final page = newUri.queryParameters['page'];
39+
if (newUri.path == basePath && page != null) {
40+
final newParams = {...newUri.queryParameters}..remove('page');
41+
newUri = newUri.replace(
42+
path: '$basePath$page',
43+
queryParameters: newParams,
44+
);
45+
}
46+
return newUri.toString();
47+
}
48+
49+
return null;
50+
}

packages/devtools_app/lib/src/shared/routing.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class DevToolsRouterDelegate extends RouterDelegate<DevToolsRouteConfiguration>
139139
/// If page and args would be the same, does nothing.
140140
/// Existing arguments (for example &uri=) will be preserved unless
141141
/// overwritten by [argUpdates].
142-
void navigateIfNotCurrent(String page, [Map<String, String>? argUpdates]) {
142+
void navigateIfNotCurrent(String page, [Map<String, String?>? argUpdates]) {
143143
final pageChanged = page != currentConfiguration!.page;
144144
final argsChanged = _changesArgs(argUpdates);
145145
if (!pageChanged && !argsChanged) {
@@ -211,7 +211,7 @@ class DevToolsRouterDelegate extends RouterDelegate<DevToolsRouteConfiguration>
211211

212212
/// Checks whether applying [changes] over the current routes args will result
213213
/// in any changes.
214-
bool _changesArgs(Map<String, String>? changes) => !mapEquals(
214+
bool _changesArgs(Map<String, String?>? changes) => !mapEquals(
215215
{...currentConfiguration!.args, ...?changes},
216216
{...currentConfiguration!.args},
217217
);

packages/devtools_app/lib/src/shared/scaffold.dart

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,21 +215,27 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
215215
// Clear error count when navigating to a screen.
216216
serviceManager.errorBadgeManager.clearErrors(screen.screenId);
217217

218-
// If the tab index is 0 and the current route has no page ID (eg. we're
219-
// at the URL /?uri= with no page ID), those are equivalent pages but
220-
// navigateIfNotCurrent does not know that and will try to navigate, so
221-
// skip that here.
222-
final routerDelegate = DevToolsRouterDelegate.of(context);
223-
if (_tabController!.index == 0 &&
224-
(routerDelegate.currentConfiguration!.page.isEmpty)) {
225-
return;
226-
}
227-
228218
// Update routing with the change.
219+
final routerDelegate = DevToolsRouterDelegate.of(context);
229220
routerDelegate.navigateIfNotCurrent(screen.screenId);
230221
}
231222
});
232223

224+
// If we had no explicit page, we want to write one into the URL but
225+
// without triggering a navigation. Since we can't nagivate during a build
226+
// we have to wrap this in `Future.microtask`.
227+
if (widget.page == null && _currentScreen is! SimpleScreen) {
228+
WidgetsBinding.instance.addPostFrameCallback((_) {
229+
final routerDelegate = DevToolsRouterDelegate.of(context);
230+
Router.neglect(context, () {
231+
routerDelegate.navigateIfNotCurrent(
232+
_currentScreen.screenId,
233+
routerDelegate.currentConfiguration?.args,
234+
);
235+
});
236+
});
237+
}
238+
233239
// Broadcast the initial page.
234240
frameworkController.notifyPageChange(
235241
PageChangeEvent(_currentScreen.screenId, widget.embed),
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:devtools_app/src/primitives/url_utils.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
8+
void main() {
9+
group('url utils', () {
10+
group('extractCurrentPageFromUrl', () {
11+
test('parses the current page from the path', () {
12+
final page =
13+
extractCurrentPageFromUrl('http://localhost:9000/inspector?uri=x');
14+
expect(page, 'inspector');
15+
});
16+
17+
test('parses the current page from the query string', () {
18+
final page = extractCurrentPageFromUrl(
19+
'http://localhost:9000/?uri=x&page=inspector&theme=dark',
20+
);
21+
expect(page, 'inspector');
22+
});
23+
24+
test(
25+
'parses the current page from the path even if query string is populated',
26+
() {
27+
final page = extractCurrentPageFromUrl(
28+
'http://localhost:9000/memory?uri=x&page=inspector&theme=dark',
29+
);
30+
expect(page, 'memory');
31+
});
32+
});
33+
34+
group('mapLegacyUrl', () {
35+
for (final prefix in [
36+
'http://localhost:123',
37+
'http://localhost:123/authToken=/devtools'
38+
]) {
39+
group(' with $prefix prefix', () {
40+
test('does not map new-style URLs', () {
41+
expect(mapLegacyUrl('$prefix'), isNull);
42+
expect(mapLegacyUrl('$prefix/'), isNull);
43+
expect(mapLegacyUrl('$prefix/foo?uri=ws://foo'), isNull);
44+
expect(mapLegacyUrl('$prefix?uri=ws://foo'), isNull);
45+
expect(mapLegacyUrl('$prefix/?uri=ws://foo'), isNull);
46+
expect(mapLegacyUrl('$prefix/?uri=ws://foo#'), isNull);
47+
});
48+
49+
test('maps legacy URIs with page names in path', () {
50+
expect(
51+
mapLegacyUrl('$prefix/#/inspector?foo=bar'),
52+
'$prefix/inspector?foo=bar',
53+
);
54+
});
55+
56+
test('maps legacy URIs with page names in querystring', () {
57+
expect(
58+
mapLegacyUrl('$prefix/#/?page=inspector&foo=bar'),
59+
'$prefix/inspector?foo=bar',
60+
);
61+
});
62+
63+
test('maps legacy URIs with no page names', () {
64+
expect(
65+
mapLegacyUrl('$prefix/#/?foo=bar'),
66+
'$prefix/?foo=bar',
67+
);
68+
});
69+
});
70+
}
71+
});
72+
});
73+
}
74+
75+
const dartSdkUrl =
76+
'org-dartlang-sdk:///third_party/dart/sdk/lib/async/zone.dart';
77+
const flutterUrl =
78+
'file:///path/to/flutter/packages/flutter/lib/src/widgets/binding.dart';
79+
const flutterUrlFromNonFlutterDir =
80+
'file:///path/to/non-flutter/packages/flutter/lib/src/widgets/binding.dart';

packages/devtools_app/web/index.html

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
<head>
1111
<meta charset="utf-8">
1212
<meta http-equiv="X-UA-Compatible" content="IE=edge">
13+
<!-- Note: This tag is replaced when served through DDS! -->
14+
<base href="/">
1315

1416
<title></title>
1517
<link href="favicon.png" rel="icon" sizes="64x64">
@@ -41,18 +43,6 @@
4143
if (!supportsES6Classes()) {
4244
window.location.href = '/unsupported-browser.html';
4345
}
44-
45-
// Handle URLs that pass all variables directly on a querystring without
46-
// the fragment (for ex. VS Code while it has some encoding bugs preventing
47-
// building the correct URLs using fragments
48-
// https://github.com/microsoft/vscode/issues/85930).
49-
if (window.location.search && window.location.search.length > 1) {
50-
// Ensure each component is encoded, because if the URI contains / slashes
51-
// Flutter will split on them and try to push multiple routes.
52-
const params = new URLSearchParams(unescape(window.location.search));
53-
params.forEach(function(v, k) { params.set(k, encodeURIComponent(v)) });
54-
window.location.replace(window.location.origin + '/#/?' + params.toString());
55-
}
5646
</script>
5747
</head>
5848

0 commit comments

Comments
 (0)