blob: 8c15dfc98d6e23d914cf160de3a8cfdcbd95f013 [file] [log] [blame]
// 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>;
}
}
}