// 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. import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:js_interop'; import 'package:dwds/src/utilities/shared.dart'; import 'package:graphs/graphs.dart' as graphs; import 'package:http/browser_client.dart'; import 'package:web/web.dart'; import '../run_main.dart'; import '../web_utils.dart'; import 'restarter.dart'; @JS(r'$dartRunMain') external set dartRunMain(JSFunction func); @JS(r'$dartRunMain') external JSFunction get dartRunMain; @JS(r'$requireLoader') external RequireLoader get requireLoader; @anonymous @JS() @staticInterop class RequireLoader {} extension RequireLoaderExtension on RequireLoader { external String get digestsPath; external JsMap get moduleParentsGraph; external void forceLoadModule( JSString moduleId, JSFunction callback, JSFunction onError, ); } @anonymous @JS() @staticInterop class Sdk {} @anonymous @JS() @staticInterop class SdkDeveloper {} @anonymous @JS() @staticInterop class SdkDart {} @anonymous @JS() @staticInterop class SdkExt {} @anonymous @JS() @staticInterop class MainLibrary {} @JS(r'$loadModuleConfig') external Sdk require(String value); Sdk get sdk => require('dart_sdk'); extension SdkExtension on Sdk { external SdkDart get dart; external SdkDeveloper get developer; } extension SdkDeveloperExtension on SdkDeveloper { external JSPromise invokeExtension(String key, String params); external SdkExt get _extensions; Future maybeInvokeFlutterDisassemble() async { final method = 'ext.flutter.disassemble'; if (_extensions.containsKey(method)) { await invokeExtension(method, '{}').toDart; } } } extension SdkDartExtension on SdkDart { external void hotRestart(); external JSObject getModuleLibraries(String? moduleId); MainLibrary getMainLibrary(String? moduleId) { final libraries = getModuleLibraries(moduleId).values; return libraries.first! as MainLibrary; } } extension SdkExtExtension on SdkExt { external bool containsKey(String key); } extension MainLibraryExtension on MainLibrary { external void main(); } class HotReloadFailedException implements Exception { final String _s; HotReloadFailedException(this._s); @override String toString() => "HotReloadFailedException: '$_s'"; } /// Handles hot restart reloading for use with the require module system. class RequireRestarter implements Restarter { /// The last known digests of all the modules in the application. /// /// This is updated in place during calls to hotRestart. static late Map _lastKnownDigests; final _moduleOrdering = HashMap(); late SplayTreeSet _dirtyModules; var _running = Completer()..complete(true); var count = 0; RequireRestarter._() { _dirtyModules = SplayTreeSet(_moduleTopologicalCompare); } @override Future<(bool, JSArray?)> restart({ String? runId, Future? readyToRunMain, String? reloadedSourcesPath, }) async { assert( reloadedSourcesPath == null, "'reloadedSourcesPath' should not be used for the AMD module format.", ); await sdk.developer.maybeInvokeFlutterDisassemble(); final newDigests = await _getDigests(); final modulesToLoad = []; for (final 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); } sdk.dart.hotRestart(); safeUnawaited(_runMainWhenReady(readyToRunMain)); return (result, null); } @override Future hotReloadEnd() => throw UnimplementedError( 'Hot reload is not supported for the AMD module format.', ); @override Future> hotReloadStart(String reloadedSourcesPath) => throw UnimplementedError( 'Hot reload is not supported for the AMD module format.', ); Future _runMainWhenReady(Future? readyToRunMain) async { if (readyToRunMain != null) { await readyToRunMain; } runMain(); } Iterable _allModules() => requireLoader.moduleParentsGraph.modules; Future> _getDigests() async { final client = BrowserClient(); final response = await client.get(Uri.parse(requireLoader.digestsPath)); return (jsonDecode(response.body) as Map).cast(); } Future _initialize() async { _lastKnownDigests = await _getDigests(); } List _moduleParents(String module) => requireLoader.moduleParentsGraph.parents(module); 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 _reload(List modules) async { final dart = sdk.dart; // 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. // ignore: unnecessary_lambdas dartRunMain = () { dart.getMainLibrary(previousModuleId).main(); }.toJS; } 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 _reloadModule(String moduleId) { final completer = Completer(); final stackTrace = StackTrace.current; requireLoader.forceLoadModule( moduleId.toJS, // Removing the argument type in complete() // ignore: unnecessary_lambdas () { completer.complete(); }.toJS, (JsError e) { completer.completeError( HotReloadFailedException(e.message), stackTrace, ); }.toJS, ); 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 (final module in stronglyConnectedComponents[i]) { _moduleOrdering[module] = i; } } } static Future create() async { final reloader = RequireRestarter._(); await reloader._initialize(); return reloader; } }