| // Copyright (c) 2024, 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:convert'; |
| import 'dart:js_interop'; |
| |
| import 'package:dwds/src/utilities/shared.dart'; |
| |
| import 'restarter.dart'; |
| |
| @JS('dartDevEmbedder') |
| external _DartDevEmbedder get _dartDevEmbedder; |
| |
| extension type _DartDevEmbedder._(JSObject _) implements JSObject { |
| external _Debugger get debugger; |
| external JSPromise<JSAny?> hotRestart(); |
| external JSPromise<JSAny?> hotReload( |
| JSArray<JSString> filesToLoad, |
| JSArray<JSString> librariesToReload, |
| ); |
| external _DartDevEmbedderConfig get config; |
| } |
| |
| extension type _DartDevEmbedderConfig._(JSObject _) implements JSObject { |
| external JSFunction? capturedMainHandler; |
| external JSFunction? capturedHotReloadEndHandler; |
| } |
| |
| extension type _Debugger._(JSObject _) implements JSObject { |
| external JSPromise<JSString> invokeExtension(String key, String params); |
| external JSArray<JSString> get extensionNames; |
| |
| Future<void> maybeInvokeFlutterDisassemble() async { |
| final method = 'ext.flutter.disassemble'; |
| if (extensionNames.toDart.contains(method.toJS)) { |
| await invokeExtension(method, '{}').toDart; |
| } |
| } |
| |
| Future<void> maybeInvokeFlutterReassemble() async { |
| final method = 'ext.flutter.reassemble'; |
| if (extensionNames.toDart.contains(method.toJS)) { |
| await invokeExtension(method, '{}').toDart; |
| } |
| } |
| } |
| |
| @JS('XMLHttpRequest') |
| extension type _XMLHttpRequest._(JSObject _) implements JSObject { |
| external _XMLHttpRequest(); |
| external set withCredentials(bool value); |
| external set onreadystatechange(JSFunction fun); |
| external void open(String method, String url, bool async); |
| void get(String url, bool async) => open('GET', url, async); |
| external void send(); |
| external int get readyState; |
| external int get status; |
| external String get responseText; |
| } |
| |
| extension on JSArray<JSString> { |
| external void push(JSString value); |
| } |
| |
| class DdcLibraryBundleRestarter implements Restarter { |
| JSFunction? _capturedHotReloadEndCallback; |
| |
| Future<void> _runMainWhenReady( |
| Future? readyToRunMain, |
| JSFunction runMain, |
| ) async { |
| if (readyToRunMain != null) { |
| await readyToRunMain; |
| } |
| |
| runMain.callAsFunction(); |
| } |
| |
| Future<List<Map>> _getSrcModuleLibraries(String reloadedSourcesPath) async { |
| final completer = Completer<String>(); |
| final xhr = _XMLHttpRequest(); |
| xhr.withCredentials = true; |
| xhr.onreadystatechange = |
| () { |
| // If the request has completed and OK, or the response has not |
| // changed. |
| if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) { |
| completer.complete(xhr.responseText); |
| } |
| }.toJS; |
| xhr.get(reloadedSourcesPath, true); |
| xhr.send(); |
| final responseText = await completer.future; |
| |
| return (json.decode(responseText) as List).cast<Map>(); |
| } |
| |
| @override |
| Future<(bool, JSArray<JSObject>?)> restart({ |
| String? runId, |
| Future? readyToRunMain, |
| String? reloadedSourcesPath, |
| }) async { |
| assert( |
| reloadedSourcesPath != null, |
| "Expected 'reloadedSourcesPath' to not be null in a hot restart.", |
| ); |
| await _dartDevEmbedder.debugger.maybeInvokeFlutterDisassemble(); |
| final mainHandler = |
| (JSFunction runMain) { |
| _dartDevEmbedder.config.capturedMainHandler = null; |
| safeUnawaited(_runMainWhenReady(readyToRunMain, runMain)); |
| }.toJS; |
| _dartDevEmbedder.config.capturedMainHandler = mainHandler; |
| final srcModuleLibraries = await _getSrcModuleLibraries( |
| reloadedSourcesPath!, |
| ); |
| await _dartDevEmbedder.hotRestart().toDart; |
| return (true, srcModuleLibraries.jsify() as JSArray<JSObject>); |
| } |
| |
| @override |
| Future<JSArray<JSObject>> hotReloadStart(String reloadedSourcesPath) async { |
| final filesToLoad = JSArray<JSString>(); |
| final librariesToReload = JSArray<JSString>(); |
| final srcModuleLibraries = await _getSrcModuleLibraries( |
| reloadedSourcesPath, |
| ); |
| for (final srcModuleLibrary in srcModuleLibraries) { |
| final srcModuleLibraryCast = srcModuleLibrary.cast<String, Object>(); |
| final src = srcModuleLibraryCast['src'] as String; |
| final libraries = |
| (srcModuleLibraryCast['libraries'] as List).cast<String>(); |
| filesToLoad.push(src.toJS); |
| for (final library in libraries) { |
| librariesToReload.push(library.toJS); |
| } |
| } |
| _dartDevEmbedder.config.capturedHotReloadEndHandler = |
| (JSFunction hotReloadEndCallback) { |
| _capturedHotReloadEndCallback = hotReloadEndCallback; |
| }.toJS; |
| await _dartDevEmbedder.hotReload(filesToLoad, librariesToReload).toDart; |
| return srcModuleLibraries.jsify() as JSArray<JSObject>; |
| } |
| |
| @override |
| Future<void> hotReloadEnd() async { |
| // Requires a previous call to `hotReloadStart`. |
| _capturedHotReloadEndCallback!.callAsFunction(); |
| _dartDevEmbedder.config.capturedHotReloadEndHandler = null; |
| _capturedHotReloadEndCallback = null; |
| } |
| |
| /// Handles service extension requests using the dart dev embedder |
| Future<Map<String, dynamic>?> handleServiceExtension( |
| String method, |
| Map<String, dynamic> args, |
| ) async { |
| if (method == 'ext.flutter.reassemble') { |
| await _dartDevEmbedder.debugger.maybeInvokeFlutterReassemble(); |
| return {'status': 'reassemble invoked'}; |
| } else if (method == 'getExtensionRpcs') { |
| final rpcs = |
| _dartDevEmbedder.debugger.extensionNames.toDart.cast<String>(); |
| return {'rpcs': rpcs}; |
| } else { |
| // For other extension methods, delegate to the debugger |
| final params = args.isNotEmpty ? jsonEncode(args) : '{}'; |
| final resultJson = |
| await _dartDevEmbedder.debugger |
| .invokeExtension(method, params) |
| .toDart; |
| return jsonDecode(resultJson.toDart) as Map<String, dynamic>; |
| } |
| } |
| } |