Unverified Commit ffcf4191 authored by Mouad Debbar's avatar Mouad Debbar Committed by GitHub

[web] Support custom url strategies (#59797)

parent 4f2fcca6
......@@ -43,6 +43,7 @@ dependencies:
http: 0.12.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
intl: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
json_rpc_2: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
matcher: 0.12.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......@@ -105,7 +106,6 @@ dev_dependencies:
html: 0.14.0+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
mime: 0.9.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
node_interop: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......
......@@ -28,6 +28,7 @@ dependencies:
connectivity_macos: 0.1.0+5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
connectivity_platform_interface: 1.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
device_info_platform_interface: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
path: 1.8.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
plugin_platform_interface: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......@@ -73,7 +74,6 @@ dev_dependencies:
http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
json_rpc_2: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
matcher: 0.12.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......
......@@ -17,5 +17,6 @@
/// describing how the `url_launcher` package was created using [flutter_web_plugins].
library flutter_web_plugins;
export 'src/navigation/url_strategy.dart';
export 'src/plugin_event_channel.dart';
export 'src/plugin_registry.dart';
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
@JS()
library js_location_strategy;
import 'dart:async';
import 'dart:html' as html;
import 'dart:ui' as ui;
import 'package:js/js.dart';
import 'package:meta/meta.dart';
import 'url_strategy.dart';
typedef _JsSetUrlStrategy = void Function(JsUrlStrategy);
/// A JavaScript hook to customize the URL strategy of a Flutter app.
//
// Keep this in sync with the JS name in the web engine. Find it at:
// https://github.com/flutter/engine/blob/custom_location_strategy/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart
//
// TODO(mdebbar): Add integration test https://github.com/flutter/flutter/issues/66852
@JS('_flutter_web_set_location_strategy')
external _JsSetUrlStrategy get jsSetUrlStrategy;
typedef _PathGetter = String Function();
typedef _StateGetter = Object Function();
typedef _AddPopStateListener = ui.VoidCallback Function(html.EventListener);
typedef _StringToString = String Function(String);
typedef _StateOperation = void Function(
Object state, String title, String url);
typedef _HistoryMove = Future<void> Function(int count);
/// Given a Dart implementation of URL strategy, converts it to a JavaScript
/// URL strategy to be passed through JS interop.
JsUrlStrategy convertToJsUrlStrategy(UrlStrategy strategy) {
if (strategy == null) {
return null;
}
return JsUrlStrategy(
getPath: allowInterop(strategy.getPath),
getState: allowInterop(strategy.getState),
addPopStateListener: allowInterop(strategy.addPopStateListener),
prepareExternalUrl: allowInterop(strategy.prepareExternalUrl),
pushState: allowInterop(strategy.pushState),
replaceState: allowInterop(strategy.replaceState),
go: allowInterop(strategy.go),
);
}
/// The JavaScript representation of a URL strategy.
///
/// This is used to pass URL strategy implementations across a JS-interop
/// bridge from the app to the engine.
@JS()
@anonymous
abstract class JsUrlStrategy {
/// Creates an instance of [JsUrlStrategy] from a bag of URL strategy
/// functions.
external factory JsUrlStrategy({
@required _PathGetter getPath,
@required _StateGetter getState,
@required _AddPopStateListener addPopStateListener,
@required _StringToString prepareExternalUrl,
@required _StateOperation pushState,
@required _StateOperation replaceState,
@required _HistoryMove go,
});
/// Adds a listener to the `popstate` event and returns a function that
/// removes the listener.
external ui.VoidCallback addPopStateListener(html.EventListener fn);
/// Returns the active path in the browser.
external String getPath();
/// Returns the history state in the browser.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state
external Object getState();
/// Given a path that's internal to the app, create the external url that
/// will be used in the browser.
external String prepareExternalUrl(String internalUrl);
/// Push a new history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
external void pushState(Object state, String title, String url);
/// Replace the currently active history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
external void replaceState(Object state, String title, String url);
/// Moves forwards or backwards through the history stack.
///
/// A negative [count] value causes a backward move in the history stack. And
/// a positive [count] value causs a forward move.
///
/// Examples:
///
/// * `go(-2)` moves back 2 steps in history.
/// * `go(3)` moves forward 3 steps in hisotry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go
external Future<void> go(int count);
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:html';
AnchorElement _urlParsingNode;
/// Extracts the pathname part of a full [url].
///
/// Example: for the url `http://example.com/foo`, the extracted pathname will
/// be `/foo`.
String extractPathname(String url) {
// TODO(mdebbar): Use the `URI` class instead?
_urlParsingNode ??= AnchorElement();
_urlParsingNode.href = url;
final String pathname = _urlParsingNode.pathname;
return (pathname.isEmpty || pathname[0] == '/') ? pathname : '/$pathname';
}
Element _baseElement;
/// Finds the <base> element in the document and returns its `href` attribute.
///
/// Returns null if the element isn't found.
String getBaseElementHrefFromDom() {
if (_baseElement == null) {
_baseElement = document.querySelector('base');
if (_baseElement == null) {
return null;
}
}
return _baseElement.getAttribute('href');
}
/// Checks that [baseHref] is set.
///
/// Throws an exception otherwise.
String checkBaseHref(String baseHref) {
if (baseHref == null) {
throw Exception('Please add a <base> element to your index.html');
}
if (!baseHref.endsWith('/')) {
throw Exception('The base href has to end with a "/" to work correctly');
}
return baseHref;
}
/// Prepends a forward slash to [path] if it doesn't start with one already.
///
/// Returns [path] unchanged if it already starts with a forward slash.
String ensureLeadingSlash(String path) {
if (!path.startsWith('/')) {
return '/$path';
}
return path;
}
/// Removes the trailing forward slash from [path] if any.
String stripTrailingSlash(String path) {
if (path.endsWith('/')) {
return path.substring(0, path.length - 1);
}
return path;
}
......@@ -10,6 +10,8 @@ dependencies:
flutter:
sdk: flutter
js: 0.6.3-nullsafety.1
characters: 1.1.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
collection: 1.15.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......@@ -34,4 +36,4 @@ dev_dependencies:
term_glyph: 1.2.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_api: 0.2.19-nullsafety.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
# PUBSPEC CHECKSUM: 417a
# PUBSPEC CHECKSUM: 2180
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:html';
@TestOn('chrome') // Uses web-only Flutter SDK
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
void main() {
group('$HashUrlStrategy', () {
TestPlatformLocation location;
setUp(() {
location = TestPlatformLocation();
});
tearDown(() {
location = null;
});
test('leading slash is optional', () {
final HashUrlStrategy strategy = HashUrlStrategy(location);
location.hash = '#/';
expect(strategy.getPath(), '/');
location.hash = '#/foo';
expect(strategy.getPath(), '/foo');
location.hash = '#foo';
expect(strategy.getPath(), 'foo');
});
test('path should not be empty', () {
final HashUrlStrategy strategy = HashUrlStrategy(location);
location.hash = '';
expect(strategy.getPath(), '/');
location.hash = '#';
expect(strategy.getPath(), '/');
});
});
group('$PathUrlStrategy', () {
TestPlatformLocation location;
setUp(() {
location = TestPlatformLocation();
});
tearDown(() {
location = null;
});
test('validates base href', () {
location.baseHref = '/';
expect(
() => PathUrlStrategy(location),
returnsNormally,
);
location.baseHref = '/foo/';
expect(
() => PathUrlStrategy(location),
returnsNormally,
);
location.baseHref = '';
expect(
() => PathUrlStrategy(location),
throwsException,
);
location.baseHref = 'foo';
expect(
() => PathUrlStrategy(location),
throwsException,
);
location.baseHref = '/foo';
expect(
() => PathUrlStrategy(location),
throwsException,
);
});
test('leading slash is always prepended', () {
location.baseHref = '/';
final PathUrlStrategy strategy = PathUrlStrategy(location);
location.pathname = '';
expect(strategy.getPath(), '/');
location.pathname = 'foo';
expect(strategy.getPath(), '/foo');
});
test('gets path correctly in the presence of basePath', () {
location.baseHref = 'https://example.com/foo/';
final PathUrlStrategy strategy = PathUrlStrategy(location);
location.pathname = '/foo/';
expect(strategy.getPath(), '/');
location.pathname = '/foo';
expect(strategy.getPath(), '/');
location.pathname = '/foo/bar';
expect(strategy.getPath(), '/bar');
});
test('gets path correctly in the presence of query params', () {
location.baseHref = 'https://example.com/foo/';
location.pathname = '/foo/bar';
final PathUrlStrategy strategy = PathUrlStrategy(location);
location.search = '?q=1';
expect(strategy.getPath(), '/bar?q=1');
location.search = '?q=1&t=r';
expect(strategy.getPath(), '/bar?q=1&t=r');
});
test('generates external path correctly in the presence of basePath', () {
location.baseHref = 'https://example.com/foo/';
final PathUrlStrategy strategy = PathUrlStrategy(location);
expect(strategy.prepareExternalUrl(''), '/foo');
expect(strategy.prepareExternalUrl('/'), '/foo/');
expect(strategy.prepareExternalUrl('bar'), '/foo/bar');
expect(strategy.prepareExternalUrl('/bar'), '/foo/bar');
expect(strategy.prepareExternalUrl('/bar/'), '/foo/bar/');
});
});
}
/// A mock implementation of [PlatformLocation] that doesn't access the browser.
class TestPlatformLocation extends PlatformLocation {
@override
String pathname = '';
@override
String search = '';
@override
String hash = '';
@override
dynamic state;
/// Mocks the base href of the document.
String baseHref = '';
@override
void addPopStateListener(EventListener fn) {
throw UnimplementedError();
}
@override
void removePopStateListener(EventListener fn) {
throw UnimplementedError();
}
@override
void pushState(dynamic state, String title, String url) {
throw UnimplementedError();
}
@override
void replaceState(dynamic state, String title, String url) {
throw UnimplementedError();
}
@override
void go(int count) {
throw UnimplementedError();
}
@override
String getBaseHref() => baseHref;
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
@TestOn('browser') // Uses web-only Flutter SDK
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_web_plugins/src/navigation/utils.dart';
void main() {
test('checks base href', () {
expect(() => checkBaseHref(null), throwsException);
expect(() => checkBaseHref('foo'), throwsException);
expect(() => checkBaseHref('/foo'), throwsException);
expect(() => checkBaseHref('foo/bar'), throwsException);
expect(() => checkBaseHref('/foo/bar'), throwsException);
expect(() => checkBaseHref('/'), returnsNormally);
expect(() => checkBaseHref('/foo/'), returnsNormally);
expect(() => checkBaseHref('/foo/bar/'), returnsNormally);
});
test('extracts pathname from URL', () {
expect(extractPathname('/'), '/');
expect(extractPathname('/foo'), '/foo');
expect(extractPathname('/foo/'), '/foo/');
expect(extractPathname('/foo/bar'), '/foo/bar');
expect(extractPathname('/foo/bar/'), '/foo/bar/');
expect(extractPathname('https://example.com'), '/');
expect(extractPathname('https://example.com/'), '/');
expect(extractPathname('https://example.com/foo'), '/foo');
expect(extractPathname('https://example.com/foo#bar'), '/foo');
expect(extractPathname('https://example.com/foo/#bar'), '/foo/');
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment