Skip to content

Hot reload #1786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Aug 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
73d8a1a
Add assets digests handler (#1709)
samogot Aug 7, 2018
e3a4fae
Emit useful information about build results (#1710)
samogot Aug 7, 2018
1a7b69c
Merge branch 'master' into hot-reload
samogot Aug 7, 2018
065cf24
Add travis checks to hot-reload branch and fix discovered issues
samogot Aug 7, 2018
e06dac3
Merge pull request #1731 from dart-lang/master-merge-in
samogot Aug 8, 2018
ccc52c5
Merge remote-tracking branch 'origin/master' into hot-reload
samogot Aug 9, 2018
87b316f
Merge remote-tracking branch 'origin/master' into hot-reload
samogot Aug 9, 2018
f8cd20b
Move live reload client code to new hot_reload_client.dart file (#1732)
samogot Aug 9, 2018
58ed11a
Implement basic hot reload (#1741)
samogot Aug 13, 2018
8b9e18d
Merge remote-tracking branch 'origin/master' into hot-reload
samogot Aug 13, 2018
76b8be4
Basic graph handling for hot reload (#1759)
samogot Aug 14, 2018
22f239d
Add rough graph changes handling by full page reload (#1763)
samogot Aug 15, 2018
9a33ae6
Simplify module interface (#1764)
samogot Aug 15, 2018
f883234
Merge remote-tracking branch 'origin/master' into hot-reload
samogot Aug 17, 2018
2c190f3
Merge remote-tracking branch 'origin/master' into hot-reload
samogot Aug 17, 2018
4d3c1c3
Change HMR API to work with several libraries bundled in one module (…
samogot Aug 17, 2018
a95a8d9
Close Hot Reload listeners on exit (#1775)
samogot Aug 21, 2018
9946be4
Refactor and improve HMR code (#1770)
samogot Aug 21, 2018
28be96f
Merge remote-tracking branch 'origin/master' into hot-reload
samogot Aug 21, 2018
0e9fd42
Finalize hot-reloading feature (#1773)
samogot Aug 21, 2018
f802f58
Restore --live-reload option (#1778)
samogot Aug 23, 2018
9472429
Disable travis in hot-reload branch
samogot Aug 24, 2018
8bb35e8
Final nits
samogot Aug 24, 2018
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ tool/test_all.dart
# Ignore extra files from dart2js that we don't want
build_runner/lib/src/server/graph_viz_main.dart.js.deps
build_runner/lib/src/server/graph_viz_main.dart.js.tar.gz
build_runner/lib/src/server/build_updates_client/hot_reload_client.dart.js.deps
build_runner/lib/src/server/build_updates_client/hot_reload_client.dart.js.tar.gz

# Ignore dazel generated
.dazel
Expand Down
5 changes: 5 additions & 0 deletions build_runner/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.10.2

- Added `--hot-reload` cli option and appropriate functionality.
See [hot-module-reloading](../docs/hot_module_reloading.md) for more info.

## 0.10.1+1

- Added better error handling when a socket is already in use in `serve` mode.
Expand Down
4 changes: 4 additions & 0 deletions build_runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ Some commands also have additional options:

- `--hostname`: The host to run the server on.
- `--live-reload`: Enables automatic page reloading on rebuilds.
Can't be used together with `--hot-reload`.
- `--hot-reload`: Enables automatic reloading of changed modules on rebuilds.
See [hot-module-reloading](../docs/hot_module_reloading.md) for more info.
Can't be used together with `--live-reload`.

Trailing args of the form `<directory>:<port>` are supported to customize what
directories are served, and on what ports.
Expand Down
22 changes: 19 additions & 3 deletions build_runner/lib/src/entrypoint/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const assumeTtyOption = 'assume-tty';
const defineOption = 'define';
const deleteFilesByDefaultOption = 'delete-conflicting-outputs';
const liveReloadOption = 'live-reload';
const hotReloadOption = 'hot-reload';
const logPerformanceOption = 'log-performance';
const logRequestsOption = 'log-requests';
const lowResourcesModeOption = 'low-resources-mode';
Expand All @@ -28,6 +29,8 @@ const trackPerformanceOption = 'track-performance';
const skipBuildScriptCheckOption = 'skip-build-script-check';
const symlinkOption = 'symlink';

enum BuildUpdatesOption { none, liveReload, hotReload }

final _defaultWebDirs = const ['web', 'test', 'example', 'benchmark'];

/// Base options that are shared among all commands.
Expand Down Expand Up @@ -131,13 +134,13 @@ class SharedOptions {
/// Options specific to the `serve` command.
class ServeOptions extends SharedOptions {
final String hostName;
final bool liveReload;
final BuildUpdatesOption buildUpdates;
final bool logRequests;
final List<ServeTarget> serveTargets;

ServeOptions._({
@required this.hostName,
@required this.liveReload,
@required this.buildUpdates,
@required this.logRequests,
@required this.serveTargets,
@required bool assumeTty,
Expand Down Expand Up @@ -201,9 +204,22 @@ class ServeOptions extends SharedOptions {
var buildDirs = _buildDirsFromOutputMap(outputMap)
..addAll(serveTargets.map((t) => t.dir));

BuildUpdatesOption buildUpdates;
if (argResults[liveReloadOption] as bool &&
argResults[hotReloadOption] as bool) {
throw UsageException(
'Options --$liveReloadOption and --$hotReloadOption '
"can't both be used together",
command.usage);
} else if (argResults[liveReloadOption] as bool) {
buildUpdates = BuildUpdatesOption.liveReload;
} else if (argResults[hotReloadOption] as bool) {
buildUpdates = BuildUpdatesOption.hotReload;
}

return ServeOptions._(
hostName: argResults[hostnameOption] as String,
liveReload: argResults[liveReloadOption] as bool,
buildUpdates: buildUpdates,
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 @@ -29,7 +29,13 @@ class ServeCommand extends WatchCommand {
..addFlag(liveReloadOption,
defaultsTo: false,
negatable: false,
help: 'Enables automatic page reloading on rebuilds.');
help: 'Enables automatic page reloading on rebuilds. '
"Can't be used together with --$hotReloadOption.")
..addFlag(hotReloadOption,
defaultsTo: false,
negatable: false,
help: 'Enables automatic reloading of changed modules on rebuilds. '
"Can't be used together with --$liveReloadOption.");
}

@override
Expand Down Expand Up @@ -91,7 +97,7 @@ class ServeCommand extends WatchCommand {
server,
handler.handlerFor(target.dir,
logRequests: options.logRequests,
liveReload: options.liveReload));
buildUpdates: options.buildUpdates));
});

_ensureBuildWebCompilersDependency(packageGraph, logger);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

@JS()
library hot_reload_client;

import 'dart:async';
import 'dart:convert';
import 'dart:html';

import 'package:js/js.dart';
import 'package:js/js_util.dart';

import 'module.dart';
import 'reload_handler.dart';
import 'reloading_manager.dart';

final _assetsDigestPath = r'$assetDigests';
final _buildUpdatesProtocol = r'$buildUpdates';

@anonymous
@JS()
abstract class HotReloadableLibrary {
/// Implement this function with any code to release resources before destroy.
///
/// Any object returned from this function will be passed to update hooks. Use
/// it to save any state you need to be preserved between hot reloadings.
/// Try do not use any custom types here, as it might prevent their code from
/// reloading. Better serialise to JSON or plain types.
///
/// This function will be called on old version of module before unloading.
@JS()
external Object hot$onDestroy();

/// Implement this function to handle update of the module itself.
///
/// May return nullable bool. To indicate that reload completes successfully
/// return true. To indicate that hot-reload is undoable return false - this
/// will lead to full page reload. If null returned, reloading will be
/// propagated to parent.
///
/// If any state was saved from previous version, it will be passed to [data].
///
/// This function will be called on new version of module after reloading.
@JS()
external bool hot$onSelfUpdate([Object data]);

/// Implement this function to handle update of child modules.
///
/// May return nullable bool. To indicate that reload of child completes
/// successfully return true. To indicate that hot-reload is undoable for this
/// child return false - this will lead to full page reload. If null returned,
/// reloading will be propagated to current module itself.
///
/// The name of the child will be provided in [childId]. New version of child
/// module object will be provided in [child].
/// If any state was saved from previous version, it will be passed to [data].
///
/// This function will be called on old version of module current after child
/// reloading.
@JS()
external bool hot$onChildUpdate(String childId, HotReloadableLibrary child,
[Object data]);
}

class LibraryWrapper implements Library {
final HotReloadableLibrary _internal;

LibraryWrapper(this._internal);

@override
Object onDestroy() {
if (_internal != null && hasProperty(_internal, r'hot$onDestroy')) {
return _internal.hot$onDestroy();
}
return null;
}

@override
bool onSelfUpdate([Object data]) {
if (_internal != null && hasProperty(_internal, r'hot$onSelfUpdate')) {
return _internal.hot$onSelfUpdate(data);
}
// ignore: avoid_returning_null
return null;
}

@override
bool onChildUpdate(String childId, Library child, [Object data]) {
if (_internal != null && hasProperty(_internal, r'hot$onChildUpdate')) {
return _internal.hot$onChildUpdate(
childId, (child as LibraryWrapper)._internal, data);
}
// ignore: avoid_returning_null
return null;
}
}

@JS('Map')
abstract class JsMap<K, V> {
@JS()
external Object keys();

@JS()
external V get(K key);
}

@JS('Error')
abstract class JsError {
@JS()
external String get message;

@JS()
external String get stack;
}

@anonymous
@JS()
class DartLoader {
@JS()
external JsMap<String, String> get urlToModuleId;

@JS()
external JsMap<String, List<String>> get moduleParentsGraph;

@JS()
external void forceLoadModule(String moduleId, void Function() callback,
void Function(JsError e) onError);

@JS()
external Object getModuleLibraries(String moduleId);
}

@JS(r'$dartLoader')
external DartLoader get dartLoader;

@JS('Array.from')
external List _jsArrayFrom(Object any);

@JS('Object.keys')
external List _jsObjectKeys(Object any);

@JS('Object.values')
external List _jsObjectValues(Object any);

List<K> keys<K, V>(JsMap<K, V> map) {
return List.from(_jsArrayFrom(map.keys()));
}

Module _moduleLibraries(String moduleId) {
var moduleObj = dartLoader.getModuleLibraries(moduleId);
if (moduleObj == null) {
throw HotReloadFailedException("Failed to get module '$moduleId'. "
"This error might appear if such module doesn't exist or isn't already loaded");
}
var moduleKeys = List<String>.from(_jsObjectKeys(moduleObj));
var moduleValues =
List<HotReloadableLibrary>.from(_jsObjectValues(moduleObj));
var moduleLibraries = moduleValues.map((x) => LibraryWrapper(x));
return Module(Map.fromIterables(moduleKeys, moduleLibraries));
}

Future<Module> _reloadModule(String moduleId) {
var completer = Completer<Module>();
var stackTrace = StackTrace.current;
dartLoader.forceLoadModule(moduleId, allowInterop(() {
completer.complete(_moduleLibraries(moduleId));
}),
allowInterop((e) => completer.completeError(
HotReloadFailedException(e.message), stackTrace)));
return completer.future;
}

void _reloadPage() {
window.location.reload();
}

main() async {
var currentOrigin = '${window.location.origin}/';
var modulePaths = keys(dartLoader.urlToModuleId)
.map((key) => key.replaceFirst(currentOrigin, ''))
.toList();
var modulePathsJson = json.encode(modulePaths);

var request = await HttpRequest.request('/$_assetsDigestPath',
responseType: 'json', sendData: modulePathsJson, method: 'POST');
var digests = (request.response as Map).cast<String, String>();

var manager = ReloadingManager(
_reloadModule,
_moduleLibraries,
_reloadPage,
(module) => dartLoader.moduleParentsGraph.get(module),
() => keys(dartLoader.moduleParentsGraph));

var handler = ReloadHandler(digests,
(path) => dartLoader.urlToModuleId.get(currentOrigin + path), manager);

var webSocket =
WebSocket('ws://${window.location.host}', [_buildUpdatesProtocol]);
webSocket.onMessage.listen((event) => handler.listener(event.data as String));
}
Loading