diff --git a/.analysis-options b/.analysis-options
new file mode 100644
index 00000000..518eb901
--- /dev/null
+++ b/.analysis-options
@@ -0,0 +1,2 @@
+analyzer:
+ strong-mode: true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 4d2a4d6d..99e7978e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,47 @@ pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
diff --git a/.idea/hot.iml b/.idea/hot.iml
new file mode 100644
index 00000000..466ce67a
--- /dev/null
+++ b/.idea/hot.iml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..81b04c42
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/server_dart.xml b/.idea/runConfigurations/server_dart.xml
new file mode 100644
index 00000000..84a78f11
--- /dev/null
+++ b/.idea/runConfigurations/server_dart.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 60b6883b..856205e2 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,80 @@
# hot
-Supports hot reloading of Angel servers on file changes.
+[![version 1.0.0-rc.1](https://img.shields.io/badge/pub-1.0.0--rc.1-brightgreen.svg)](https://pub.dartlang.org/packages/angel_rethink)
+
+Supports *hot reloading* of Angel servers on file changes. This is faster and
+more reliable than merely reactively restarting a `Process`.
+
+This package only works with the [Angel framework](https://github.com/angel-dart/angel).
+
+# Installation
+In your `pubspec.yaml`:
+
+```yaml
+dependencies:
+ angel_framework: ^1.0.0
+ angel_hot: ^1.0.0
+```
+
+# Usage
+This package is dependent on the Dart VM service, so you *must* run
+Dart with the `--enable-vm-service` argument!!!
+
+Usage is fairly simple. Pass a function that creates an `Angel` server, along with a collection of paths
+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
+within a separate function, conventionally named `createServer`. Using this in production mode is pointless.
+
+You can watch:
+ * Files
+ * Directories
+ * Globs
+ * URI's
+ * `package:` URI's
+
+```dart
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_compress/angel_compress.dart';
+import 'package:angel_diagnostics/angel_diagnostics.dart';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_hot/angel_hot.dart';
+import 'src/foo.dart';
+
+main() async {
+ var hot = new HotReloader(createServer, [
+ new Directory('config'),
+ new Directory('lib'),
+ new Directory('web'),
+ new Directory('src'),
+ 'bin/server.dart',
+ Uri.parse('some_file.dart'),
+ Uri.parse('package:angel_hot/angel_hot.dart')
+ ]);
+
+ var server = await hot.startServer(InternetAddress.LOOPBACK_IP_V4, 3000);
+ print(
+ 'Hot server listening at http://${server.address.address}:${server.port}');
+}
+
+Future createServer() async {
+ var app = new Angel();
+
+ app.lazyParseBodies = true;
+ app.injectSerializer(JSON.encode);
+
+ app.get('/', {'hello': 'hot world!'});
+
+ app.post('/foo/bar', (req, res) async {
+ var result = await someLengthyOperation();
+ return {'status': result};
+ });
+
+ app.after.add(() => throw new AngelHttpException.notFound());
+
+ app.responseFinalizers.add(gzip());
+ await app.configure(logRequests());
+ return app;
+}
+```
\ No newline at end of file
diff --git a/example/basic/server.dart b/example/basic/server.dart
new file mode 100644
index 00000000..3c550213
--- /dev/null
+++ b/example/basic/server.dart
@@ -0,0 +1,37 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_compress/angel_compress.dart';
+import 'package:angel_diagnostics/angel_diagnostics.dart';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_hot/angel_hot.dart';
+import 'src/foo.dart';
+
+main() async {
+ var hot = new HotReloader(createServer, [
+ new Directory('src'),
+ 'server.dart',
+ Uri.parse('package:angel_hot/angel_hot.dart')
+ ]);
+ var server = await hot.startServer(InternetAddress.LOOPBACK_IP_V4, 3000);
+ print(
+ 'Hot server listening at http://${server.address.address}:${server.port}');
+}
+
+Future createServer() async {
+ // Max speed???
+ var app = new Angel();
+
+ app.lazyParseBodies = true;
+ app.injectSerializer(JSON.encode);
+
+ app.get('/', {'hello': 'hot world!'});
+ app.get('/foo', new Foo(bar: 'baz'));
+
+ app.after.add(() => throw new AngelHttpException.notFound());
+
+ app.responseFinalizers.add(gzip());
+ //await app.configure(cacheResponses());
+ await app.configure(logRequests());
+ return app;
+}
diff --git a/example/basic/src/foo.dart b/example/basic/src/foo.dart
new file mode 100644
index 00000000..7d584344
--- /dev/null
+++ b/example/basic/src/foo.dart
@@ -0,0 +1,9 @@
+class Foo {
+ final String bar;
+
+ Foo({this.bar});
+
+ Map toJson() {
+ return {'bar': bar};
+ }
+}
diff --git a/lib/angel_hot.dart b/lib/angel_hot.dart
new file mode 100644
index 00000000..a5036a87
--- /dev/null
+++ b/lib/angel_hot.dart
@@ -0,0 +1,201 @@
+import 'dart:async';
+import 'dart:collection';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:isolate';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:glob/glob.dart';
+import 'package:html_builder/elements.dart';
+import 'package:html_builder/html_builder.dart';
+import 'package:vm_service_client/vm_service_client.dart';
+import 'package:watcher/watcher.dart';
+
+/// A typedef over a function that returns a fresh [Angel] instance, whether synchronously or asynchronously.
+typedef FutureOr AngelGenerator();
+
+class HotReloader {
+ VMServiceClient _client;
+ final List _paths = [];
+ final StringRenderer _renderer = new StringRenderer(pretty: false);
+ final Queue _requestQueue = new Queue();
+ Angel _server;
+ Duration _timeout;
+
+ /// Invoked to load a new instance of [Angel] on file changes.
+ final AngelGenerator generator;
+
+ /// The maximum amount of time to queue incoming requests for if there is no [server] available.
+ ///
+ /// If the timeout expires, then the request will be immediately terminated with a `502 Bad Gateway` error.
+ /// Default: `5s`
+ Duration get timeout => _timeout;
+
+ /// A URL pointing to the Dart VM service.
+ ///
+ /// Default: `ws://localhost:8181/ws`.
+ final String vmServiceUrl;
+
+ /// Initializes a hot reloader that proxies the server created by [generator].
+ ///
+ /// [paths] can contain [FileSystemEntity], [Uri], [String] and [Glob] only.
+ /// URI's can be `package:` URI's as well.
+ HotReloader(this.generator, Iterable paths,
+ {Duration timeout, this.vmServiceUrl: 'ws://localhost:8181/ws'}) {
+ _timeout = timeout ?? new Duration(seconds: 5);
+ _paths.addAll(paths ?? []);
+ }
+
+ Future handleRequest(HttpRequest request) async {
+ if (_server != null)
+ return await _server.handleRequest(request);
+ else if (timeout == null)
+ _requestQueue.add(request);
+ else {
+ _requestQueue.add(request);
+ new Timer(timeout, () {
+ if (_requestQueue.remove(request)) {
+ // Send 502 response
+ var doc = html(lang: 'en', c: [
+ head(c: [
+ meta(
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1'),
+ title(c: [text('502 Bad Gateway')])
+ ]),
+ body(c: [
+ h1(c: [text('502 Bad Gateway')]),
+ i(c: [
+ text('Request timed out after ${timeout.inMilliseconds}ms.')
+ ])
+ ])
+ ]);
+
+ var response = request.response;
+ response.statusCode = HttpStatus.BAD_GATEWAY;
+ response.headers
+ ..contentType = ContentType.HTML
+ ..set(HttpHeaders.SERVER, 'angel_hot');
+
+ if (request.headers
+ .value(HttpHeaders.ACCEPT_ENCODING)
+ ?.toLowerCase()
+ ?.contains('gzip') ==
+ true) {
+ response
+ ..headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip')
+ ..add(GZIP.encode(UTF8.encode(_renderer.render(doc))));
+ } else
+ response.write(_renderer.render(doc));
+ response.close();
+ }
+ });
+ }
+ }
+
+ Future _generateServer() async {
+ var s = await generator() as Angel;
+ await Future.forEach(s.justBeforeStart, s.configure);
+ s.optimizeForProduction();
+ return s;
+ }
+
+ /// Starts listening to requests and filesystem events.
+ Future startServer([address, int port]) async {
+ if (_paths?.isNotEmpty != true)
+ print(
+ 'WARNING: You have instantiated a HotReloader without providing any filesystem paths to watch.');
+
+ var s = _server = await _generateServer();
+ while (!_requestQueue.isEmpty)
+ await s.handleRequest(_requestQueue.removeFirst());
+ await _listenToFilesystem();
+
+ var server = await HttpServer.bind(
+ address ?? InternetAddress.LOOPBACK_IP_V4, port ?? 0);
+ server.listen(handleRequest);
+ return server;
+ }
+
+ _listenToFilesystem() async {
+ for (var path in _paths) {
+ if (path is String) {
+ await _listenToStat(path);
+ } else if (path is Glob) {
+ await for (var entity in path.list()) {
+ await _listenToStat(entity.path);
+ }
+ } else if (path is FileSystemEntity) {
+ await _listenToStat(path.path);
+ } else if (path is Uri) {
+ if (path.scheme == 'package') {
+ var uri = await Isolate.resolvePackageUri(path);
+ await _listenToStat(uri.toFilePath());
+ } else
+ await _listenToStat(path.toFilePath());
+ } else {
+ throw new ArgumentError(
+ 'Hot reload paths must be a FileSystemEntity, a Uri, a String or a Glob. You provided: $path');
+ }
+ }
+ }
+
+ _listenToStat(String path) async {
+ _listen() async {
+ try {
+ var stat = await FileStat.stat(path);
+ if (stat.type == FileSystemEntityType.LINK) {
+ var lnk = new Link(path);
+ var p = await lnk.resolveSymbolicLinks();
+ return await _listenToStat(p);
+ } else if (stat.type == FileSystemEntityType.FILE) {
+ var file = new File(path);
+ if (!await file.exists()) return null;
+ } else if (stat.type == FileSystemEntityType.DIRECTORY) {
+ var dir = new Directory(path);
+ if (!await dir.exists()) return null;
+ } else
+ return null;
+
+ var watcher = new Watcher(path);
+ //await watcher.ready;
+ watcher.events.listen(_handleWatchEvent);
+ print('Listening for file changes at ${path}...');
+ return true;
+ } catch (e) {
+ if (e is! FileSystemException) rethrow;
+ }
+ }
+
+ var r = await _listen();
+
+ if (r == null) {
+ print(
+ 'WARNING: Unable to watch path "$path" from working directory "${Directory.current.path}". Please ensure that it exists.');
+ }
+ }
+
+ _handleWatchEvent(WatchEvent e) async {
+ print('${e.path} changed. Reloading server...');
+ var old = _server;
+ _server = null;
+ Future.forEach(old.justBeforeStop, old.configure);
+
+ _client ??=
+ new VMServiceClient.connect(vmServiceUrl ?? 'ws://localhost:8181/ws');
+ var vm = await _client.getVM();
+ var mainIsolate = vm.isolates.first;
+ var runnable = await mainIsolate.loadRunnable();
+ var report = await runnable.reloadSources();
+
+ if (!report.status) {
+ stderr.writeln('Hot reload failed!!!');
+ stderr.writeln(report.message);
+ exit(1);
+ }
+
+ var s = await _generateServer();
+ _server = s;
+ while (!_requestQueue.isEmpty)
+ await s.handleRequest(_requestQueue.removeFirst());
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 00000000..6173fead
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,20 @@
+name: angel_hot
+description: Supports hot reloading of Angel servers on file changes.
+version: 1.0.0-rc.1
+author: Tobe O
+homepage: https://github.com/angel-dart/hot
+environment:
+ sdk: ">=1.19.0"
+dependencies:
+ angel_framework: ^1.0.0-dev
+ html_builder: ^1.0.0
+ vm_service_client:
+ git:
+ url: git://github.com/BlackHC/vm_service_client.git
+ ref: reload_sources_poc
+dev_dependencies:
+ angel_compress: ^1.0.0
+ angel_diagnostics: ^1.0.0
+ angel_test: ^1.0.0
+ http: ^0.11.3
+ test: ^0.12.15
\ No newline at end of file