First RC.

This commit is contained in:
thosakwe 2017-06-06 08:07:59 -04:00
parent a0f7cf671b
commit cc759d5268
11 changed files with 434 additions and 1 deletions

2
.analysis-options Normal file
View file

@ -0,0 +1,2 @@
analyzer:
strong-mode: true

44
.gitignore vendored
View file

@ -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

20
.idea/hot.iml Normal file
View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/example/basic/packages" />
<excludeFolder url="file://$MODULE_DIR$/example/basic/src/packages" />
<excludeFolder url="file://$MODULE_DIR$/example/packages" />
<excludeFolder url="file://$MODULE_DIR$/packages" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

8
.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/hot.iml" filepath="$PROJECT_DIR$/.idea/hot.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="server.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="VMOptions" value="--enable-vm-service" />
<option name="filePath" value="$PROJECT_DIR$/example/basic/server.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/example/basic" />
<method />
</configuration>
</component>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -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<Angel> 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;
}
```

37
example/basic/server.dart Normal file
View file

@ -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<Angel> 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;
}

View file

@ -0,0 +1,9 @@
class Foo {
final String bar;
Foo({this.bar});
Map<String, dynamic> toJson() {
return {'bar': bar};
}
}

201
lib/angel_hot.dart Normal file
View file

@ -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<Angel> AngelGenerator();
class HotReloader {
VMServiceClient _client;
final List _paths = [];
final StringRenderer _renderer = new StringRenderer(pretty: false);
final Queue<HttpRequest> _requestQueue = new Queue<HttpRequest>();
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<Angel> _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<HttpServer> 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());
}
}

20
pubspec.yaml Normal file
View file

@ -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 <thosakwe@gmail.com>
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