2.0.0
This commit is contained in:
parent
8cbfb713bd
commit
eaf78ed1a5
5 changed files with 101 additions and 38 deletions
|
@ -1,6 +1,8 @@
|
||||||
# hot
|
# hot
|
||||||
[![Pub](https://img.shields.io/pub/v/angel_hot.svg)](https://pub.dartlang.org/packages/angel_hot)
|
[![Pub](https://img.shields.io/pub/v/angel_hot.svg)](https://pub.dartlang.org/packages/angel_hot)
|
||||||
|
|
||||||
|
![Screenshot of terminal](screenshots/screenshot.png)
|
||||||
|
|
||||||
Supports *hot reloading* of Angel servers on file changes. This is faster and
|
Supports *hot reloading* of Angel servers on file changes. This is faster and
|
||||||
more reliable than merely reactively restarting a `Process`.
|
more reliable than merely reactively restarting a `Process`.
|
||||||
|
|
||||||
|
@ -22,7 +24,9 @@ Usage is fairly simple. Pass a function that creates an `Angel` server, along wi
|
||||||
to watch, to the `HotReloader` constructor. The rest is history!!!
|
to watch, to the `HotReloader` constructor. The rest is history!!!
|
||||||
|
|
||||||
The recommended pattern is to only use hot-reloading in your application entry point. Create your `Angel` instance
|
The recommended pattern is to only use hot-reloading in your application entry point. Create your `Angel` instance
|
||||||
within a separate function, conventionally named `createServer`. Using this in production mode is pointless.
|
within a separate function, conventionally named `createServer`.
|
||||||
|
|
||||||
|
**Using this in production mode is pointless.**
|
||||||
|
|
||||||
You can watch:
|
You can watch:
|
||||||
* Files
|
* Files
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
analyzer:
|
analyzer:
|
||||||
strong-mode: true
|
strong-mode:
|
||||||
|
implicit-casts: false
|
|
@ -14,9 +14,7 @@ main() async {
|
||||||
'main.dart',
|
'main.dart',
|
||||||
Uri.parse('package:angel_hot/angel_hot.dart')
|
Uri.parse('package:angel_hot/angel_hot.dart')
|
||||||
]);
|
]);
|
||||||
var server = await hot.startServer('127.0.0.1', 3000);
|
await hot.startServer('127.0.0.1', 3000);
|
||||||
print(
|
|
||||||
'Hot server listening at http://${server.address.address}:${server.port}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Angel> createServer() async {
|
Future<Angel> createServer() async {
|
||||||
|
|
|
@ -15,10 +15,12 @@ import 'dart:io'
|
||||||
Platform,
|
Platform,
|
||||||
exit,
|
exit,
|
||||||
stderr,
|
stderr,
|
||||||
|
stdin,
|
||||||
stdout;
|
stdout;
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_websocket/server.dart';
|
import 'package:angel_websocket/server.dart';
|
||||||
|
import 'package:charcode/ascii.dart';
|
||||||
import 'package:dart2_constant/convert.dart';
|
import 'package:dart2_constant/convert.dart';
|
||||||
import 'package:dart2_constant/io.dart';
|
import 'package:dart2_constant/io.dart';
|
||||||
import 'package:glob/glob.dart';
|
import 'package:glob/glob.dart';
|
||||||
|
@ -102,16 +104,16 @@ class HotReloader {
|
||||||
var response = request.response;
|
var response = request.response;
|
||||||
response.statusCode = HttpStatus.badGateway;
|
response.statusCode = HttpStatus.badGateway;
|
||||||
response.headers
|
response.headers
|
||||||
..contentType = ContentType.HTML
|
..contentType = ContentType.html
|
||||||
..set(HttpHeaders.SERVER, 'angel_hot');
|
..set(HttpHeaders.serverHeader, 'angel_hot');
|
||||||
|
|
||||||
if (request.headers
|
if (request.headers
|
||||||
.value(HttpHeaders.ACCEPT_ENCODING)
|
.value(HttpHeaders.acceptEncodingHeader)
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
?.contains('gzip') ==
|
?.contains('gzip') ==
|
||||||
true) {
|
true) {
|
||||||
response
|
response
|
||||||
..headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip')
|
..headers.set(HttpHeaders.contentEncodingHeader, 'gzip')
|
||||||
..add(gzip.encode(utf8.encode(_renderer.render(doc))));
|
..add(gzip.encode(utf8.encode(_renderer.render(doc))));
|
||||||
} else
|
} else
|
||||||
response.write(_renderer.render(doc));
|
response.write(_renderer.render(doc));
|
||||||
|
@ -148,44 +150,94 @@ class HotReloader {
|
||||||
|
|
||||||
/// Starts listening to requests and filesystem events.
|
/// Starts listening to requests and filesystem events.
|
||||||
Future<HttpServer> startServer([address, int port]) async {
|
Future<HttpServer> startServer([address, int port]) async {
|
||||||
|
var isHot = true;
|
||||||
_server = await _generateServer();
|
_server = await _generateServer();
|
||||||
|
|
||||||
if (_paths?.isNotEmpty != true)
|
if (_paths?.isNotEmpty != true)
|
||||||
print(
|
print(yellow.wrap(
|
||||||
'WARNING: You have instantiated a HotReloader without providing any filesystem paths to watch.');
|
'WARNING: You have instantiated a HotReloader without providing any filesystem paths to watch.'));
|
||||||
|
|
||||||
if (!Platform.executableArguments.contains('--observe') &&
|
if (!Platform.executableArguments.contains('--observe') &&
|
||||||
!Platform.executableArguments.contains('--enable-vm-service')) {
|
!Platform.executableArguments.contains('--enable-vm-service')) {
|
||||||
stderr.writeln(
|
stderr.writeln(yellow.wrap(
|
||||||
'WARNING: You have instantiated a HotReloader without passing `--enable-vm-service` or `--observe` to the Dart VM. Hot reloading will be disabled.');
|
'WARNING: You have instantiated a HotReloader without passing `--enable-vm-service` or `--observe` to the Dart VM. Hot reloading will be disabled.'));
|
||||||
|
isHot = false;
|
||||||
} else {
|
} else {
|
||||||
_client = await vm.vmServiceConnect(
|
_client = await vm.vmServiceConnect(
|
||||||
vmServiceHost ?? 'localhost', vmServicePort ?? 8181);
|
vmServiceHost ?? 'localhost', vmServicePort ?? 8181);
|
||||||
_vmachine ??= await _client.getVM();
|
_vmachine ??= await _client.getVM();
|
||||||
_mainIsolate ??= _vmachine.isolates.first;
|
_mainIsolate ??= _vmachine.isolates.first;
|
||||||
await _client.setExceptionPauseMode(_mainIsolate.id, 'None');
|
await _client.setExceptionPauseMode(_mainIsolate.id, 'None');
|
||||||
|
|
||||||
_onChange.stream
|
|
||||||
.transform(new _Debounce(new Duration(seconds: 1)))
|
|
||||||
.listen(_handleWatchEvent);
|
|
||||||
await _listenToFilesystem();
|
await _listenToFilesystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onChange.stream
|
||||||
|
//.transform(new _Debounce(new Duration(seconds: 1)))
|
||||||
|
.listen(_handleWatchEvent);
|
||||||
|
|
||||||
while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst());
|
while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst());
|
||||||
var server = await HttpServer.bind(address ?? '127.0.0.1', port ?? 0);
|
var server = await HttpServer.bind(address ?? '127.0.0.1', port ?? 0);
|
||||||
server.listen(handleRequest);
|
server.listen(handleRequest);
|
||||||
|
|
||||||
// Print a Flutter-like prompt...
|
// Print a Flutter-like prompt...
|
||||||
if (enableHotkeys) {
|
if (enableHotkeys) {
|
||||||
|
var serverUri = new Uri(
|
||||||
|
scheme: 'http', host: server.address.address, port: server.port);
|
||||||
var host = vmServiceHost == 'localhost' ? '127.0.0.1' : vmServiceHost;
|
var host = vmServiceHost == 'localhost' ? '127.0.0.1' : vmServiceHost;
|
||||||
var observatoryUri =
|
var observatoryUri =
|
||||||
new Uri(scheme: 'http', host: host, port: vmServicePort);
|
new Uri(scheme: 'http', host: host, port: vmServicePort);
|
||||||
|
|
||||||
print(styleBold.wrap(
|
print(styleBold.wrap(
|
||||||
'🔥 To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".'));
|
'\n🔥 To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".'));
|
||||||
|
stdout.write('Your Angel server is listening at: ');
|
||||||
|
print(wrapWith('$serverUri', [cyan, styleUnderlined]));
|
||||||
stdout.write(
|
stdout.write(
|
||||||
'An Observatory debugger and profiler on iPhone XS Max is available at: ');
|
'An Observatory debugger and profiler on ${Platform.operatingSystem} is available at: ');
|
||||||
print(wrapWith('$observatoryUri', [cyan, styleUnderlined]));
|
print(wrapWith('$observatoryUri', [cyan, styleUnderlined]));
|
||||||
print('To quit, press "q".');
|
print(
|
||||||
|
'For a more detailed help message, press "h". To quit, press "q".\n');
|
||||||
|
|
||||||
|
if (_paths.isNotEmpty) {
|
||||||
|
print(darkGray.wrap(
|
||||||
|
'Changes to the following path(s) will also trigger a hot reload:'));
|
||||||
|
|
||||||
|
for (var p in _paths) {
|
||||||
|
print(darkGray.wrap(' * $p'));
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.writeln();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for hotkeys
|
||||||
|
stdin.lineMode = stdin.echoMode = false;
|
||||||
|
|
||||||
|
StreamSubscription<int> sub;
|
||||||
|
sub = stdin.expand((l) => l).listen((ch) async {
|
||||||
|
var ch = stdin.readByteSync();
|
||||||
|
|
||||||
|
if (ch == $r) {
|
||||||
|
_handleWatchEvent(
|
||||||
|
new WatchEvent(ChangeType.MODIFY, '[manual-reload]'), isHot);
|
||||||
|
}
|
||||||
|
if (ch == $R) {
|
||||||
|
//print('Manually restarting server...\n');
|
||||||
|
_handleWatchEvent(
|
||||||
|
new WatchEvent(ChangeType.MODIFY, '[manual-restart]'), false);
|
||||||
|
} else if (ch == $q) {
|
||||||
|
stdin.echoMode = stdin.lineMode = true;
|
||||||
|
close();
|
||||||
|
sub.cancel();
|
||||||
|
exit(0);
|
||||||
|
} else if (ch == $h) {
|
||||||
|
print(
|
||||||
|
'Press "r" to hot reload the Dart VM, and restart the active server.');
|
||||||
|
print(
|
||||||
|
'Press "R" to restart the server, WITHOUT a hot reload of the VM.');
|
||||||
|
print('Press "q" to quit the server.');
|
||||||
|
print('Press "h" to display this help information.');
|
||||||
|
stdout.writeln();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
|
@ -240,7 +292,7 @@ class HotReloader {
|
||||||
stderr.writeln('Could not listen to file changes at ${path}: $e');
|
stderr.writeln('Could not listen to file changes at ${path}: $e');
|
||||||
});
|
});
|
||||||
|
|
||||||
print('Listening for file changes at ${path}...');
|
// print('Listening for file changes at ${path}...');
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is! FileSystemException) rethrow;
|
if (e is! FileSystemException) rethrow;
|
||||||
|
@ -255,37 +307,43 @@ class HotReloader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleWatchEvent(WatchEvent e) async {
|
_handleWatchEvent(WatchEvent e, [bool hot = true]) async {
|
||||||
print('${e.path} changed. Reloading server...');
|
print('${e.path} changed. Reloading server...\n');
|
||||||
var old = _server;
|
var old = _server;
|
||||||
|
|
||||||
if (old != null) {
|
if (old != null) {
|
||||||
// Do this asynchronously, because we really don't care about the old server anymore.
|
// Do this asynchronously, because we really don't care about the old server anymore.
|
||||||
new Future(() async {
|
new Future(() async {
|
||||||
// TODO: Instead of disconnecting, just forward websockets to the next server.
|
|
||||||
// Disconnect active WebSockets
|
// Disconnect active WebSockets
|
||||||
var ws = old.app.container.make(AngelWebSocket) as AngelWebSocket;
|
try {
|
||||||
|
var ws = old.app.container.make<AngelWebSocket>();
|
||||||
|
|
||||||
for (var client in ws.clients) {
|
for (var client in ws.clients) {
|
||||||
try {
|
try {
|
||||||
await client.close(WebSocketStatus.goingAway);
|
await client.close(WebSocketStatus.goingAway);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
stderr.writeln(
|
stderr.writeln(
|
||||||
'Couldn\'t close WebSocket from session #${client.request.session.id}: $e');
|
'Couldn\'t close WebSocket from session #${client.request.session.id}: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future.forEach(old.app.shutdownHooks, old.app.configure);
|
await Future.forEach(old.app.shutdownHooks, old.app.configure);
|
||||||
|
} catch (_) {
|
||||||
|
// Fail silently...
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_server = null;
|
_server = null;
|
||||||
var report = await _client.reloadSources(_mainIsolate.id);
|
|
||||||
|
|
||||||
if (!report.success) {
|
if (hot) {
|
||||||
stderr.writeln('Hot reload failed!!!');
|
var report = await _client.reloadSources(_mainIsolate.id);
|
||||||
stderr.writeln(report.toString());
|
|
||||||
exit(1);
|
if (!report.success) {
|
||||||
|
stderr.writeln('Hot reload failed!!!');
|
||||||
|
stderr.writeln(report.toString());
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var s = await _generateServer();
|
var s = await _generateServer();
|
||||||
|
@ -294,6 +352,7 @@ class HotReloader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
class _Debounce<S> extends StreamTransformerBase<S, S> {
|
class _Debounce<S> extends StreamTransformerBase<S, S> {
|
||||||
final Duration _delay;
|
final Duration _delay;
|
||||||
|
|
||||||
|
@ -313,3 +372,4 @@ class _Debounce<S> extends StreamTransformerBase<S, S> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
BIN
screenshots/screenshot.png
Normal file
BIN
screenshots/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
Loading…
Reference in a new issue