First RC.
This commit is contained in:
parent
a0f7cf671b
commit
cc759d5268
11 changed files with 434 additions and 1 deletions
2
.analysis-options
Normal file
2
.analysis-options
Normal file
|
@ -0,0 +1,2 @@
|
|||
analyzer:
|
||||
strong-mode: true
|
44
.gitignore
vendored
44
.gitignore
vendored
|
@ -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
20
.idea/hot.iml
Normal 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
8
.idea/modules.xml
Normal 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>
|
8
.idea/runConfigurations/server_dart.xml
Normal file
8
.idea/runConfigurations/server_dart.xml
Normal 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
6
.idea/vcs.xml
Normal 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>
|
80
README.md
80
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<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
37
example/basic/server.dart
Normal 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;
|
||||
}
|
9
example/basic/src/foo.dart
Normal file
9
example/basic/src/foo.dart
Normal 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
201
lib/angel_hot.dart
Normal 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
20
pubspec.yaml
Normal 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
|
Loading…
Reference in a new issue