blob: a46b061d153d3e77035803819af197f23776133b [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
@JS()
library hot_reload_client;
import 'dart:async';
import 'dart:convert';
import 'dart:html';
import 'package:build_daemon/data/build_status.dart';
import 'package:dwds/data/connect_request.dart';
import 'package:dwds/data/devtools_request.dart';
import 'package:dwds/data/isolate_events.dart';
import 'package:dwds/data/run_request.dart';
import 'package:dwds/data/serializers.dart';
import 'package:js/js.dart';
import 'package:js/js_util.dart';
import 'package:path/path.dart' as p;
import 'package:sse/client/sse_client.dart';
import 'package:uuid/uuid.dart';
import 'module.dart';
import 'promise.dart';
import 'reloading_manager.dart';
/// The last known digests of all the modules in the application.
///
/// This is updated in place during calls to [hotRestart].
Map<String, String> _lastKnownDigests;
// GENERATE:
// pub run build_runner build web
Future<void> main() {
return runZoned(() async {
// Set the unique id for this instance of the app.
// Test apps may already have this set.
dartAppInstanceId ??= Uuid().v1();
_lastKnownDigests = await _getDigests();
var manager = ReloadingManager(
_reloadModule,
_moduleLibraries,
_reloadPage,
(module) => dartLoader.moduleParentsGraph.get(module)?.cast(),
() => keys(dartLoader.moduleParentsGraph));
var client = SseClient(r'/$sseHandler');
hotRestartJs = allowInterop(() {
return toPromise(hotRestart(manager, client));
});
launchDevToolsJs = allowInterop(() {
if (!_isChrome) {
window.alert('Dart DevTools is only supported on Chrome');
return;
}
client.sink.add(jsonEncode(serializers.serialize(DevToolsRequest((b) => b
..appId = dartAppId
..instanceId = dartAppInstanceId))));
});
client.stream.listen((serialized) async {
var event = serializers.deserialize(jsonDecode(serialized));
if (event is DefaultBuildResult) {
if (reloadConfiguration == 'ReloadConfiguration.liveReload') {
window.location.reload();
} else if (reloadConfiguration == 'ReloadConfiguration.hotRestart') {
await hotRestart(manager, client);
} else if (reloadConfiguration == 'ReloadConfiguration.hotReload') {
print('Hot reload is currently unsupported. Ignoring change.');
}
} else if (event is DevToolsResponse) {
if (!event.success) {
window.alert('DevTools failed to open with: ${event.error}');
}
} else if (event is RunRequest) {
runMain();
}
});
window.onKeyDown.listen((Event e) {
if (e is KeyboardEvent &&
const [
'd',
'D',
'∂', // alt-d output on Mac
'Î', // shift-alt-D output on Mac
].contains(e.key) &&
e.altKey &&
!e.ctrlKey &&
!e.metaKey) {
e.preventDefault();
launchDevToolsJs();
}
});
if (_isChrome) {
// Wait for the connection to be estabilished before sending the AppId.
await client.onOpen.first;
client.sink.add(jsonEncode(serializers.serialize(ConnectRequest((b) => b
..appId = dartAppId
..instanceId = dartAppInstanceId))));
} else {
// If not chrome we just invoke main, devtools aren't supported.
runMain();
}
}, onError: (error, stackTrace) {
print('''
Unhandled error detected in the injected client.js script.
You can disable this script in webdev by passing --no-injected-client if it
is preventing your app from loading, but note that this will also prevent
all debugging and hot reload/restart functionality from working.
The original error is below, please file an issue at
https://github.com/dart-lang/webdev/issues/new and attach this output:
$error
$stackTrace
''');
});
}
/// Attemps to perform a hot restart, and returns whether it was successful or
/// not.
Future<bool> hotRestart(ReloadingManager manager, SseClient sseClient) async {
var developer = getProperty(require('dart_sdk'), 'developer');
if (callMethod(getProperty(developer, '_extensions'), 'containsKey',
['ext.flutter.disassemble']) as bool) {
await toFuture(callMethod(
developer, 'invokeExtension', ['ext.flutter.disassemble', '{}'])
as Promise<void>);
}
var newDigests = await _getDigests();
var modulesToLoad = <String>[];
for (var jsPath in newDigests.keys) {
if (!_lastKnownDigests.containsKey(jsPath) ||
_lastKnownDigests[jsPath] != newDigests[jsPath]) {
_lastKnownDigests[jsPath] = newDigests[jsPath];
var parts = p.url.split(jsPath);
// We serve top level dirs, so this strips the top level dir from all
// but `packages` paths.
var servePath =
parts.first == 'packages' ? jsPath : p.url.joinAll(parts.skip(1));
var jsUri = '${window.location.origin}/$servePath';
var moduleName = dartLoader.urlToModuleId.get(jsUri);
if (moduleName == null) {
print('Error during script reloading, refreshing the page. \n'
'Unable to find an existing module for script $jsUri.');
_reloadPage();
return false;
}
modulesToLoad.add(moduleName);
}
}
void rerunApp() {
// Notify webdev that the isolate is about to exit.
sseClient.sink.add(jsonEncode(serializers.serialize(IsolateExit((b) => b
..appId = dartAppId
..instanceId = dartAppInstanceId))));
callMethod(getProperty(require('dart_sdk'), 'dart'), 'hotRestart', []);
// Notify webdev that the isolate has been created.
sseClient.sink.add(jsonEncode(serializers.serialize(IsolateStart((b) => b
..appId = dartAppId
..instanceId = dartAppInstanceId))));
runMain();
}
if (modulesToLoad.isNotEmpty) {
manager.updateGraph();
var result = await manager.reload(modulesToLoad);
if (result == null) {
rerunApp();
result = true;
}
return result;
}
rerunApp();
return true;
}
@JS(r'$dartAppId')
external String get dartAppId;
@JS(r'$dartAppInstanceId')
external String get dartAppInstanceId;
@JS(r'$dartAppInstanceId')
external set dartAppInstanceId(String id);
@JS(r'$dartRunMain')
external void Function() get runMain;
@JS(r'$dartHotRestart')
external set hotRestartJs(Promise<bool> Function() cb);
@JS(r'$launchDevTools')
external set launchDevToolsJs(void Function() cb);
@JS(r'$launchDevTools')
external void Function() get launchDevToolsJs;
@JS(r'$dartLoader')
external DartLoader get dartLoader;
@JS(r'$dartReloadConfiguration')
external String get reloadConfiguration;
@JS(r'$loadModuleConfig')
external Object Function(String module) get require;
List<K> keys<K, V>(JsMap<K, V> map) {
return List.from(_jsArrayFrom(map.keys()));
}
Future<Map<String, String>> _getDigests() async {
var request = await HttpRequest.request(dartLoader.appDigests,
responseType: 'json', method: 'GET');
return (request.response as Map).cast<String, String>();
}
bool get _isChrome =>
window.navigator.userAgent.contains('Chrome') &&
// Edge has `Chrome` in its user agent string, but it also has `Edg` which
// chrome doesn't.
!window.navigator.userAgent.contains('Edg');
@JS('Array.from')
external List _jsArrayFrom(Object any);
@JS('Object.keys')
external List _jsObjectKeys(Object any);
@JS('Object.values')
external List _jsObjectValues(Object any);
Module _moduleLibraries(String moduleId) {
var moduleObj = dartLoader.getModuleLibraries(moduleId);
if (moduleObj == null) {
throw HotReloadFailedException("Failed to get module '$moduleId'. "
"This error might appear if such module doesn't exist or isn't already "
'loaded');
}
var moduleKeys = List<String>.from(_jsObjectKeys(moduleObj));
var moduleValues =
List<HotReloadableLibrary>.from(_jsObjectValues(moduleObj));
var moduleLibraries = moduleValues.map((x) => LibraryWrapper(x));
return Module(Map.fromIterables(moduleKeys, moduleLibraries));
}
Future<Module> _reloadModule(String moduleId) {
var completer = Completer<Module>();
var stackTrace = StackTrace.current;
dartLoader.forceLoadModule(moduleId, allowInterop(() {
completer.complete(_moduleLibraries(moduleId));
}),
allowInterop((e) => completer.completeError(
HotReloadFailedException(e.message), stackTrace)));
return completer.future;
}
void _reloadPage() {
window.location.reload();
}
@anonymous
@JS()
class DartLoader {
@JS()
external String get appDigests;
@JS()
external JsMap<String, List<String>> get moduleParentsGraph;
@JS()
external void forceLoadModule(String moduleId, void Function() callback,
void Function(JsError e) onError);
@JS()
external Object getModuleLibraries(String moduleId);
@JS()
external JsMap<String, String> get urlToModuleId;
}
@anonymous
@JS()
abstract class HotReloadableLibrary {
/// Implement this function to handle update of child modules.
///
/// May return nullable bool. To indicate that reload of child completes
/// successfully return true. To indicate that hot-reload is undoable for this
/// child return false - this will lead to full page reload. If null returned,
/// reloading will be propagated to current module itself.
///
/// The name of the child will be provided in [childId]. New version of child
/// module object will be provided in [child].
/// If any state was saved from previous version, it will be passed to [data].
///
/// This function will be called on old version of module current after child
/// reloading.
@JS()
external bool hot$onChildUpdate(String childId, HotReloadableLibrary child,
[Object data]);
/// Implement this function with any code to release resources before destroy.
///
/// Any object returned from this function will be passed to update hooks. Use
/// it to save any state you need to be preserved between hot reloadings.
/// Try do not use any custom types here, as it might prevent their code from
/// reloading. Better serialise to JSON or plain types.
///
/// This function will be called on old version of module before unloading.
@JS()
external Object hot$onDestroy();
/// Implement this function to handle update of the module itself.
///
/// May return nullable bool. To indicate that reload completes successfully
/// return true. To indicate that hot-reload is undoable return false - this
/// will lead to full page reload. If null returned, reloading will be
/// propagated to parent.
///
/// If any state was saved from previous version, it will be passed to [data].
///
/// This function will be called on new version of module after reloading.
@JS()
external bool hot$onSelfUpdate([Object data]);
}
@JS('Error')
abstract class JsError {
@JS()
external String get message;
@JS()
external String get stack;
}
@JS('Map')
abstract class JsMap<K, V> {
@JS()
external V get(K key);
@JS()
external Object keys();
}
class LibraryWrapper implements Library {
final HotReloadableLibrary _internal;
LibraryWrapper(this._internal);
@override
bool onChildUpdate(String childId, Library child, [Object data]) {
if (_internal != null && hasProperty(_internal, r'hot$onChildUpdate')) {
return _internal.hot$onChildUpdate(
childId, (child as LibraryWrapper)._internal, data);
}
// ignore: avoid_returning_null
return null;
}
@override
Object onDestroy() {
if (_internal != null && hasProperty(_internal, r'hot$onDestroy')) {
return _internal.hot$onDestroy();
}
return null;
}
@override
bool onSelfUpdate([Object data]) {
if (_internal != null && hasProperty(_internal, r'hot$onSelfUpdate')) {
return _internal.hot$onSelfUpdate(data);
}
// ignore: avoid_returning_null
return null;
}
}