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; |
+ } |
} |