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
|
# Directory created by dartdoc
|
||||||
# If you don't generate documentation locally you can remove this line.
|
# If you don't generate documentation locally you can remove this line.
|
||||||
doc/api/
|
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
|
# 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