Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(493)

Unified Diff: sdk/lib/_internal/pub/lib/src/command/serve.dart

Issue 22986002: Add file watching to pub serve. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Move some code around. Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | sdk/lib/_internal/pub/lib/src/pub_package_provider.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: sdk/lib/_internal/pub/lib/src/command/serve.dart
diff --git a/sdk/lib/_internal/pub/lib/src/command/serve.dart b/sdk/lib/_internal/pub/lib/src/command/serve.dart
index 69d92a1f6085e33b1ca9a41004e0b2438f2dfe4d..49dc4a1abcf394d51f321242b0fe9abe352bcfe9 100644
--- a/sdk/lib/_internal/pub/lib/src/command/serve.dart
+++ b/sdk/lib/_internal/pub/lib/src/command/serve.dart
@@ -9,10 +9,12 @@ import 'dart:io';
import 'package:barback/barback.dart';
import 'package:path/path.dart' as path;
+import 'package:watcher/watcher.dart';
import '../command.dart';
import '../entrypoint.dart';
import '../exit_codes.dart' as exit_codes;
+import '../io.dart';
import '../log.dart' as log;
import '../pub_package_provider.dart';
import '../utils.dart';
@@ -26,111 +28,110 @@ class ServeCommand extends PubCommand {
String get description => "Run a local web development server.";
String get usage => 'pub serve';
+ PubPackageProvider _provider;
+ Barback _barback;
+
+ /// The completer for the top-level future returned by the command.
+ ///
+ /// Only used to keep pub running (by not completing) and to pipe fatal
+ /// errors to pub's top-level error-handling machinery.
+ final _commandCompleter = new Completer();
+
ServeCommand() {
commandParser.addOption('port', defaultsTo: '8080',
help: 'The port to listen on.');
}
Future onRun() {
- // The completer for the top-level future returned by the command. Only
- // used to keep pub running (by not completing) and to pipe fatal errors
- // to pub's top-level error-handling machinery.
- var completer = new Completer();
+ var port = parsePort();
- return new Future.value().then((_) {
- // The server relies on an up-to-date lockfile, so install first if
- // needed.
- if (!entrypoint.isLockFileUpToDate()) {
- log.message("Dependencies have changed, installing...");
- return entrypoint.installDependencies().then((_) {
- log.message("Dependencies installed!");
- });
- }
- }).then((_) {
+ return ensureLockFileIsUpToDate().then((_) {
return PubPackageProvider.create(entrypoint);
}).then((provider) {
- var port;
- try {
- port = int.parse(commandOptions['port']);
- } on FormatException catch(_) {
- log.error('Could not parse port "${commandOptions['port']}"');
- this.printUsage();
- exit(exit_codes.USAGE);
- }
+ _provider = provider;
- var barback = new Barback(provider);
+ initBarback();
- barback.results.listen((result) {
- if (result.succeeded) {
- // TODO(rnystrom): Report using growl/inotify-send where available.
- log.message("Build completed ${_green}successfully$_none");
- } else {
- log.message("Build completed with "
- "${_red}${result.errors.length}$_none errors.");
- }
- });
+ HttpServer.bind("localhost", port).then((server) {
+ watchSources();
- barback.errors.listen((error) {
- log.error("${_red}Build error:\n$error$_none");
+ log.message("Serving ${entrypoint.root.name} "
+ "on http://localhost:${server.port}");
+
+ server.listen(handleRequest);
});
- // TODO(rnystrom): Watch file system and update sources again when they
- // are added or modified.
+ return _commandCompleter.future;
+ });
+ }
- HttpServer.bind("localhost", port).then((server) {
- // Add all of the visible files.
- for (var package in provider.packages) {
- barback.updateSources(provider.listAssets(package));
- }
+ /// Parses the `--port` command-line argument and exits if it isn't valid.
+ int parsePort() {
+ try {
+ return int.parse(commandOptions['port']);
+ } on FormatException catch(_) {
+ log.error('Could not parse port "${commandOptions['port']}"');
+ this.printUsage();
+ exit(exit_codes.USAGE);
+ }
+ }
- log.message("Serving ${entrypoint.root.name} "
- "on http://localhost:${server.port}");
+ /// Installs dependencies is the lockfile is out of date with respect to the
+ /// pubspec.
+ Future ensureLockFileIsUpToDate() {
+ return new Future.sync(() {
+ // The server relies on an up-to-date lockfile, so install first if
+ // needed.
+ if (!entrypoint.isLockFileUpToDate()) {
+ log.message("Dependencies have changed, installing...");
+ return entrypoint.installDependencies().then((_) {
+ log.message("Dependencies installed!");
+ });
+ }
+ });
+ }
- server.listen((request) {
- var id = getIdFromUri(request.uri);
- if (id == null) {
- return notFound(request, "Path ${request.uri.path} is not valid.");
- }
+ void handleRequest(HttpRequest request) {
+ var id = getIdFromUri(request.uri);
+ if (id == null) {
+ notFound(request, "Path ${request.uri.path} is not valid.");
+ return;
+ }
- barback.getAssetById(id).then((asset) {
- return validateStream(asset.read()).then((stream) {
- log.message(
- "$_green${request.method}$_none ${request.uri} -> $asset");
- // TODO(rnystrom): Set content-type based on asset type.
- return request.response.addStream(stream).then((_) {
- request.response.close();
- });
- }).catchError((error) {
- log.error("$_red${request.method}$_none "
- "${request.uri} -> $error");
-
- // If we couldn't read the asset, handle the error gracefully.
- if (error is FileException) {
- // Assume this means the asset was a file-backed source asset
- // and we couldn't read it, so treat it like a missing asset.
- notFound(request, error);
- return;
- }
-
- // Otherwise, it's some internal error.
- request.response.statusCode = 500;
- request.response.reasonPhrase = "Internal Error";
- request.response.write(error);
- request.response.close();
- });
- }).catchError((error) {
- log.error("$_red${request.method}$_none ${request.uri} -> $error");
- if (error is! AssetNotFoundException) {
- completer.completeError(error);
- return;
- }
-
- notFound(request, error);
- });
+ _barback.getAssetById(id).then((asset) {
+ return validateStream(asset.read()).then((stream) {
+ log.message(
+ "$_green${request.method}$_none ${request.uri} -> $asset");
+ // TODO(rnystrom): Set content-type based on asset type.
+ return request.response.addStream(stream).then((_) {
+ request.response.close();
});
+ }).catchError((error) {
+ log.error("$_red${request.method}$_none "
+ "${request.uri} -> $error");
+
+ // If we couldn't read the asset, handle the error gracefully.
+ if (error is FileException) {
+ // Assume this means the asset was a file-backed source asset
+ // and we couldn't read it, so treat it like a missing asset.
+ notFound(request, error);
+ return;
+ }
+
+ // Otherwise, it's some internal error.
+ request.response.statusCode = 500;
+ request.response.reasonPhrase = "Internal Error";
+ request.response.write(error);
+ request.response.close();
});
+ }).catchError((error) {
+ log.error("$_red${request.method}$_none ${request.uri} -> $error");
+ if (error is! AssetNotFoundException) {
+ _commandCompleter.completeError(error);
+ return;
+ }
- return completer.future;
+ notFound(request, error);
});
}
@@ -186,4 +187,91 @@ class ServeCommand extends PubCommand {
return new AssetId(entrypoint.root.name,
path.url.join("web", path.url.joinAll(parts)));
}
+
+ /// Creates the [Barback] instance and listens to its outputs.
+ void initBarback() {
+ assert(_provider != null);
+
+ _barback = new Barback(_provider);
+
+ _barback.results.listen((result) {
+ if (result.succeeded) {
+ // TODO(rnystrom): Report using growl/inotify-send where available.
+ log.message("Build completed ${_green}successfully$_none");
+ } else {
+ log.message("Build completed with "
+ "${_red}${result.errors.length}$_none errors.");
+ }
+ });
+
+ _barback.errors.listen((error) {
+ log.error("${_red}Build error:\n$error$_none");
+ });
+ }
+
+ /// Adds all of the source assets in the provided packages to barback and
+ /// then watches the public directories for changes.
+ void watchSources() {
+ assert(_provider != null);
+ assert(_barback != null);
+
+ for (var package in _provider.packages) {
+ // Add the initial sources.
+ _barback.updateSources(listAssets(package));
+
+ // Watch the visible package directories for changes.
+ var packageDir = _provider.getPackageDir(package);
+
+ for (var name in getPublicDirectories(package)) {
+ var subdirectory = path.join(packageDir, name);
+ var watcher = new DirectoryWatcher(subdirectory);
+ watcher.events.listen((event) {
+ var relativePath = path.relative(event.path, from: packageDir);
+ var id = new AssetId(package, relativePath);
+ if (event.type == ChangeType.REMOVE) {
+ _barback.removeSources([id]);
+ } else {
+ _barback.updateSources([id]);
+ }
+ });
+ }
+ }
+ }
+
+ /// Lists all of the visible files in [package].
+ ///
+ /// This is the recursive contents of the "asset" and "lib" directories (if
+ /// present). If [package] is the entrypoint package, it also includes the
+ /// contents of "web".
+ List<AssetId> listAssets(String package) {
+ var files = <AssetId>[];
+
+ for (var dirPath in getPublicDirectories(package)) {
+ var packageDir = _provider.getPackageDir(package);
+ var dir = path.join(packageDir, dirPath);
+ if (!dirExists(dir)) continue;
+ for (var entry in listDir(dir, recursive: true)) {
+ // Ignore "packages" symlinks if there.
+ if (path.split(entry).contains("packages")) continue;
+
+ // Skip directories.
+ if (!fileExists(entry)) continue;
+
+ // AssetId paths use "/" on all platforms.
+ var relative = path.relative(entry, from: packageDir);
+ relative = path.toUri(relative).path;
+ files.add(new AssetId(package, relative));
+ }
+ }
+
+ return files;
+ }
+
+ /// Gets the names of the top-level directories in [package] whose contents
+ /// should be provided as source assets.
+ Iterable<String> getPublicDirectories(String package) {
+ var directories = ["asset", "lib"];
+ if (package == entrypoint.root.name) directories.add("web");
+ return directories;
+ }
}
« no previous file with comments | « no previous file | sdk/lib/_internal/pub/lib/src/pub_package_provider.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698