Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build_runner/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.10.1-dev

- Added `--live-reload` cli option and appropriate functionality

## 0.10.0

### Breaking Changes
Expand Down
1 change: 1 addition & 0 deletions build_runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Some commands also have additional options:
##### serve

- `--hostname`: The host to run the server on.
- `--live-reload`: Enables automatic page reloading on rebuilds.

Trailing args of the form `<directory>:<port>` are supported to customize what
directories are served, and on what ports.
Expand Down
4 changes: 4 additions & 0 deletions build_runner/lib/src/entrypoint/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:path/path.dart' as p;
const assumeTtyOption = 'assume-tty';
const defineOption = 'define';
const deleteFilesByDefaultOption = 'delete-conflicting-outputs';
const liveReloadOption = 'live-reload';
const logPerformanceOption = 'log-performance';
const logRequestsOption = 'log-requests';
const lowResourcesModeOption = 'low-resources-mode';
Expand Down Expand Up @@ -130,11 +131,13 @@ class SharedOptions {
/// Options specific to the `serve` command.
class ServeOptions extends SharedOptions {
final String hostName;
final bool liveReload;
final bool logRequests;
final List<ServeTarget> serveTargets;

ServeOptions._({
@required this.hostName,
@required this.liveReload,
@required this.logRequests,
@required this.serveTargets,
@required bool assumeTty,
Expand Down Expand Up @@ -200,6 +203,7 @@ class ServeOptions extends SharedOptions {

return new ServeOptions._(
hostName: argResults[hostnameOption] as String,
liveReload: argResults[liveReloadOption] as bool,
logRequests: argResults[logRequestsOption] as bool,
serveTargets: serveTargets,
assumeTty: argResults[assumeTtyOption] as bool,
Expand Down
10 changes: 8 additions & 2 deletions build_runner/lib/src/entrypoint/serve.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ class ServeCommand extends WatchCommand {
..addFlag(logRequestsOption,
defaultsTo: false,
negatable: false,
help: 'Enables logging for each request to the server.');
help: 'Enables logging for each request to the server.')
..addFlag(liveReloadOption,
defaultsTo: false,
negatable: false,
help: 'Enables automatic page reloading on rebuilds.');
}

@override
Expand Down Expand Up @@ -93,7 +97,9 @@ Future<HttpServer> _startServer(
ServeOptions options, ServeTarget target, ServeHandler handler) async {
var server = await _bindServer(options, target);
serveRequests(
server, handler.handlerFor(target.dir, logRequests: options.logRequests));
server,
handler.handlerFor(target.dir,
logRequests: options.logRequests, liveReload: options.liveReload));
return server;
}

Expand Down
95 changes: 87 additions & 8 deletions build_runner/lib/src/server/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ import 'package:logging/logging.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

import '../generate/watch_impl.dart';
import 'asset_graph_handler.dart';
import 'path_to_asset_id.dart';

const _performancePath = r'$perf';
final _graphPath = r'$graph';
final _buildUpdatesProtocol = r'$livereload';
final _buildUpdatesMessage = 'update';
final entrypointExtensionMarker = '/* ENTRYPOINT_EXTENTION_MARKER */';

final _logger = new Logger('Serve');

Expand All @@ -43,10 +48,13 @@ class ServeHandler implements BuildState {
final Future<AssetHandler> _assetHandler;
final Future<AssetGraphHandler> _assetGraphHandler;

final _webSocketHandler = BuildUpdatesWebSocketHandler();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're getting a little inconsistent about new (easy to do since the Analyzer no longer requires it)

@jakemac53 - should we dartfmt --fix and be done with it or should we find all of these and add back in the new?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am in favor of just doing a dartfmt --fix, but can we also enable a lint then to make us not introduce any more new keywords?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we could add unnecessary_new and unnecessary_const to avoid regressions.


ServeHandler._(this._state, this._assetHandler, this._assetGraphHandler,
this._rootPackage) {
_state.buildResults.listen((result) {
_lastBuildResult = result;
_webSocketHandler.emitUpdateMessage(result);
});
}

Expand All @@ -55,14 +63,17 @@ class ServeHandler implements BuildState {
@override
Stream<BuildResult> get buildResults => _state.buildResults;

shelf.Handler handlerFor(String rootDir, {bool logRequests}) {
shelf.Handler handlerFor(String rootDir,
{bool logRequests, bool liveReload}) {
liveReload ??= false;
logRequests ??= false;
if (p.url.split(rootDir).length != 1) {
throw new ArgumentError.value(
rootDir, 'rootDir', 'Only top level directories are supported');
}
_state.currentBuild.then((_) => _warnForEmptyDirectory(rootDir));
var cascade = new shelf.Cascade()
var cascade = new shelf.Cascade();
cascade = (liveReload ? cascade.add(_webSocketHandler.handler) : cascade)
.add(_blockOnCurrentBuild)
.add((shelf.Request request) async {
if (request.url.path == _performancePath) {
Expand All @@ -76,12 +87,14 @@ class ServeHandler implements BuildState {
var assetHandler = await _assetHandler;
return assetHandler.handle(request, rootDir);
});
var handler = logRequests
? const shelf.Pipeline()
.addMiddleware(_logRequests)
.addHandler(cascade.handler)
: cascade.handler;
return handler;
var pipeline = shelf.Pipeline();
if (logRequests) {
pipeline = pipeline.addMiddleware(_logRequests);
}
if (liveReload) {
pipeline = pipeline.addMiddleware(_injectBuildUpdatesClientCode);
}
return pipeline.addHandler(cascade.handler);
}

Future<shelf.Response> _blockOnCurrentBuild(_) async {
Expand Down Expand Up @@ -110,6 +123,72 @@ class ServeHandler implements BuildState {
}
}

/// Class that manages web socket connection handler to inform clients about
/// build updates
class BuildUpdatesWebSocketHandler {
final activeConnections = <WebSocketChannel>[];
final shelf.Handler Function(Function, {Iterable<String> protocols})
_handlerFactory;
shelf.Handler _internalHandler;

BuildUpdatesWebSocketHandler([this._handlerFactory = webSocketHandler]) {
// Because of dart-lang/shelf_web_socket#11, webSocketHandler doesn't work
// with strongly typed functions. As a workaround diskard type information
// wrapping it with lambda for now
var untypedTearOff = (webSocket, protocol) =>
_handleConnection(webSocket as WebSocketChannel, protocol as String);
_internalHandler =
_handlerFactory(untypedTearOff, protocols: [_buildUpdatesProtocol]);
}

shelf.Handler get handler => _internalHandler;

void emitUpdateMessage(BuildResult buildResult) {
for (var webSocket in activeConnections) {
webSocket.sink.add(_buildUpdatesMessage);
}
}

void _handleConnection(WebSocketChannel webSocket, String protocol) async {
activeConnections.add(webSocket);
await webSocket.stream.drain();
activeConnections.remove(webSocket);
}
}

shelf.Handler _injectBuildUpdatesClientCode(shelf.Handler innerHandler) {
return (shelf.Request request) async {
if (!request.url.path.endsWith('.js')) {
return innerHandler(request);
}
var response = await innerHandler(request);
// TODO: Find a way how to check and/or modify body without reading it whole
var body = await response.readAsString();
if (body.startsWith(entrypointExtensionMarker)) {
body += _buildUpdatesInjectedJS;
}
return response.change(body: body);
};
}

/// Hot-reload config
///
/// Listen WebSocket for updates in build results
///
/// Now only live-reload functional - just reload page on update message
final _buildUpdatesInjectedJS = '''\n
// Injected by build_runner for live reload support
(function() {
var ws = new WebSocket('ws://' + location.host, ['$_buildUpdatesProtocol']);
ws.onmessage = function(event) {
console.log(event);
if(event.data === '$_buildUpdatesMessage'){
location.reload();
}
};
}());
''';

class AssetHandler {
final FinalizedReader _reader;
final String _rootPackage;
Expand Down
4 changes: 3 additions & 1 deletion build_runner/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: build_runner
version: 0.10.0
version: 0.10.1-dev
description: Tools to write binaries that run builders.
author: Dart Team <[email protected]>
homepage: https://github.com/dart-lang/build/tree/master/build_runner
Expand Down Expand Up @@ -34,9 +34,11 @@ dependencies:
pub_semver: ^1.4.0
pubspec_parse: ^0.1.0
shelf: ">=0.6.5 <0.8.0"
shelf_web_socket: ^0.2.2+3
stack_trace: ^1.9.0
stream_transform: ^0.0.9
watcher: ^0.9.7
web_socket_channel: ^1.0.9
yaml: ^2.1.0

dev_dependencies:
Expand Down
133 changes: 133 additions & 0 deletions build_runner/test/server/serve_handler_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:io';

import 'package:logging/logging.dart';
import 'package:shelf/shelf.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart';

import 'package:build_runner/build_runner.dart';
Expand All @@ -20,6 +21,7 @@ import 'package:build_runner/src/server/server.dart';

import 'package:_test_common/common.dart';
import 'package:_test_common/package_graphs.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

void main() {
ServeHandler serveHandler;
Expand Down Expand Up @@ -162,6 +164,137 @@ void main() {
expect(await response.readAsString(), contains('--track-performance'));
});
});

group('build updates', () {
test('injects client code if enabled', () async {
_addSource('a|web/some.js', entrypointExtensionMarker + '\nalert(1)');
var response = await serveHandler.handlerFor('web', liveReload: true)(
new Request('GET', Uri.parse('http://server.com/some.js')));
expect(await response.readAsString(), contains('\$livereload'));
});

test('doesn\'t inject client code if disabled', () async {
_addSource('a|web/some.js', entrypointExtensionMarker + '\nalert(1)');
var response = await serveHandler.handlerFor('web', liveReload: false)(
new Request('GET', Uri.parse('http://server.com/some.js')));
expect(await response.readAsString(), isNot(contains('\$livereload')));
});

test('doesn\'t inject client code in non-js files', () async {
_addSource('a|web/some.html', entrypointExtensionMarker + '\n<br>some');
var response = await serveHandler.handlerFor('web', liveReload: true)(
new Request('GET', Uri.parse('http://server.com/some.html')));
expect(await response.readAsString(), isNot(contains('\$livereload')));
});

test('doesn\'t inject client code in non-marked files', () async {
_addSource('a|web/some.js', 'alert(1)');
var response = await serveHandler.handlerFor('web', liveReload: true)(
new Request('GET', Uri.parse('http://server.com/some.js')));
expect(await response.readAsString(), isNot(contains('\$livereload')));
});

test('expect websocket connection if enabled', () async {
_addSource('a|web/index.html', 'content');
expect(
serveHandler.handlerFor('web', liveReload: true)(
new Request('GET', Uri.parse('ws://server.com/'),
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Key': 'abc',
},
onHijack: (f) {})),
throwsA(TypeMatcher<HijackException>()));
});

test('reject websocket connection if disabled', () async {
_addSource('a|web/index.html', 'content');
var response = await serveHandler.handlerFor('web', liveReload: false)(
new Request('GET', Uri.parse('ws://server.com/'), headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Key': 'abc',
}));
expect(response.statusCode, 200);
expect(await response.readAsString(), 'content');
});

group('WebSocket handler', () {
BuildUpdatesWebSocketHandler buildUpdatesWebSocketHandler;
Function createMockConection;

// client to server stream controlllers
StreamController<List<int>> c2sController1;
StreamController<List<int>> c2sController2;
// server to client stream controlllers
StreamController<List<int>> s2cController1;
StreamController<List<int>> s2cController2;

WebSocketChannel clientChannel1;
WebSocketChannel clientChannel2;
WebSocketChannel serverChannel1;
WebSocketChannel serverChannel2;

setUp(() {
var mockHandlerFactory = (Function onConnect, {protocols}) {
createMockConection =
(WebSocketChannel serverChannel) => onConnect(serverChannel, '');
};
buildUpdatesWebSocketHandler =
BuildUpdatesWebSocketHandler(mockHandlerFactory);

c2sController1 = StreamController<List<int>>();
s2cController1 = StreamController<List<int>>();
serverChannel1 = WebSocketChannel(
StreamChannel(c2sController1.stream, s2cController1.sink),
serverSide: true);
clientChannel1 = WebSocketChannel(
StreamChannel(s2cController1.stream, c2sController1.sink),
serverSide: false);

c2sController2 = StreamController<List<int>>();
s2cController2 = StreamController<List<int>>();
serverChannel2 = WebSocketChannel(
StreamChannel(c2sController2.stream, s2cController2.sink),
serverSide: true);
clientChannel2 = WebSocketChannel(
StreamChannel(s2cController2.stream, c2sController2.sink),
serverSide: false);
});

tearDown(() {
c2sController1.close();
s2cController1.close();
c2sController2.close();
s2cController2.close();
});

test('emmits a message to all listners', () async {
expect(clientChannel1.stream, emitsInOrder(['update', emitsDone]));
expect(clientChannel2.stream, emitsInOrder(['update', emitsDone]));
createMockConection(serverChannel1);
createMockConection(serverChannel2);
buildUpdatesWebSocketHandler.emitUpdateMessage(null);
await clientChannel1.sink.close();
await clientChannel2.sink.close();
});

test('deletes listners on disconect', () async {
expect(clientChannel1.stream,
emitsInOrder(['update', 'update', emitsDone]));
expect(clientChannel2.stream, emitsInOrder(['update', emitsDone]));
createMockConection(serverChannel1);
createMockConection(serverChannel2);
buildUpdatesWebSocketHandler.emitUpdateMessage(null);
await clientChannel2.sink.close();
buildUpdatesWebSocketHandler.emitUpdateMessage(null);
await clientChannel1.sink.close();
});
});
});
}

class MockWatchImpl implements WatchImpl {
Expand Down
5 changes: 5 additions & 0 deletions build_web_compilers/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.4.2

- Add magic comment marker for build_runner to know where to inject
live-reloading client code.

## 0.4.1

- Support the latest build_modules, with updated dart2js support so that it can
Expand Down
Loading