| // 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:convert'; |
| import 'dart:js_interop'; |
| |
| import 'package:built_collection/built_collection.dart'; |
| import 'package:dwds/data/build_result.dart'; |
| import 'package:dwds/data/connect_request.dart'; |
| import 'package:dwds/data/debug_event.dart'; |
| import 'package:dwds/data/debug_info.dart'; |
| import 'package:dwds/data/devtools_request.dart'; |
| import 'package:dwds/data/error_response.dart'; |
| import 'package:dwds/data/extension_request.dart'; |
| import 'package:dwds/data/hot_reload_request.dart'; |
| import 'package:dwds/data/hot_reload_response.dart'; |
| import 'package:dwds/data/hot_restart_request.dart'; |
| import 'package:dwds/data/hot_restart_response.dart'; |
| import 'package:dwds/data/register_event.dart'; |
| import 'package:dwds/data/run_request.dart'; |
| import 'package:dwds/data/serializers.dart'; |
| import 'package:dwds/data/service_extension_request.dart'; |
| import 'package:dwds/data/service_extension_response.dart'; |
| import 'package:dwds/shared/batched_stream.dart'; |
| import 'package:dwds/src/sockets.dart'; |
| import 'package:http/browser_client.dart'; |
| import 'package:sse/client/sse_client.dart'; |
| import 'package:uuid/uuid.dart'; |
| import 'package:web/web.dart'; |
| import 'package:web_socket_channel/web_socket_channel.dart'; |
| |
| import 'reloader/ddc_library_bundle_restarter.dart'; |
| import 'reloader/ddc_restarter.dart'; |
| import 'reloader/manager.dart'; |
| import 'reloader/require_restarter.dart'; |
| import 'run_main.dart'; |
| import 'web_utils.dart'; |
| |
| const _batchDelayMilliseconds = 1000; |
| |
| // GENERATE: |
| // pub run build_runner build web |
| Future<void>? main() { |
| return runZonedGuarded( |
| () async { |
| // Set the unique id for this instance of the app. |
| // Test apps may already have this set. |
| dartAppInstanceId ??= const Uuid().v1(); |
| |
| final fixedPath = _fixProtocol(dwdsDevHandlerPath); |
| final fixedUri = Uri.parse(fixedPath); |
| final client = |
| fixedUri.isScheme('ws') || fixedUri.isScheme('wss') |
| ? WebSocketClient(WebSocketChannel.connect(fixedUri)) |
| : SseSocketClient( |
| SseClient(fixedPath, debugKey: 'InjectedClient'), |
| ); |
| |
| final restarter = switch (dartModuleStrategy) { |
| 'require-js' => await RequireRestarter.create(), |
| 'ddc-library-bundle' => DdcLibraryBundleRestarter(), |
| 'ddc' || 'legacy' => DdcRestarter(), |
| _ => throw StateError('Unknown module strategy: $dartModuleStrategy'), |
| }; |
| |
| final manager = ReloadingManager(client, restarter); |
| |
| hotReloadStartJs = |
| () { |
| return manager.hotReloadStart(hotReloadReloadedSourcesPath).toJS; |
| }.toJS; |
| |
| hotReloadEndJs = |
| () { |
| return manager.hotReloadEnd().toJS; |
| }.toJS; |
| |
| Completer? readyToRunMainCompleter; |
| |
| hotRestartJs = |
| (String runId, [bool? pauseIsolatesOnStart]) { |
| if (pauseIsolatesOnStart ?? false) { |
| readyToRunMainCompleter = Completer(); |
| return manager |
| .hotRestart( |
| runId: runId, |
| readyToRunMain: readyToRunMainCompleter!.future, |
| reloadedSourcesPath: hotRestartReloadedSourcesPath, |
| ) |
| .toJS; |
| } else { |
| return manager |
| .hotRestart( |
| runId: runId, |
| reloadedSourcesPath: hotRestartReloadedSourcesPath, |
| ) |
| .toJS; |
| } |
| }.toJS; |
| |
| readyToRunMainJs = |
| () { |
| if (readyToRunMainCompleter == null) return; |
| if (readyToRunMainCompleter!.isCompleted) return; |
| readyToRunMainCompleter!.complete(); |
| readyToRunMainCompleter = null; |
| }.toJS; |
| |
| final debugEventController = BatchedStreamController<DebugEvent>( |
| delay: _batchDelayMilliseconds, |
| ); |
| debugEventController.stream.listen((events) { |
| if (dartEmitDebugEvents) { |
| _trySendEvent( |
| client.sink, |
| jsonEncode( |
| serializers.serialize( |
| BatchedDebugEvents( |
| (b) => b.events = ListBuilder<DebugEvent>(events), |
| ), |
| ), |
| ), |
| ); |
| } |
| }); |
| |
| emitDebugEvent = |
| (String kind, String eventData) { |
| if (dartEmitDebugEvents) { |
| _trySendEvent( |
| debugEventController.sink, |
| DebugEvent( |
| (b) => |
| b |
| ..timestamp = (DateTime.now().millisecondsSinceEpoch) |
| ..kind = kind |
| ..eventData = eventData, |
| ), |
| ); |
| } |
| }.toJS; |
| |
| emitRegisterEvent = |
| (String eventData) { |
| _trySendEvent( |
| client.sink, |
| jsonEncode( |
| serializers.serialize( |
| RegisterEvent( |
| (b) => |
| b |
| ..timestamp = (DateTime.now().millisecondsSinceEpoch) |
| ..eventData = eventData, |
| ), |
| ), |
| ), |
| ); |
| }.toJS; |
| |
| launchDevToolsJs = |
| () { |
| if (!_isChromium) { |
| window.alert( |
| 'Dart DevTools is only supported on Chromium based browsers.', |
| ); |
| return; |
| } |
| _trySendEvent( |
| client.sink, |
| jsonEncode( |
| serializers.serialize( |
| DevToolsRequest( |
| (b) => |
| b |
| ..appId = dartAppId |
| ..instanceId = dartAppInstanceId, |
| ), |
| ), |
| ), |
| ); |
| }.toJS; |
| |
| client.stream.listen( |
| (serialized) async { |
| final event = serializers.deserialize(jsonDecode(serialized)); |
| if (event is BuildResult) { |
| if (reloadConfiguration == 'ReloadConfiguration.liveReload') { |
| manager.reloadPage(); |
| } else if (reloadConfiguration == |
| 'ReloadConfiguration.hotRestart') { |
| await manager.hotRestart( |
| reloadedSourcesPath: hotRestartReloadedSourcesPath, |
| ); |
| } else if (reloadConfiguration == 'ReloadConfiguration.hotReload') { |
| await manager.hotReloadStart(hotReloadReloadedSourcesPath); |
| await manager.hotReloadEnd(); |
| } |
| } else if (event is DevToolsResponse) { |
| if (!event.success) { |
| final alert = 'DevTools failed to open with:\n${event.error}'; |
| if (event.promptExtension && window.confirm(alert)) { |
| window.open( |
| 'https://dart.dev/to/web-debug-extension', |
| '_blank', |
| ); |
| } else { |
| window.alert(alert); |
| } |
| } |
| } else if (event is RunRequest) { |
| runMain(); |
| } else if (event is ErrorResponse) { |
| window.reportError( |
| 'Error from backend:\n\n' |
| 'Error: ${event.error}\n\n' |
| 'Stack Trace:\n${event.stackTrace}' |
| .toJS, |
| ); |
| } else if (event is HotReloadRequest) { |
| await handleWebSocketHotReloadRequest(event, manager, client.sink); |
| } else if (event is HotRestartRequest) { |
| await handleWebSocketHotRestartRequest(event, manager, client.sink); |
| } else if (event is ServiceExtensionRequest) { |
| await handleServiceExtensionRequest(event, client.sink, manager); |
| } |
| }, |
| onError: (error) { |
| // An error is propagated on a full page reload as Chrome presumably |
| // forces the SSE connection to close in a bad state. This does not cause |
| // any adverse effects so simply swallow this error as to not print the |
| // misleading unhandled error message. |
| }, |
| ); |
| |
| if (dwdsEnableDevToolsLaunch) { |
| window.onKeyDown.listen((Event e) { |
| if (e.isA<KeyboardEvent>()) { |
| final event = e as KeyboardEvent; |
| if (const [ |
| 'd', |
| 'D', |
| '∂', // alt-d output on Mac |
| 'ÃŽ', // shift-alt-D output on Mac |
| ].contains(event.key) && |
| event.altKey && |
| !event.ctrlKey && |
| !event.metaKey) { |
| event.preventDefault(); |
| launchDevToolsJs.callAsFunction(); |
| } |
| } |
| }); |
| } |
| |
| if (dartModuleStrategy != 'ddc-library-bundle') { |
| if (_isChromium) { |
| _sendConnectRequest(client.sink); |
| } else { |
| // If not Chromium we just invoke main, devtools aren't supported. |
| runMain(); |
| } |
| } else { |
| _sendConnectRequest(client.sink); |
| } |
| _launchCommunicationWithDebugExtension(); |
| }, |
| (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 |
| '''); |
| }, |
| ); |
| } |
| |
| void _trySendEvent<T>(StreamSink<T> sink, T serialized) { |
| try { |
| sink.add(serialized); |
| } on StateError catch (_) { |
| // An error is propagated on a full page reload as Chrome presumably |
| // forces the SSE connection to close in a bad state. |
| print( |
| 'Cannot send event $serialized. ' |
| 'Injected client connection is closed.', |
| ); |
| } |
| } |
| |
| void _sendConnectRequest(StreamSink clientSink) { |
| _trySendEvent( |
| clientSink, |
| jsonEncode( |
| serializers.serialize( |
| ConnectRequest( |
| (b) => |
| b |
| ..appId = dartAppId |
| ..instanceId = dartAppInstanceId |
| ..entrypointPath = dartEntrypointPath, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| /// Returns [url] modified if necessary so that, if the current page is served |
| /// over `https`, then the URL is converted to `https`. |
| String _fixProtocol(String url) { |
| var uri = Uri.parse(url); |
| if (window.location.protocol == 'https:' && |
| uri.scheme == 'http' && |
| // Chrome allows mixed content on localhost. It is not safe to assume the |
| // server is also listening on https. |
| uri.host != 'localhost') { |
| uri = uri.replace(scheme: 'https'); |
| } else if (window.location.protocol == 'wss:' && |
| uri.scheme == 'ws' && |
| uri.host != 'localhost') { |
| uri = uri.replace(scheme: 'wss'); |
| } |
| return uri.toString(); |
| } |
| |
| void _launchCommunicationWithDebugExtension() { |
| // Listen for an event from the Dart Debug Extension to authenticate the |
| // user (sent once the extension receives the dart-app-read event): |
| _listenForDebugExtensionAuthRequest(); |
| |
| // Send the dart-app-ready event along with debug info to the Dart Debug |
| // Extension so that it can debug the Dart app: |
| final debugInfoJson = jsonEncode( |
| serializers.serialize( |
| DebugInfo( |
| (b) => |
| b |
| ..appEntrypointPath = dartEntrypointPath |
| ..appId = windowContext.$dartAppId |
| ..appInstanceId = dartAppInstanceId |
| ..appOrigin = window.location.origin |
| ..appUrl = window.location.href |
| ..authUrl = _authUrl |
| ..extensionUrl = windowContext.$dartExtensionUri |
| ..isInternalBuild = windowContext.$isInternalBuild |
| ..isFlutterApp = windowContext.$isFlutterApp |
| ..workspaceName = dartWorkspaceName, |
| ), |
| ), |
| ); |
| _dispatchEvent('dart-app-ready', debugInfoJson); |
| } |
| |
| void _dispatchEvent(String message, String detail) { |
| final event = CustomEvent(message, CustomEventInit(detail: detail.toJS)); |
| document.dispatchEvent(event); |
| } |
| |
| void _listenForDebugExtensionAuthRequest() { |
| window.addEventListener('message', _handleAuthRequest.toJS); |
| } |
| |
| void _handleAuthRequest(Event event) { |
| final messageEvent = event as MessageEvent; |
| final data = messageEvent.data; |
| |
| if (!data.typeofEquals('string')) return; |
| if ((data as JSString).toDart != 'dart-auth-request') return; |
| |
| // Notify the Dart Debug Extension of authentication status: |
| if (_authUrl != null) { |
| _authenticateUser(_authUrl!).then( |
| (isAuthenticated) => |
| _dispatchEvent('dart-auth-response', '$isAuthenticated'), |
| ); |
| } |
| } |
| |
| Future<bool> _authenticateUser(String authUrl) async { |
| final client = BrowserClient()..withCredentials = true; |
| final response = await client.get(Uri.parse(authUrl)); |
| final responseText = response.body; |
| return responseText.contains('Dart Debug Authentication Success!'); |
| } |
| |
| void _sendResponse<T>( |
| StreamSink clientSink, |
| T Function(void Function(dynamic)) builder, |
| String requestId, { |
| bool success = true, |
| String? errorMessage, |
| }) { |
| _trySendEvent( |
| clientSink, |
| jsonEncode( |
| serializers.serialize( |
| builder((b) { |
| b.id = requestId; |
| b.success = success; |
| if (errorMessage != null) b.errorMessage = errorMessage; |
| }), |
| ), |
| ), |
| ); |
| } |
| |
| void _sendHotReloadResponse( |
| StreamSink clientSink, |
| String requestId, { |
| bool success = true, |
| String? errorMessage, |
| }) { |
| _sendResponse<HotReloadResponse>( |
| clientSink, |
| HotReloadResponse.new, |
| requestId, |
| success: success, |
| errorMessage: errorMessage, |
| ); |
| } |
| |
| void _sendHotRestartResponse( |
| StreamSink clientSink, |
| String requestId, { |
| bool success = true, |
| String? errorMessage, |
| }) { |
| _sendResponse<HotRestartResponse>( |
| clientSink, |
| HotRestartResponse.new, |
| requestId, |
| success: success, |
| errorMessage: errorMessage, |
| ); |
| } |
| |
| void _sendServiceExtensionResponse( |
| StreamSink clientSink, |
| String requestId, { |
| bool success = true, |
| String? errorMessage, |
| int? errorCode, |
| Map<String, dynamic>? result, |
| }) { |
| _trySendEvent( |
| clientSink, |
| jsonEncode( |
| serializers.serialize( |
| ServiceExtensionResponse.fromResult( |
| id: requestId, |
| success: success, |
| errorMessage: errorMessage, |
| errorCode: errorCode, |
| result: result, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Future<void> handleWebSocketHotReloadRequest( |
| HotReloadRequest event, |
| ReloadingManager manager, |
| StreamSink clientSink, |
| ) async { |
| final requestId = event.id; |
| try { |
| await manager.hotReloadStart(hotReloadReloadedSourcesPath); |
| await manager.hotReloadEnd(); |
| _sendHotReloadResponse(clientSink, requestId, success: true); |
| } catch (e) { |
| _sendHotReloadResponse( |
| clientSink, |
| requestId, |
| success: false, |
| errorMessage: e.toString(), |
| ); |
| } |
| } |
| |
| Future<void> handleWebSocketHotRestartRequest( |
| HotRestartRequest event, |
| ReloadingManager manager, |
| StreamSink clientSink, |
| ) async { |
| final requestId = event.id; |
| try { |
| final runId = const Uuid().v4().toString(); |
| await manager.hotRestart( |
| runId: runId, |
| reloadedSourcesPath: hotRestartReloadedSourcesPath, |
| ); |
| _sendHotRestartResponse(clientSink, requestId, success: true); |
| } catch (e) { |
| _sendHotRestartResponse( |
| clientSink, |
| requestId, |
| success: false, |
| errorMessage: e.toString(), |
| ); |
| } |
| } |
| |
| Future<void> handleServiceExtensionRequest( |
| ServiceExtensionRequest request, |
| StreamSink clientSink, |
| ReloadingManager manager, |
| ) async { |
| try { |
| final result = await manager.handleServiceExtension( |
| request.method, |
| request.args, |
| ); |
| |
| if (result != null) { |
| _sendServiceExtensionResponse( |
| clientSink, |
| request.id, |
| success: true, |
| result: result, |
| ); |
| } else { |
| // Service extension not supported by this restarter type |
| _sendServiceExtensionResponse( |
| clientSink, |
| request.id, |
| success: false, |
| errorMessage: 'Service extension not supported', |
| errorCode: -32601, // Method not found |
| ); |
| } |
| } catch (e) { |
| _sendServiceExtensionResponse( |
| clientSink, |
| request.id, |
| success: false, |
| errorMessage: e.toString(), |
| ); |
| } |
| } |
| |
| @JS(r'$dartAppId') |
| external String get dartAppId; |
| |
| @JS(r'$dartAppInstanceId') |
| external String? get dartAppInstanceId; |
| |
| @JS(r'$dwdsDevHandlerPath') |
| external String get dwdsDevHandlerPath; |
| |
| @JS(r'$dartAppInstanceId') |
| external set dartAppInstanceId(String? id); |
| |
| @JS(r'$dartModuleStrategy') |
| external String get dartModuleStrategy; |
| |
| @JS(r'$dartHotReloadStartDwds') |
| external set hotReloadStartJs(JSFunction cb); |
| |
| @JS(r'$dartHotReloadEndDwds') |
| external set hotReloadEndJs(JSFunction cb); |
| |
| @JS(r'$reloadedSourcesPath') |
| external String? get _reloadedSourcesPath; |
| |
| String? get hotRestartReloadedSourcesPath => _reloadedSourcesPath; |
| |
| String get hotReloadReloadedSourcesPath { |
| final path = _reloadedSourcesPath; |
| assert( |
| path != null, |
| "Expected 'reloadedSourcesPath' to not be null in a hot reload.", |
| ); |
| return path!; |
| } |
| |
| @JS(r'$dartHotRestartDwds') |
| external set hotRestartJs(JSFunction cb); |
| |
| @JS(r'$dartReadyToRunMain') |
| external set readyToRunMainJs(JSFunction cb); |
| |
| @JS(r'$launchDevTools') |
| external JSFunction get launchDevToolsJs; |
| |
| @JS(r'$launchDevTools') |
| external set launchDevToolsJs(JSFunction cb); |
| |
| @JS(r'$dartReloadConfiguration') |
| external String get reloadConfiguration; |
| |
| @JS(r'$dartEntrypointPath') |
| external String get dartEntrypointPath; |
| |
| @JS(r'$dwdsEnableDevToolsLaunch') |
| external bool get dwdsEnableDevToolsLaunch; |
| |
| @JS('window.top.document.dispatchEvent') |
| external void dispatchEvent(CustomEvent event); |
| |
| @JS(r'$dartEmitDebugEvents') |
| external bool get dartEmitDebugEvents; |
| |
| @JS(r'$emitDebugEvent') |
| external set emitDebugEvent(JSFunction func); |
| |
| @JS(r'$emitRegisterEvent') |
| external set emitRegisterEvent(JSFunction func); |
| |
| @JS(r'$isInternalBuild') |
| external bool get isInternalBuild; |
| |
| @JS(r'$isFlutterApp') |
| external bool get isFlutterApp; |
| |
| @JS(r'$dartWorkspaceName') |
| external String? get dartWorkspaceName; |
| |
| bool get _isChromium => window.navigator.vendor.contains('Google'); |
| |
| String? get _authUrl { |
| final extensionUrl = windowContext.$dartExtensionUri; |
| if (extensionUrl == null) return null; |
| final authUrl = Uri.parse(extensionUrl).replace(path: authenticationPath); |
| switch (authUrl.scheme) { |
| case 'ws': |
| return authUrl.replace(scheme: 'http').toString(); |
| case 'wss': |
| return authUrl.replace(scheme: 'https').toString(); |
| default: |
| return authUrl.toString(); |
| } |
| } |