| // 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 require_reloading_manager; |
| |
| import 'dart:async'; |
| import 'dart:collection'; |
| import 'dart:html'; |
| import 'dart:js_util'; |
| |
| import 'package:graphs/graphs.dart' as graphs; |
| import 'package:js/js.dart'; |
| import 'package:js/js_util.dart'; |
| |
| import '../promise.dart'; |
| import '../run_main.dart'; |
| import 'restarter.dart'; |
| |
| /// The last known digests of all the modules in the application. |
| /// |
| /// This is updated in place during calls to hotRestart. |
| /// TODO(annagrin): can this be a private field in RequireRestarter? |
| late Map<String, String> _lastKnownDigests; |
| |
| @JS(r'$requireLoader') |
| external RequireLoader get requireLoader; |
| |
| @JS(r'$loadModuleConfig') |
| external Object Function(String module) get require; |
| |
| @JS(r'$dartRunMain') |
| external set dartRunMain(Function() func); |
| |
| @JS(r'$dartRunMain') |
| external Function() get dartRunMain; |
| |
| List<K> keys<K, V>(JsMap<K, V> map) { |
| return List.from(_jsArrayFrom(map.keys())); |
| } |
| |
| @JS('Array.from') |
| external List _jsArrayFrom(Object any); |
| |
| @JS('Object.values') |
| external List _jsObjectValues(Object any); |
| |
| @anonymous |
| @JS() |
| class RequireLoader { |
| @JS() |
| external String get digestsPath; |
| |
| @JS() |
| external JsMap<String, List<String>> get moduleParentsGraph; |
| |
| @JS() |
| external void forceLoadModule(String moduleId, void Function() callback, |
| void Function(JsError e) onError); |
| } |
| |
| class HotReloadFailedException implements Exception { |
| final String _s; |
| |
| HotReloadFailedException(this._s); |
| @override |
| String toString() => "HotReloadFailedException: '$_s'"; |
| } |
| |
| @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(); |
| } |
| |
| /// Handles hot restart reloading for use with the require module system. |
| class RequireRestarter implements Restarter { |
| final _moduleOrdering = HashMap<String, int>(); |
| late SplayTreeSet<String> _dirtyModules; |
| var _running = Completer<bool>()..complete(true); |
| |
| var count = 0; |
| |
| RequireRestarter._() { |
| _dirtyModules = SplayTreeSet(_moduleTopologicalCompare); |
| } |
| |
| @override |
| Future<bool> restart({String? runId}) async { |
| final 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>); |
| } |
| |
| final newDigests = await _getDigests(); |
| final modulesToLoad = <String>[]; |
| for (var moduleId in newDigests.keys) { |
| if (!_lastKnownDigests.containsKey(moduleId)) { |
| print('Error during script reloading, refreshing the page. \n' |
| 'Unable to find an existing digest for module: $moduleId.'); |
| _reloadPage(); |
| } else if (_lastKnownDigests[moduleId] != newDigests[moduleId]) { |
| _lastKnownDigests[moduleId] = newDigests[moduleId]!; |
| modulesToLoad.add(moduleId); |
| } |
| } |
| |
| var result = true; |
| if (modulesToLoad.isNotEmpty) { |
| _updateGraph(); |
| result = await _reload(modulesToLoad); |
| } |
| callMethod(getProperty(require('dart_sdk'), 'dart'), 'hotRestart', []); |
| runMain(); |
| return result; |
| } |
| |
| List<String> _allModules() => keys(requireLoader.moduleParentsGraph); |
| |
| Future<Map<String, String>> _getDigests() async { |
| final request = await HttpRequest.request(requireLoader.digestsPath, |
| responseType: 'json', method: 'GET'); |
| return (request.response as Map).cast<String, String>(); |
| } |
| |
| Future<void> _initialize() async { |
| _lastKnownDigests = await _getDigests(); |
| } |
| |
| List<String> _moduleParents(String module) => |
| requireLoader.moduleParentsGraph.get(module)?.cast() ?? []; |
| |
| int _moduleTopologicalCompare(String module1, String module2) { |
| var topological = 0; |
| |
| final order1 = _moduleOrdering[module1]; |
| final order2 = _moduleOrdering[module2]; |
| |
| if (order1 == null || order2 == null) { |
| final missing = order1 == null ? module1 : module2; |
| throw HotReloadFailedException( |
| 'Unable to fetch ordering info for module: $missing'); |
| } |
| |
| topological = Comparable.compare( |
| _moduleOrdering[module2]!, _moduleOrdering[module1]!); |
| |
| if (topological == 0) { |
| // If modules are in cycle (same strongly connected component) compare |
| // their string id, to ensure total ordering for SplayTreeSet uniqueness. |
| topological = module1.compareTo(module2); |
| } |
| |
| return topological; |
| } |
| |
| /// Returns `true` if the reload was fully handled, `false` if it failed |
| /// explicitly, or `null` for an unhandled reload. |
| Future<bool> _reload(List<String> modules) async { |
| // As function is async, it can potentially be called second time while |
| // first invocation is still running. In this case just mark as dirty and |
| // wait until loop from the first call will do the work |
| if (!_running.isCompleted) return await _running.future; |
| _running = Completer(); |
| |
| var reloadedModules = 0; |
| try { |
| _dirtyModules.addAll(modules); |
| String? previousModuleId; |
| while (_dirtyModules.isNotEmpty) { |
| final moduleId = _dirtyModules.first; |
| _dirtyModules.remove(moduleId); |
| final parentIds = _moduleParents(moduleId); |
| // Check if this is the root / bootstrap module. |
| if (parentIds.isEmpty) { |
| // The bootstrap module is not reloaded but we need to update the |
| // $dartRunMain reference to the newly loaded child module. |
| final childModule = callMethod( |
| getProperty(require('dart_sdk'), 'dart'), |
| 'getModuleLibraries', |
| [previousModuleId]); |
| dartRunMain = allowInterop(() { |
| callMethod(_jsObjectValues(childModule).first, 'main', []); |
| }); |
| } else { |
| ++reloadedModules; |
| await _reloadModule(moduleId); |
| parentIds.sort(_moduleTopologicalCompare); |
| _dirtyModules.addAll(parentIds); |
| previousModuleId = moduleId; |
| } |
| } |
| print('$reloadedModules module(s) were hot-reloaded.'); |
| _running.complete(true); |
| } on HotReloadFailedException catch (e) { |
| print('Error during script reloading. Firing full page reload. $e'); |
| _reloadPage(); |
| _running.complete(false); |
| } |
| return _running.future; |
| } |
| |
| Future<void> _reloadModule(String moduleId) { |
| final completer = Completer(); |
| final stackTrace = StackTrace.current; |
| requireLoader.forceLoadModule( |
| moduleId, |
| allowInterop(completer.complete), |
| allowInterop((e) { |
| completer.completeError( |
| HotReloadFailedException(e.message), stackTrace); |
| }), |
| ); |
| return completer.future; |
| } |
| |
| void _reloadPage() { |
| window.location.reload(); |
| } |
| |
| void _updateGraph() { |
| final allModules = _allModules(); |
| final stronglyConnectedComponents = |
| graphs.stronglyConnectedComponents(allModules, _moduleParents); |
| _moduleOrdering.clear(); |
| for (var i = 0; i < stronglyConnectedComponents.length; i++) { |
| for (var module in stronglyConnectedComponents[i]) { |
| _moduleOrdering[module] = i; |
| } |
| } |
| } |
| |
| static Future<RequireRestarter> create() async { |
| final reloader = RequireRestarter._(); |
| await reloader._initialize(); |
| return reloader; |
| } |
| } |